text: # Copyright 2016 Red Hat, Inc.
  # All Rights Reserved.
  #
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
  # not use this file except in compliance with the License. You may obtain
  # a copy of the License at
  #
  #     http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  # License for the specific language governing permissions and limitations
  # under the License.
  import logging
  from operator import itemgetter
  import shutil
  import tempfile
  import yaml
  
  from heatclient import exc as heatexceptions
  from mistral_lib import actions
  from oslo_concurrency import processutils
  import six
  from swiftclient import exceptions as swiftexceptions
  
  from tripleo_common.actions import base
  from tripleo_common import constants
  from tripleo_common import exception
  from tripleo_common.utils import plan as plan_utils
  from tripleo_common.utils import roles as roles_utils
  from tripleo_common.utils import swift as swiftutils
  from tripleo_common.utils.validations import pattern_validator
  
  
  LOG = logging.getLogger(__name__)
  
  default_container_headers = {
      constants.TRIPLEO_META_USAGE_KEY: 'plan'
  }
  
  
  class CreateContainerAction(base.TripleOAction):
      """Creates an object container
  
      This action creates an object container for a given name.  If a container
      with the same name already exists an exception is raised.
      """
  
      def __init__(self, container):
          super(CreateContainerAction, self).__init__()
          self.container = container
  
      def run(self, context):
          oc = self.get_object_client(context)
  
          # checks to see if a container has a valid name
          if not pattern_validator(constants.PLAN_NAME_PATTERN, self.container):
              message = ("Unable to create plan. The plan name must "
                         "only contain letters, numbers or dashes")
              return actions.Result(error=message)
  
          # checks to see if a container with that name exists
          if self.container in [container["name"] for container in
                                oc.get_account()[1]]:
              result_string = ("A container with the name %s already"
                               " exists.") % self.container
              return actions.Result(error=result_string)
          oc.put_container(self.container, headers=default_container_headers)
  
  
  class ListPlansAction(base.TripleOAction):
      """Lists deployment plans
  
      This action lists all deployment plans residing in the undercloud.  A
      deployment plan consists of a container marked with metadata
      'x-container-meta-usage-tripleo'.
      """
  
      def run(self, context):
          # Plans consist of a container object marked with metadata to ensure it
          # isn't confused with another container
          plan_list = []
          oc = self.get_object_client(context)
  
          for item in oc.get_account()[1]:
              container = oc.get_container(item['name'])[0]
              if constants.TRIPLEO_META_USAGE_KEY in container.keys():
                  plan_list.append(item['name'])
          return list(set(plan_list))
  
  
  class DeletePlanAction(base.TripleOAction):
      """Deletes a plan and associated files
  
      Deletes a plan by deleting the container matching plan_name. It
      will not delete the plan if a stack exists with the same name.
  
      Raises StackInUseError if a stack with the same name as plan_name
      exists.
      """
  
      def __init__(self, container):
          super(DeletePlanAction, self).__init__()
          self.container = container
  
      def run(self, context):
          error_text = None
          # heat throws HTTPNotFound if the stack is not found
          try:
              stack = self.get_orchestration_client(context).stacks.get(
                  self.container, resolve_outputs=False)
          except heatexceptions.HTTPNotFound:
              pass
          else:
              if stack is not None:
                  raise exception.StackInUseError(name=self.container)
  
          try:
              swift = self.get_object_client(context)
              swiftutils.delete_container(swift, self.container)
              swiftutils.delete_container(swift,
                                          "%s-swift-rings" % self.container)
              swiftutils.delete_container(swift,
                                          "%s-messages" % self.container)
          except swiftexceptions.ClientException as ce:
              LOG.exception("Swift error deleting plan.")
              error_text = ce.msg
          except Exception as err:
              LOG.exception("Error deleting plan.")
              error_text = six.text_type(err)
  
          if error_text:
              return actions.Result(error=error_text)
  
  
  class ListRolesAction(base.TripleOAction):
      """Returns a deployment plan's roles
  
      DEPRECATED, please use tripleo.plan_management.v1.list_roles workflow
  
      Parses roles_data.yaml and returns the names of all available roles.
  
      :param container: name of the Swift container / plan name
      :param detail: if false(default), displays role names only.  if true,
                     returns all roles data
      :return: list of roles in the container's deployment plan
      """
  
      def __init__(self, container=constants.DEFAULT_CONTAINER_NAME,
                   role_file_name=constants.OVERCLOUD_J2_ROLES_NAME,
                   detail=False):
          super(ListRolesAction, self).__init__()
          self.container = container
          self.role_file_name = role_file_name
          self.detail = detail
  
      def run(self, context):
          try:
              swift = self.get_object_client(context)
              roles_data = yaml.safe_load(swiftutils.get_object_string(
                  swift, self.container, self.role_file_name))
          except Exception as err:
              err_msg = ("Error retrieving roles data from deployment plan: %s"
                         % err)
              LOG.exception(err_msg)
              return actions.Result(error=err_msg)
  
          if self.detail:
              return roles_data
          else:
              return [role['name'] for role in roles_data]
  
  
  class ExportPlanAction(base.TripleOAction):
      """Exports a deployment plan
  
      This action exports a deployment plan with a given name. The plan
      templates are downloaded from the Swift container, packaged up in a tarball
      and uploaded to Swift.
      """
  
      def __init__(self, plan, delete_after, exports_container):
          super(ExportPlanAction, self).__init__()
          self.plan = plan
          self.delete_after = delete_after
          self.exports_container = exports_container
  
      def run(self, context):
          swift = self.get_object_client(context)
          swift_service = self.get_object_service(context)
  
          tmp_dir = tempfile.mkdtemp()
          tarball_name = '%s.tar.gz' % self.plan
  
          try:
              swiftutils.download_container(swift, self.plan, tmp_dir)
              swiftutils.create_and_upload_tarball(
                  swift_service, tmp_dir, self.exports_container, tarball_name,
                  delete_after=self.delete_after)
          except swiftexceptions.ClientException as err:
              msg = "Error attempting an operation on container: %s" % err
              return actions.Result(error=msg)
          except (OSError, IOError) as err:
              msg = "Error while writing file: %s" % err
              return actions.Result(error=msg)
          except processutils.ProcessExecutionError as err:
              msg = "Error while creating a tarball: %s" % err
              return actions.Result(error=msg)
          except Exception as err:
              msg = "Error exporting plan: %s" % err
              return actions.Result(error=msg)
          finally:
              shutil.rmtree(tmp_dir)
  
  
  class UpdateNetworksAction(base.TripleOAction):
      def __init__(self, networks, current_networks, replace_all=False):
          super(UpdateNetworksAction, self).__init__()
          self.networks = networks
          self.current_networks = current_networks
          self.replace_all = replace_all
  
      def run(self, context):
          network_data_to_save = self.networks or []
  
          # if replace_all flag is true, discard current networks and save input
          # if replace_all flag is false, merge input into current networks
          if not self.replace_all:
              # merge the networks_data and the network_input into networks
              # to be saved
              network_data_to_save = [net for net in {
                  x['name']: x for x in
                  self.current_networks + self.networks
              }.values()]
  
          return actions.Result(data={'network_data': network_data_to_save})
  
  
  class ValidateRolesDataAction(base.TripleOAction):
      """Validates Roles Data
  
      Validates the format of input (verify that each role in input has the
      required attributes set. see README in roles directory in t-h-t),
      validates that roles in input exist in roles directory in deployment plan
      """
  
      def __init__(self, roles, available_roles,
                   container=constants.DEFAULT_CONTAINER_NAME):
          super(ValidateRolesDataAction, self).__init__()
          self.container = container
          self.roles = roles
          self.available_roles = available_roles
  
      def run(self, context):
          err_msg = ""
          # validate roles in input exist in roles directory in t-h-t
          try:
              roles_utils.check_role_exists(
                  [role['name'] for role in self.available_roles],
                  [role['name'] for role in self.roles])
          except Exception as chk_err:
              err_msg = str(chk_err)
  
          # validate role yaml
          for role in self.roles:
              try:
                  roles_utils.validate_role_yaml(yaml.safe_dump([role]))
              except exception.RoleMetadataError as rme:
                  if 'name' in role:
                      err_msg += "\n%s for %s" % (str(rme), role['name'])
                  else:
                      err_msg += "\n%s" % str(rme)
  
          if err_msg:
              return actions.Result(error=err_msg)
          return actions.Result(data=True)
  
  
  class UpdateRolesAction(base.TripleOAction):
      """Updates roles_data.yaml object in plan with given roles.
  
      :param roles: role input data (json)
      :param current_roles: data from roles_data.yaml file in plan (json)
      :param replace_all: boolean value indicating if input roles should merge
      with or replace data from roles_data.yaml.  Defaults to False (merge)
      :param container: name of the Swift container / plan name
      """
  
      def __init__(self, roles, current_roles, replace_all=False,
                   container=constants.DEFAULT_CONTAINER_NAME):
          super(UpdateRolesAction, self).__init__()
          self.container = container
          self.roles = roles
          self.current_roles = current_roles
          self.replace_all = replace_all
  
      def run(self, context):
          role_data_to_save = self.roles
  
          # if replace_all flag is true, discard current roles and save input
          # if replace_all flag is false, merge input into current roles
          if not self.replace_all:
              # merge the roles_data and the role_input into roles to be saved
              role_data_to_save = [role for role in {
                  x['name']: x for x in
                  self.current_roles + self.roles
              }.values()]
  
          # ensure required primary tag exists in roles to be saved
          primary = [role for role in role_data_to_save if
                     'tags' in role and 'primary' in role['tags']]
          if len(primary) < 1:
              # throw error
              raise exception.RoleMetadataError("At least one role must contain"
                                                " a 'primary' tag.")
  
          # sort the data to have a predictable result
          save_roles = sorted(role_data_to_save, key=itemgetter('name'),
                              reverse=True)
          return actions.Result(data={'roles': save_roles})
  
  
  class RemoveNoopDeployStepAction(base.TripleOAction):
      """Remove all the pre, post and deploy step in the plan-environment.
  
      :param container: name of the Swift container / plan name
      """
  
      def __init__(self, container=constants.DEFAULT_CONTAINER_NAME):
          super(RemoveNoopDeployStepAction, self).__init__()
          self.container = container
  
      def run(self, context):
          # get the stack. Error if doesn't exist
          heat = self.get_orchestration_client(context)
          try:
              stack = heat.stacks.get(self.container)
          except heatexceptions.HTTPNotFound:
              msg = "Error retrieving stack: %s" % self.container
              LOG.exception(msg)
              return actions.Result(error=msg)
  
          swift = self.get_object_client(context)
  
          # Get output and check if DeployStep are None
          removals = ['OS::TripleO::DeploymentSteps']
          for output in stack.to_dict().get('outputs', {}):
              if output['output_key'] == 'RoleData':
                  for role in output['output_value']:
                      removals.append("OS::TripleO::Tasks::%sPreConfig" % role)
                      removals.append("OS::TripleO::Tasks::%sPostConfig" % role)
  
          plan_env = plan_utils.get_env(swift, self.container)
          self.remove_noops_from_env(removals, plan_env)
          plan_utils.put_env(swift, plan_env)
  
          user_env = plan_utils.get_user_env(swift, self.container)
          self.remove_noops_from_env(removals, user_env)
          plan_utils.put_user_env(swift, self.container, user_env)
  
      def remove_noops_from_env(self, removals, env):
          # Remove noop Steps
          for rm in removals:
              if rm in env.get('resource_registry', {}):
                  if env['resource_registry'][rm] == 'OS::Heat::None':
                      env['resource_registry'].pop(rm)
  