text: #!/usr/bin/python
  # -*- coding: utf-8 -*-
  # Copyright 2020 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.
  
  from __future__ import absolute_import, division, print_function
  __metaclass__ = type
  
  import copy
  import os
  
  import tenacity
  import yaml
  
  from ansible.errors import AnsibleActionFail
  from ansible.plugins.action import ActionBase
  from ansible.utils.display import Display
  
  
  DISPLAY = Display()
  
  DOCUMENTATION = """
  module: container_systemd
  author:
    - "TripleO team"
  version_added: '2.9'
  short_description: Create systemd files and manage services to run containers
  notes: []
  description:
    - Manage the systemd unit files for containers with a restart policy and
      then make sure the services are started so the containers are running.
      It takes the container config data in entry to figure out how the unit
      files will be configured. It returns a list of services that were
      restarted.
  requirements:
    - None
  options:
    container_config:
      description:
        - List of container configurations
      type: list
      elements: dict
    systemd_healthchecks:
      default: true
      description:
        - Whether or not we cleanup the old healthchecks with SystemD.
      type: boolean
    debug:
      default: false
      description:
        - Whether or not debug is enabled.
      type: boolean
  """
  EXAMPLES = """
  - name: Manage container systemd services
    container_systemd:
      container_config:
        - keystone:
            image: quay.io/tripleo/keystone
            restart: always
        - mysql:
            image: quay.io/tripleo/mysql
            stop_grace_period: 25
            restart: always
  """
  RETURN = """
  restarted:
      description: List of services that were restarted
      returned: always
      type: list
      sample:
        - tripleo_keystone.service
        - tripleo_mysql.service
  """
  
  
  class ActionModule(ActionBase):
      """Class for the container_systemd action plugin.
      """
  
      _VALID_ARGS = yaml.safe_load(DOCUMENTATION)['options']
  
      def _get_args(self):
          missing = []
          args = {}
  
          for option, vals in self._VALID_ARGS.items():
              if 'default' not in vals:
                  if self._task.args.get(option, None) is None:
                      missing.append(option)
                      continue
                  args[option] = self._task.args.get(option)
              else:
                  args[option] = self._task.args.get(option, vals['default'])
  
          if missing:
              raise AnsibleActionFail('Missing required parameters: {}'.format(
                  ', '.join(missing)))
          return args
  
      def _cleanup_requires(self, container_names, task_vars):
          """Cleanup systemd requires files.
  
          :param container_names: List of container names.
          :param task_vars: Dictionary of Ansible task variables.
          """
          for name in container_names:
              path = "/etc/systemd/system/tripleo_{}.requires".format(name)
              if self.debug:
                  DISPLAY.display('Removing {} file'.format(path))
              results = self._execute_module(
                  module_name='file',
                  module_args=dict(path=path, state='absent'),
                  task_vars=task_vars
              )
              if results.get('changed', False):
                  self.changed = True
  
      def _delete_service(self, name, task_vars):
          """Stop and disable a systemd service.
  
          :param name: String for service name to stop and disable.
          :param task_vars: Dictionary of Ansible task variables.
          """
          tvars = copy.deepcopy(task_vars)
          results = self._execute_module(
              module_name='systemd',
              module_args=dict(state='stopped',
                               name='tripleo_{}_healthcheck.timer'.format(name),
                               enabled=False,
                               daemon_reload=False),
              task_vars=tvars
          )
          return results
  
      def _cleanup_healthchecks(self, container_names, task_vars):
          """Cleanup systemd healthcheck files.
  
          :param container_names: List of container names.
          :param task_vars: Dictionary of Ansible task variables.
          """
          systemd_reload = False
          for cname in container_names:
              h_path = os.path.join('/etc/systemd/system',
                                    'tripleo_{}_healthcheck.timer'.format(cname))
              healthcheck_stat = self._execute_module(
                  module_name='stat',
                  module_args=dict(path=h_path),
                  task_vars=task_vars
              )
              if healthcheck_stat.get('stat', {}).get('exists', False):
                  if self.debug:
                      DISPLAY.display('Cleaning-up systemd healthcheck for '
                                      '{}'.format(cname))
                  self._delete_service(cname, task_vars)
                  files_ext = ['service', 'timer']
                  for ext in files_ext:
                      sysd_base = '/etc/systemd/system'
                      file_path = 'tripleo_{}_healthcheck.{}'.format(cname, ext)
                      full_path = os.path.join(sysd_base, file_path)
                      results = self._execute_module(
                          module_name='file',
                          module_args=dict(path=full_path, state='absent'),
                          task_vars=task_vars
                      )
                      if results.get('changed', False):
                          self.changed = True
                          systemd_reload = True
          if systemd_reload:
              self._systemd_reload(task_vars)
  
      def _get_unit_template(self):
          """Return systemd unit template data
  
          :returns data: Template data.
          """
          if self._task._role:
              file_path = self._task._role._role_path
          else:
              file_path = self._loader.get_basedir()
          # NOTE: if templates doesn't exist, it'll always return
          # file_path/systemd-service.j2
          # This file is required to exist from the
          # tripleo_container_manage role, as there is no
          # parameter to override it now.
          source = self._loader.path_dwim_relative(
              file_path,
              'templates',
              'systemd-service.j2'
          )
          if not os.path.exists(source):
              raise AnsibleActionFail('Template {} was '
                                      'not found'.format(source))
          with open(source) as template_file:
              data = template_file.read()
          return data
  
      def _create_units(self, container_config, task_vars):
          """Create system units and get list of changed services
  
          :param container_config: List of dictionaries for container configs.
          :param task_vars: Dictionary of Ansible task variables.
          :returns changed_containers: List of containers which has a new unit.
          """
          try:
              remote_user = self._get_remote_user()
          except Exception:
              remote_user = task_vars.get('ansible_user')
              if not remote_user:
                  remote_user = self._play_context.remote_user
          tmp = self._make_tmp_path(remote_user)
          unit_template = self._get_unit_template()
          changed_containers = []
          for container in container_config:
              for name, config in container.items():
                  dest = '/etc/systemd/system/tripleo_{}.service'.format(name)
                  task_vars['container_data_unit'] = container
                  unit = (self._templar.template(unit_template,
                                                 preserve_trailing_newlines=True,
                                                 escape_backslashes=False,
                                                 convert_data=False))
                  del task_vars['container_data_unit']
                  remote_data = self._transfer_data(
                      self._connection._shell.join_path(tmp, 'source'), unit)
  
                  results = self._execute_module(
                      module_name='copy',
                      module_args=dict(src=remote_data,
                                       dest=dest,
                                       mode='0644',
                                       owner='root',
                                       group='root'),
                      task_vars=task_vars)
                  if results.get('changed', False):
                      changed_containers.append(name)
          if self.debug:
              DISPLAY.display('Systemd unit files were created or updated for: '
                              '{}'.format(changed_containers))
          return changed_containers
  
      def _systemd_reload(self, task_vars):
          """Reload systemd to load new units.
  
          :param task_vars: Dictionary of Ansible task variables.
          """
          if self.debug:
              DISPLAY.display('Running systemd daemon reload')
          results = self._execute_module(
              module_name='systemd',
              module_args=dict(daemon_reload=True),
              task_vars=task_vars
          )
          if results.get('changed', False):
              self.changed = True
  
      @tenacity.retry(
          reraise=True,
          stop=tenacity.stop_after_attempt(5),
          wait=tenacity.wait_fixed(5)
      )
      def _manage_service(self, name, state, task_vars):
          """Manage a systemd service with retries and delay.
  
          :param name: String for service name to manage.
          :param state: String for service state.
          :param task_vars: Dictionary of Ansible task variables.
          """
          tvars = copy.deepcopy(task_vars)
          results = self._execute_module(
              module_name='systemd',
              module_args=dict(state=state,
                               name='tripleo_{}.service'.format(name),
                               enabled=True,
                               daemon_reload=False),
              task_vars=tvars
          )
          try:
              if 'Result' in results['status']:
                  if results['status']['Result'] == 'success':
                      if results.get('changed', False):
                          self.changed = True
                          self.restarted.append('tripleo_{}'
                                                '.service'.format(name))
                      return
          except KeyError:
              # if 'systemd' task failed to start the service, the 'status'
              # key doesn't exist, so we'll use the final raise to report the
              # issue if the service never start after the attempts.
              pass
          raise AnsibleActionFail('Service {} has not started yet'.format(name))
  
      def _restart_services(self, service_names, task_vars):
          """Restart systemd services.
  
          :param service_names: List of services to restart.
          :param task_vars: Dictionary of Ansible task variables.
          """
          for name in service_names:
              if self.debug:
                  DISPLAY.display('Restarting systemd service for '
                                  '{}'.format(name))
              self._manage_service(name=name, state='restarted',
                                   task_vars=task_vars)
  
      def _ensure_started(self, service_names, task_vars):
          """Ensure systemd services are started.
  
          :param service_names: List of services to start.
          :param task_vars: Dictionary of Ansible task variables.
          """
          for name in service_names:
              if self.debug:
                  DISPLAY.display('Ensure that systemd service for '
                                  '{} is started'.format(name))
              self._manage_service(name=name, state='started',
                                   task_vars=task_vars)
  
      def run(self, tmp=None, task_vars=None):
          self.changed = False
          self.restarted = []
          already_created = []
  
          if task_vars is None:
              task_vars = dict()
          result = super(ActionModule, self).run(tmp, task_vars)
          del tmp
  
          # parse args
          args = self._get_args()
  
          container_config = args['container_config']
          systemd_healthchecks = args['systemd_healthchecks']
          self.debug = args['debug']
  
          container_names = []
          for container in container_config:
              for name, config in container.items():
                  container_names.append(name)
  
          self._cleanup_requires(container_names, task_vars)
  
          if systemd_healthchecks:
              self._cleanup_healthchecks(container_names, task_vars)
  
          changed_services = self._create_units(container_config, task_vars)
          if len(changed_services) > 0:
              self._systemd_reload(task_vars)
          self._restart_services(changed_services, task_vars)
          for c in container_names:
              # For services that didn't restart, make sure they're started
              if c not in changed_services:
                  already_created.append(c)
          if len(already_created) > 0:
              self._ensure_started(already_created, task_vars)
  
          result['changed'] = self.changed
          result['restarted'] = self.restarted
          return result
  