#!/usr/bin/python3 from os import readlink, walk, chdir, getcwd, makedirs from os.path import join, islink, isdir from typing import Dict, Any from shutil import rmtree, copy2 import tarfile from ansible.module_utils._text import to_text from ansible import constants from rougail.template import base from rougail.error import TemplateError from risotto.machine import build_files, INSTALL_DIR, INSTALL_CONFIG_DIR, INSTALL_TMPL_DIR, INSTALL_IMAGES_DIR, INSTALL_TESTS_DIR from risotto.utils import custom_filters try: from ansible.plugins.action import ActionBase from ansible.module_utils.basic import AnsibleModule class FakeModule(AnsibleModule): def __init__(self): pass from ansible.plugins.action.template import ActionModule as TmplActionModule except: class ActionBase(): def __init__(self, *args, **kwargs): raise Exception('works only with ansible') ARCHIVES_DIR = '/tmp/new_configurations' def is_diff(server_name, remote_directories, certificates, ): ret = {} module = FakeModule() current_path = getcwd() root = join(INSTALL_DIR, INSTALL_CONFIG_DIR, server_name) chdir(root) search_paths = [join(directory[2:], f) for directory, subdirectories, files in walk('.') for f in files] chdir(current_path) for path in search_paths: if path not in remote_directories: return True full_path = join(root, path) if not islink(full_path): if remote_directories[path] != module.digest_from_file(full_path, 'sha256'): return True elif remote_directories[path] != readlink(full_path): return True remote_directories.pop(path) if remote_directories: for certificate in certificates: for typ in ['name', 'private', 'authority']: if not typ in certificate: continue name = certificate[typ][1:] if name in remote_directories: remote_directories.pop(name) if remote_directories: return True return False class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): super(ActionModule, self).run(tmp, task_vars) module_args = self._task.args.copy() hostname = module_args.pop('hostname') only_machine = module_args.pop('only_machine') configure_host = module_args.pop('configure_host') copy_tests = module_args.pop('copy_tests') # define ansible engine base.ENGINES['ansible'] = Tmpl(task_vars, self._task, self._connection, self._play_context, self._loader, self._templar, self._shared_loader_obj, ) if 'copy_templates' in module_args: copy_templates = module_args.pop('copy_templates') else: copy_templates = False directories, certificates = build_files(hostname, only_machine, False, copy_tests, ) module_args['directories'] = list(directories.values()) module_args['directories'].append('/var/lib/risotto/images_files') remote = self._execute_module(module_name='compare', module_args=module_args, task_vars=task_vars, ) if remote.get('failed'): if 'module_stdout' in remote: msg = remote['module_stdout'] else: msg = remote['msg'] raise Exception(f'error in remote action: {msg}') if copy_templates: build_files(hostname, only_machine, True, copy_tests, ) machines_changed = [] for machine, directory in directories.items(): if directory not in remote['directories']: machines_changed.append(machine) continue if is_diff(machine, remote['directories'][directory], certificates['certificates'].get(machine, []), ): machines_changed.append(machine) current_path = getcwd() if isdir(ARCHIVES_DIR): rmtree(ARCHIVES_DIR) makedirs(ARCHIVES_DIR) if machines_changed: self._execute_module(module_name='file', module_args={'path': ARCHIVES_DIR, 'state': 'absent', }, task_vars=task_vars, ) self._execute_module(module_name='file', module_args={'path': ARCHIVES_DIR, 'state': 'directory', }, task_vars=task_vars, ) machines = machines_changed.copy() if self._task.args['hostname'] in machines_changed: machine = self._task.args['hostname'] machines.remove(machine) chdir(f'{task_vars["host_install_dir"]}/{INSTALL_CONFIG_DIR}/{machine}') tar_filename = f'{ARCHIVES_DIR}/host.tar' with tarfile.open(tar_filename, 'w') as archive: archive.add('.') chdir(current_path) self._transfer_file(tar_filename, tar_filename) # archive and send if machines: chdir(f'{task_vars["host_install_dir"]}/{INSTALL_CONFIG_DIR}') tar_filename = f'{ARCHIVES_DIR}/machines.tar' with tarfile.open(tar_filename, 'w') as archive: for machine in machines: if machine == self._task.args['hostname']: continue archive.add(f'{machine}') self._transfer_file(tar_filename, tar_filename) else: machines = [] # archive and send chdir(f'{task_vars["host_install_dir"]}/{INSTALL_IMAGES_DIR}/') tar_filename = f'{ARCHIVES_DIR}/{INSTALL_IMAGES_DIR}.tar' with tarfile.open(tar_filename, 'w') as archive: archive.add('.') self._execute_module(module_name='file', module_args={'path': '/tmp/new_configurations', 'state': 'directory', }, task_vars=task_vars, ) self._transfer_file(tar_filename, tar_filename) # tests self._execute_module(module_name='file', module_args={'path': '/var/lib/risotto/tests', 'state': 'absent', }, task_vars=task_vars, ) if copy_tests: chdir(f'{task_vars["host_install_dir"]}/{INSTALL_TESTS_DIR}/') tar_filename = f'{ARCHIVES_DIR}/{INSTALL_TESTS_DIR}.tar' with tarfile.open(tar_filename, 'w') as archive: archive.add('.') self._transfer_file(tar_filename, tar_filename) # templates self._execute_module(module_name='file', module_args={'path': '/var/lib/risotto/templates', 'state': 'absent', }, task_vars=task_vars, ) if copy_templates: chdir(f'{task_vars["host_install_dir"]}/') tar_filename = f'{ARCHIVES_DIR}/{INSTALL_TMPL_DIR}.tar' with tarfile.open(tar_filename, 'w') as archive: archive.add(INSTALL_TMPL_DIR) self._transfer_file(tar_filename, tar_filename) remote = self._execute_module(module_name='unarchive', module_args={'remote_src': True, 'src': '/tmp/new_configurations/templates.tar', 'dest': '/var/lib/risotto', }, task_vars=task_vars, ) chdir(current_path) changed = machines_changed != [] return dict(ansible_facts=dict({}), changed=changed, machines_changed=machines, host_changed=self._task.args['hostname'] in machines_changed, ) class FakeCopy: def __init__(self, task): self.task = task def run(self, *args, **kwargs): copy2(self.task.args['src'], self.task.args['dest']) return {} class FakeGet: def __init__(self, klass): self.klass = klass def fake_get(self, action, *args, task, **kwargs): if action == 'ansible.legacy.copy': return FakeCopy(task) return self.klass.ori_get(action, *args, task=task, **kwargs) class Tmpl(TmplActionModule): def __init__(self, task_vars, *args): super().__init__(*args) self.task_vars = task_vars def _early_needs_tmp_path(self): # do not create tmp remotely return False def process(self, filename: str, source: str, true_destfilename: str, destfilename: str, destdir: str, variable: Any, index: int, rougail_variables_dict: Dict, eosfunc: Dict, extra_variables: Any=None, ): if source is not None: # pragma: no cover raise TemplateError(_('source is not supported for ansible')) task_vars = rougail_variables_dict | self.task_vars if variable is not None: task_vars['rougail_variable'] = variable if index is not None: task_vars['rougail_index'] = index if extra_variables: task_vars['extra_variables'] = extra_variables task_vars['rougail_filename'] = true_destfilename task_vars['rougail_destination_dir'] = destdir self._task.args['src'] = filename self._task.args['dest'] = destfilename # add custom filter custom_filters.update(eosfunc) # do not copy file in host but stay it locally self._shared_loader_obj.action_loader.ori_get = self._shared_loader_obj.action_loader.get self._shared_loader_obj.action_loader.get = FakeGet(self._shared_loader_obj.action_loader).fake_get # template ret = self.run(task_vars=task_vars) # restore get function self._shared_loader_obj.action_loader.get = self._shared_loader_obj.action_loader.ori_get # remove custom filter custom_filters.clear() if ret.get('failed'): raise TemplateError(f'error while templating "{filename}": {ret["msg"]}')