From afd28627f322e92560d15a51336b489a75f96c5b Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Mon, 23 Jan 2023 20:23:32 +0100 Subject: [PATCH] simplify ansible role --- ansible/action_plugins/build_images.py | 56 ++- ansible/action_plugins/rougail.py | 226 +++++++++--- ansible/host.yml | 160 ++------- ansible/host_modified.yml | 74 ++++ ansible/installations | 1 - ansible/inventory.json | 15 - ansible/inventory.py | 5 +- ansible/library/compare.py | 73 ++-- ansible/machine.yml | 126 +------ ansible/machines.yml | 33 +- ansible/playbook.txt | 23 -- ansible/playbook.yml | 47 +-- ansible/remove_image.yml | 14 + ansible/sbin/build_image | 26 +- ansible/sbin/compare_image | 20 +- ansible/sbin/make_changelog | 6 +- ansible/sbin/make_volatile | 2 +- ansible/sbin/test_images | 30 ++ ansible/sbin/update_images | 41 ++- doc/authentification.svg | 405 +++++++++++++++++++++ doc/example_smtp.png | Bin 0 -> 102822 bytes doc/example_smtp.svg | 466 +++++++++++++++++++++++++ sbin/risotto_auto_doc | 60 +++- sbin/risotto_templates | 7 +- src/risotto/image.py | 37 +- src/risotto/machine.py | 204 +++++++---- src/risotto/rougail/annotator.py | 88 +++-- 27 files changed, 1648 insertions(+), 597 deletions(-) create mode 100644 ansible/host_modified.yml delete mode 120000 ansible/installations delete mode 100644 ansible/inventory.json delete mode 100644 ansible/playbook.txt create mode 100644 ansible/remove_image.yml create mode 100755 ansible/sbin/test_images create mode 100644 doc/authentification.svg create mode 100644 doc/example_smtp.png create mode 100644 doc/example_smtp.svg diff --git a/ansible/action_plugins/build_images.py b/ansible/action_plugins/build_images.py index 10ccdad..5389692 100644 --- a/ansible/action_plugins/build_images.py +++ b/ansible/action_plugins/build_images.py @@ -8,51 +8,37 @@ from ansible.plugins.action import ActionBase from risotto.utils import RISOTTO_CONFIG + + class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): super(ActionModule, self).run(tmp, task_vars) module_args = self._task.args.copy() modules = module_args['modules'] + copy_tests = module_args.get('copy_tests', False) dataset_directories = RISOTTO_CONFIG['directories']['datasets'] install_dir = join('/tmp/risotto/images') if isdir(install_dir): rmtree(install_dir) + if copy_tests: + install_tests_dir = join('/tmp/risotto/tests') + if isdir(install_tests_dir): + rmtree(install_tests_dir) for module_name, depends in modules.items(): for dataset_directory in dataset_directories: for depend in depends: - manual = join(dataset_directory, depend, 'manual', 'image') - if not isdir(manual): - continue - for filename in listdir(manual): - src_file = join(manual, filename) - dst_file = join(install_dir, module_name, filename) - if isdir(src_file): - if not isdir(dst_file): - makedirs(dst_file) - for subfilename in listdir(src_file): - if not isfile(dst_file): - src = join(src_file, subfilename) - dst = join(dst_file, subfilename) - if isfile(src): - copy2(src, dst) - else: - copytree(src, dst) - elif not isfile(dst_file): - dst = dirname(dst_file) - if not isdir(dst): - makedirs(dst) - if isfile(src_file): - copy2(src_file, dst_file) - else: - copytree(src_file, dst_file) + if copy_tests: + tests_dir = join(dataset_directory, depend, 'tests') + if isdir(tests_dir): + for filename in listdir(tests_dir): + src_file = join(tests_dir, filename) + dst_file = join(install_tests_dir, module_name, filename) + copy(src_file, dst_file) +# manual = join(dataset_directory, depend, 'manual', 'image') +# if not isdir(manual): +# continue +# for filename in listdir(manual): +# src_file = join(manual, filename) +# dst_file = join(install_dir, module_name, filename) +# copy(src_file, dst_file) return dict(ansible_facts=dict({})) -#A REFAIRE ICI tests_dir = join(as_dir, 'tests') -#A REFAIRE ICI if isdir(tests_dir): -#A REFAIRE ICI cfg.tests.append(tests_dir) -#A REFAIRE ICI# for filename in listdir(tests_dir): -#A REFAIRE ICI# src_file = join(tests_dir, filename) -#A REFAIRE ICI# dst_file = join(INSTALL_DIR, 'tests', filename) -#A REFAIRE ICI# applicationservice_copy(src_file, -#A REFAIRE ICI# dst_file, -#A REFAIRE ICI# False, -#A REFAIRE ICI# ) diff --git a/ansible/action_plugins/rougail.py b/ansible/action_plugins/rougail.py index cae79d9..0484919 100644 --- a/ansible/action_plugins/rougail.py +++ b/ansible/action_plugins/rougail.py @@ -1,8 +1,11 @@ #!/usr/bin/python3 from asyncio import run -from os import readlink -from os.path import join, islink -from risotto.machine import load, templates +from os import readlink, walk, chdir, getcwd, makedirs +from os.path import join, islink, isdir +from risotto.machine import load, templates, INSTALL_DIR, INSTALL_CONFIG_DIR, INSTALL_TMPL_DIR, INSTALL_IMAGES_DIR, INSTALL_TESTS_DIR +from rougail.utils import normalize_family +from shutil import rmtree +import tarfile try: from ansible.plugins.action import ActionBase from ansible.module_utils.basic import AnsibleModule @@ -10,59 +13,192 @@ try: def __init__(self): pass except: - import traceback - traceback.print_exc() class ActionBase(): def __init__(self, *args, **kwargs): raise Exception('works only with ansible') -async def build_files(server_name: str, +ARCHIVES_DIR = '/tmp/new_configurations' + + +async def build_files(hostname: str, + only_machine: str, just_copy: bool, + copy_tests: bool, ) -> None: - config = await load() - await templates(server_name, - config, - just_copy=just_copy, - ) + config = await load(copy_tests=copy_tests) + if only_machine: + machines = [only_machine] + else: + machines = [await subconfig.option.description() for subconfig in await config.option.list(type='optiondescription')] +# shasums = {} + directories = {} + for machine in machines: + if just_copy and hostname == machine: + continue + await templates(machine, + config, + just_copy=just_copy, + copy_manuals=True, + ) + #FIXME dest_dir? + + is_host = machine == hostname +# shasums[machine] = {'shasums': get_shasums(dest_dir, is_host)} + if is_host: +# shasums[machine]['config_dir'] = '/usr/local/lib' + directories[machine] = '/usr/local/lib' + else: +# shasums[machine]['config_dir'] = await config.option(normalize_family(machine)).option('general.config_dir').value.get() + directories[machine] = await config.option(normalize_family(machine)).option('general.config_dir').value.get() + return directories + + +def is_diff(server_name, remote_directories): + 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: + 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() - root_local = module_args.pop('root_local') - root_remote= module_args.pop('root_remote') - name = module_args.pop('hostname') - is_host = module_args.pop('is_host') - just_copy = module_args.get('just_copy', False) - module_args['root'] = root_remote - - run(build_files(name, just_copy)) - # - remote = self._execute_module(module_name='compare', module_args=module_args, task_vars=task_vars) + 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') + if 'copy_templates' in module_args: + copy_templates = module_args.pop('copy_templates') + else: + copy_templates = False + directories = run(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'): - raise Exception(f'error in remote action: {remote["module_stdout"]}') - # - module = FakeModule() - modified_files = [] - changed = False - for path in module_args['paths']: - full_path = join(root_local, path['name'][1:]) - if remote['compare'].get(path['name']): - if remote['compare'][path['name']]['type'] == 'file': - if remote['compare'][path['name']]['shasum'] == module.digest_from_file(full_path, 'sha256'): - continue - else: - # it's a symlink - if islink(full_path) and remote['compare'][path['name']]['name'] == readlink(full_path): - continue - changed = True - modified_files.append(path['name']) - if not is_host: - for old_file in remote['old_files']: - changed = True - # module_args['path'] = old_file - # module_args['state'] = 'absent' - # self._execute_module(module_name='ansible.builtin.file', module_args=module_args, task_vars=task_vars) - return dict(ansible_facts=dict({}), changed=changed) + if 'module_stdout' in remote: + msg = remote['module_stdout'] + else: + msg = remote['msg'] + raise Exception(f'error in remote action: {msg}') + if copy_templates: + run(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]): + 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._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) diff --git a/ansible/host.yml b/ansible/host.yml index ce5cb80..40285dc 100644 --- a/ansible/host.yml +++ b/ansible/host.yml @@ -8,113 +8,9 @@ update_cache: yes state: latest -- name: "Build host files" - rougail: - paths: "{{ vars[inventory_hostname]['services'] | fileslist(is_host=True) }}" - root_local: "{{ host_install_dir }}" - root_remote: "/" - hostname: "{{ inventory_hostname }}" - is_host: True - -- name: "Create /usr/local/lib/systemd/system" - file: - path: /usr/local/lib/systemd/system - state: directory - mode: 0755 - -- name: "Copy service file only if not exists" - when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] and item.value['engine'] != 'none' - copy: - src: '{{ host_install_dir }}/usr/local/lib/systemd/system/{{ item.value["doc"] }}' - force: no - dest: '/usr/local/lib/systemd/system/{{ item.value["doc"] }}' - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" - -- name: "Stop services" - when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] != 'none' - ansible.builtin.service: - name: "{{ item.value['doc'] }}" - state: stopped - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" - -- name: "Create host directories" - file: path={{ item }} state=directory mode=0755 - loop: "{{ vars[inventory_hostname]['services'] | directorieslist }}" - -- name: "Copy systemd-tmpfiles" - when: item.name.startswith('/usr/local/lib/risotto-tmpfiles.d') - ansible.builtin.copy: - src: "{{ host_install_dir }}/{{ item.name }}" - dest: "{{ item.name }}" - owner: "{{ item.owner }}" - group: "{{ item.group }}" - mode: "{{ item.mode }}" - loop: "{{ vars[inventory_hostname]['services'] | fileslist(is_host=True) }}" - loop_control: - label: "{{ item.name}}" - -- name: "Execute systemd-tmpfiles" - when: item.name.startswith('/usr/local/lib/risotto-tmpfiles.d') - command: /usr/bin/systemd-tmpfiles --create --clean --remove {{ item.name }} - loop: "{{ vars[inventory_hostname]['services'] | fileslist(is_host=True) }}" - loop_control: - label: "{{ item.name}}" - -- name: "Copy host files" - when: not item.name.startswith('/usr/local/lib/tmpfiles.d') - ansible.builtin.copy: - src: "{{ host_install_dir }}/{{ item.name }}" - dest: "{{ item.name }}" - owner: "{{ item.owner }}" - group: "{{ item.group }}" - mode: "{{ item.mode }}" - loop: "{{ vars[inventory_hostname]['services'] | fileslist(is_host=True) }}" - loop_control: - label: "{{ item.name}}" - -- name: "Reload systemd services configuration" - ansible.builtin.systemd: - daemon_reload: yes - -- name: "Enable services" - when: item.value['manage'] and item.value['activate'] and '@.service' not in item.value['doc'] - ansible.builtin.service: - name: "{{ item.value['doc'] }}" - enabled: yes - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" - -- name: "Disable services" - when: item.value['manage'] and not item.value['activate'] and not item.value['undisable'] and '@.service' not in item.value['doc'] - ansible.builtin.service: - name: "{{ item.value['doc'] }}" - enabled: yes - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" - -- name: "Start services" - when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] != 'none' - ansible.builtin.service: - name: "{{ item.value['doc'] }}" - state: started - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" - -- name: "Restart services" - when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] == 'none' - ansible.builtin.service: - name: "{{ item.value['doc'] }}" - state: restarted - loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" - loop_control: - label: "{{ item.value['doc'] }}" +- name: "Host is modified" + include_tasks: host_modified.yml + when: build_host.host_changed - name: "Copy machines scripts" ansible.builtin.copy: @@ -125,43 +21,39 @@ mode: "0755" loop: "{{ lookup('fileglob', 'sbin/*', wantlist=True) | list }}" -# Images informations -- name: "Remove images tar" - local_action: - module: file - path: /tmp/risotto/images.tar - state: absent - -- name: "Build images files" - local_action: - module: build_images - modules: "{{ vars['modules'] }}" - -- name: "Compress images files" - local_action: - module: archive - path: "/tmp/risotto/images/" - dest: /tmp/risotto/images.tar - format: tar - - name: "Remove dest images files" file: path: /var/lib/risotto/images_files - state: absent - -- name: "Create images files" - file: - path: /var/lib/risotto/images_files - state: directory + state: "{{ item }}" mode: "0700" + with_items: + - absent + - directory - name: "Copy images files" unarchive: - src: "/tmp/risotto/images.tar" + remote_src: true + src: "/tmp/new_configurations/images_files.tar" dest: "/var/lib/risotto/images_files" - name: "Create versions directory" file: - path: /var/lib/risotto/machines_versions + path: /var/lib/risotto/machines_informations state: directory mode: "0700" + +- name: "Empty tests files" + file: + path: /var/lib/risotto/tests + state: "{{ item }}" + mode: "0700" + with_items: + - absent + - directory + +- name: "Copy tests files" + unarchive: + remote_src: true + src: "/tmp/new_configurations/tests.tar" + dest: "/var/lib/risotto/tests" + when: copy_tests diff --git a/ansible/host_modified.yml b/ansible/host_modified.yml new file mode 100644 index 0000000..9bf5353 --- /dev/null +++ b/ansible/host_modified.yml @@ -0,0 +1,74 @@ +- name: "Stop services" + ansible.builtin.service: + name: "{{ item.value['doc'] }}" + state: stopped + when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] != 'none' and item.value['doc'] in services + loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" + loop_control: + label: "{{ item.value['doc'] }}" + +- name: "Remove old config files" + file: + path: /usr/local/lib/ + state: "{{ item }}" + mode: "0700" + with_items: + - absent + - directory + +- name: "Copy config files" + unarchive: + remote_src: true + src: "/tmp/new_configurations/host.tar" + dest: /usr/local/lib/ + owner: root + group: root + +- name: "Execute systemd-tmpfiles" + command: /usr/bin/systemd-tmpfiles --create --clean --remove -E --exclude-prefix=/tmp + +- name: "Remove tmpfiles files directory" + local_action: + module: file + path: /usr/local/lib/tmpfiles.d/ + state: absent + +- name: "Reload systemd services configuration" + ansible.builtin.systemd: + daemon_reload: yes + +- name: "Enable services" + when: item.value['manage'] and item.value['activate'] and '@.service' not in item.value['doc'] + ansible.builtin.service: + name: "{{ item.value['doc'] }}" + enabled: yes + loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" + loop_control: + label: "{{ item.value['doc'] }}" + +- name: "Disable services" + when: item.value['manage'] and not item.value['activate'] and not item.value['undisable'] and '@.service' not in item.value['doc'] + ansible.builtin.service: + name: "{{ item.value['doc'] }}" + enabled: no + loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" + loop_control: + label: "{{ item.value['doc'] }}" + +- name: "Start services" + when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] != 'none' + ansible.builtin.service: + name: "{{ item.value['doc'] }}" + state: started + loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" + loop_control: + label: "{{ item.value['doc'] }}" + +- name: "Restart services" + when: item.value['manage'] and item.value['activate'] and item.value['doc'].endswith('.service') and not item.value['doc'].endswith('@.service') and item.value['engine'] == 'none' + ansible.builtin.service: + name: "{{ item.value['doc'] }}" + state: restarted + loop: "{{ vars[inventory_hostname]['services'] | dict2items }}" + loop_control: + label: "{{ item.value['doc'] }}" diff --git a/ansible/installations b/ansible/installations deleted file mode 120000 index 00e5d0b..0000000 --- a/ansible/installations +++ /dev/null @@ -1 +0,0 @@ -/home/gnunux/git/risotto/risotto/installations/ \ No newline at end of file diff --git a/ansible/inventory.json b/ansible/inventory.json deleted file mode 100644 index 7c658c9..0000000 --- a/ansible/inventory.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "_meta": { - "hostvars": {} - }, - "all": { - "children": [ - "ungrouped" - ] - }, - "ungrouped": { - "hosts": [ - "cloud.silique.fr" - ] - } -} diff --git a/ansible/inventory.py b/ansible/inventory.py index 97b299d..4a0ab75 100755 --- a/ansible/inventory.py +++ b/ansible/inventory.py @@ -103,7 +103,8 @@ class RisottoInventory(object): ret['delete_old_image'] = False ret['configure_host'] = True ret['only_machine'] = None - ret['copy_template'] = False + ret['copy_templates'] = False + ret['copy_tests'] = False ret['host_install_dir'] = ret[host_name].pop('host_install_dir') return dumps(ret, cls=RougailEncoder) @@ -117,7 +118,7 @@ async def main(): except Exception as err: if DEBUG: print_exc() - exit(err) + print(err) run(main()) diff --git a/ansible/library/compare.py b/ansible/library/compare.py index d41dd98..952af78 100644 --- a/ansible/library/compare.py +++ b/ansible/library/compare.py @@ -1,10 +1,8 @@ #!/usr/bin/python3 from time import sleep -from os import fdopen, walk, readlink -from os.path import join, islink -from dbus import SystemBus, Array -from dbus.exceptions import DBusException +from os import fdopen, walk, readlink, chdir, getcwd +from os.path import join, islink, isdir from ansible.module_utils.basic import AnsibleModule @@ -12,8 +10,8 @@ from ansible.module_utils.basic import AnsibleModule def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( - root=dict(type='str', required=True), - paths=dict(type='list', required=True), + # shasums=dict(type='dict', required=True), + directories=dict(type='list', required=True), ) # seed the result dict in the object @@ -22,10 +20,7 @@ def run_module(): # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( - changed=False, - compare={}, - symlink={}, - old_files=[], + directories={}, ) # the AnsibleModule object will be our abstraction working with Ansible @@ -34,28 +29,48 @@ def run_module(): # supports check mode module = AnsibleModule( argument_spec=module_args, - supports_check_mode=True + supports_check_mode=True, ) - root = module.params['root'] - if root != '/': - paths = {join(root, path['name'][1:]): path['name'] for path in module.params['paths']} - search_paths = [join(directory, f) for directory, subdirectories, files in walk(root) for f in files] - else: - paths = {path['name']: path['name'] for path in module.params['paths']} - search_paths = paths - for path in search_paths: - if path in paths: - if not islink(path): - result['compare'][paths[path]] = {'type': 'file', - 'shasum': module.digest_from_file(path, 'sha256'), - } + current_path = getcwd() + for directory in module.params['directories']: + result['directories'][directory] = {} + if not isdir(directory): + continue + chdir(directory) + search_paths = [join(directory_[2:], f) for directory_, subdirectories, files in walk('.') for f in files] + for path in search_paths: + full_path = join(directory, path) + if not islink(full_path): + result['directories'][directory][path] = module.digest_from_file(full_path, 'sha256') else: - result['compare'][paths[path]] = {'type': 'symlink', - 'name': readlink(path), - } - else: - result['old_files'].append(path) + result['directories'][directory][path] = readlink(full_path) + chdir(current_path) +# current_path = getcwd() +# for server_name, dico in module.params['shasums'].items(): +# root = dico['config_dir'] +# if not isdir(root): +# result['machines_changed'].append(server_name) +# continue +# 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 in dico['shasums']: +# full_path = join(root, path) +# if not islink(full_path): +# if module.digest_from_file(full_path, 'sha256') != dico['shasums'][path]: +# result['machines_changed'].append(server_name) +# break +# elif dico['shasums'][path] != readlink(full_path): +# result['machines_changed'].append(server_name) +# break +# del dico['shasums'][path] +# else: +# result['machines_changed'].append(server_name) +# break +# if server_name not in result['machines_changed'] and dico['shasums']: +# result['machines_changed'].append(server_name) module.exit_json(**result) diff --git a/ansible/machine.yml b/ansible/machine.yml index a90a023..cf69c78 100644 --- a/ansible/machine.yml +++ b/ansible/machine.yml @@ -1,121 +1,15 @@ - name: "Create SRV directory for {{ item.name}}" + file: + path: /var/lib/risotto/srv/{{ item.name }} + state: directory + mode: 0755 when: "item.srv" - file: path=/var/lib/risotto/srv/{{ item.name }} state=directory mode=0755 -- name: "Create SystemD directory for {{ item.name }}" - file: path=/var/lib/risotto/journals/{{ item.name }} state=directory mode=0755 - -- name: "Build machine files for {{ item.name }}" - rougail: - paths: "{{ vars[item.name]['services'] | fileslist }}" - root_local: "{{ host_install_dir }}" - root_remote: "/var/lib/risotto/configurations/{{ item.name }}" - hostname: "{{ item.name}}" - is_host: False - register: up_to_date_configuration - -- name: "Change secrets right" - local_action: - module: file - path: "{{ host_install_dir }}/secrets" +- name: "Create journald directory for {{ item.name }}" + file: + path: /var/lib/risotto/journals/{{ item.name }} state: directory - mode: 0700 + mode: 0755 -- name: "Compress files for {{ item.name }}" - local_action: - module: archive - path: "{{ host_install_dir }}/" - dest: /tmp/new_configurations/{{ item.name }} - format: tar - when: up_to_date_configuration.changed - -- name: "Build machine templates for {{ item.name }}" - rougail: - paths: "{{ vars[item.name]['services'] | fileslist }}" - root_local: "{{ host_install_dir }}" - root_remote: "/var/lib/risotto/configurations/{{ item.name }}" - hostname: "{{ item.name}}" - just_copy: true - is_host: False - when: copy_template - register: up_to_date_configuration - -- name: "Compress templates for {{ item.name }}" - local_action: - module: archive - path: "../templates/" - dest: /tmp/new_templates/{{ item.name }} - format: tar - when: copy_template - -- name: "Remove templates directory for {{ item.name }}" - file: - path: "/var/lib/risotto/templates/{{ item.name }}" - state: absent - when: copy_template - -- name: "Create templates directory for {{ item.name }}" - file: - path: "/var/lib/risotto/templates/{{ item.name }}" - state: directory - when: copy_template - -- name: "Copy templates for {{ item.name }}" - unarchive: - src: "/tmp/new_templates/{{ item.name }}" - dest: "/var/lib/risotto/templates/{{ item.name }}/" - when: copy_template - -- name: "Remove old image {{ vars | modulename(item.name) }}" - file: - path: "/var/lib/risotto/images/{{ vars | modulename(item.name) }}" - state: absent - when: delete_old_image == true - -- name: "Stop machine {{ item.name }}" - machinectl: - state: stopped - machines: "{{ item.name }}" - when: delete_old_image == true - -- name: "Remove old machine {{ item.name }}" - file: - path: /var/lib/machines/{{ item.name }} - state: absent - when: delete_old_image == true - -- name: "Create system directory for {{ item.name }}" - file: - path: /var/lib/machines/{{ item.name }} - state: directory - register: system_directory_created - -- name: "Check image for {{ item.name }}" - stat: - path: "/var/lib/risotto/images/{{ vars | modulename(item.name) }}" - follow: true - register: register_name - when: system_directory_created.changed - -#- name: Print return information from the previous task -# ansible.builtin.debug: -# var: register_name - -- name: "Build image for {{ item.name }}" - ansible.builtin.shell: "/usr/local/sbin/build_image {{ vars | modulename(item.name) }}" - when: "'stat' in register_name and not register_name.stat.exists" - register: ret - failed_when: ret.rc != 0 - -- name: "Copy machine image for {{ item.name }}" - ansible.builtin.shell: "/usr/bin/cp -a --reflink=auto /var/lib/risotto/images/{{ vars | modulename(item.name) }}/* /var/lib/machines/{{ item.name }}" - when: system_directory_created.changed - -- name: "Copy machine image version for {{ item.name }}" - ansible.builtin.copy: - src: "/var/lib/risotto/images/{{ vars | modulename(item.name) }}.version" - remote_src: true - dest: "/var/lib/risotto/machines_versions/{{ item.name }}.version" - owner: "root" - group: "root" - when: system_directory_created.changed +- name: "Create informations for {{ item.name }}" + ansible.builtin.shell: "/usr/bin/echo {{ vars | modulename(item.name) }} > /var/lib/risotto/machines_informations/{{ item.name }}.image" diff --git a/ansible/machines.yml b/ansible/machines.yml index a0d0c91..e6d3db9 100644 --- a/ansible/machines.yml +++ b/ansible/machines.yml @@ -1,27 +1,30 @@ -- name: "Stop machines with new configuration" +#- name: Print return information from the previous task +# ansible.builtin.debug: +# var: build_host.machines_changed + +- name: "Rebuild images" + ansible.builtin.shell: "/usr/local/sbin/update_images just_need_images" + register: ret + failed_when: ret.rc != 0 + +- name: "Stop machines with new configuration {{ build_host.machines_changed }}" machinectl: state: stopped - machines: "{{ lookup('fileglob', '/tmp/new_configurations/*', wantlist=True) | map('basename') | list }}" + machines: "{{ build_host.machines_changed }}" - name: "Remove files directory" file: path: "/var/lib/risotto/configurations/{{ item }}" state: absent - loop: "{{ lookup('fileglob', '/tmp/new_configurations/*', wantlist=True) | map('basename') | list }}" - -- name: "Create files directory" - file: - path: "/var/lib/risotto/configurations/{{ item }}" - state: directory - loop: "{{ lookup('fileglob', '/tmp/new_configurations/*', wantlist=True) | map('basename') | list }}" + loop: "{{ build_host.machines_changed }}" - name: "Copy configuration" unarchive: - src: "{{ item }}" - dest: /var/lib/risotto/configurations/{{ item | basename }}/ + src: /tmp/new_configurations/machines.tar + dest: /var/lib/risotto/configurations/ owner: root group: root - loop: "{{ lookup('fileglob', '/tmp/new_configurations/*', wantlist=True) }}" + when: build_host.machines_changed - name: "Enable machines" machinectl: @@ -38,9 +41,3 @@ module: file path: /tmp/new_configurations state: absent - -- name: "Remove compressed templates directory" - local_action: - module: file - path: /tmp/new_templates - state: absent diff --git a/ansible/playbook.txt b/ansible/playbook.txt deleted file mode 100644 index 5626aa0..0000000 --- a/ansible/playbook.txt +++ /dev/null @@ -1,23 +0,0 @@ - - name: installation dépendances - apt: - pkg: - - systemd-container - - dnf - - jq - - debootstrap - - htop - - gettext - - patch - - unzip - - mlocate - - xz-utils - - iptables - update_cache: yes - state: latest - -MARCHE - - name: installation dépendances - apt: - pkg: "{{ packages }}" - update_cache: yes - state: latest diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 2902c72..2c248be 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -2,41 +2,28 @@ - name: Risotto hosts: all tasks: + - name: "Build host files" + rougail: + hostname: "{{ vars['inventory_hostname'] }}" + only_machine: "{{ only_machine }}" + configure_host: "{{ configure_host }}" + copy_tests: "{{ copy_tests }}" + copy_templates: "{{ copy_templates }}" + register: build_host + - name: "Configure the host" include_tasks: host.yml when: configure_host == true - - - name: "Remove compressed files directory" - local_action: - module: file - path: /tmp/new_configurations - state: absent - - - name: "Create compressed configuration files directory" - local_action: - module: file - path: /tmp/new_configurations - state: directory - mode: 0700 - - - name: "Remove compressed templates files directory" - local_action: - module: file - path: /tmp/new_templates - state: absent - when: copy_template - - - name: "Create compressed templates files directory" - local_action: - module: file - path: /tmp/new_templates - state: directory - mode: 0700 - when: copy_template - + - name: "Prepare machine configuration" include_tasks: machine.yml + when: item.name in build_host.machines_changed loop: "{{ vars | machineslist(only=only_machine) }}" - + # + # - name: "Remove images" + # include_tasks: remove_image.yml + # loop: "{{ vars | machineslist(only=only_machine) }}" + # when: delete_old_image == true + # - name: "Install and apply configurations" include_tasks: machines.yml diff --git a/ansible/remove_image.yml b/ansible/remove_image.yml new file mode 100644 index 0000000..248cc94 --- /dev/null +++ b/ansible/remove_image.yml @@ -0,0 +1,14 @@ +- name: "Stop machine {{ item.name }}" + machinectl: + state: stopped + machines: "{{ item.name }}" + +- name: "Remove old machine {{ item.name }}" + file: + path: /var/lib/machines/{{ item.name }} + state: absent + +- name: "Remove old image {{ vars | modulename(item.name) }}" + file: + path: "/var/lib/risotto/images/{{ vars | modulename(item.name) }}" + state: absent diff --git a/ansible/sbin/build_image b/ansible/sbin/build_image index f46e461..57610a1 100755 --- a/ansible/sbin/build_image +++ b/ansible/sbin/build_image @@ -2,6 +2,12 @@ IMAGE_NAME=$1 +if [ -z "$1" ]; then + ONLY_IF_DATASET_MODIF=false +else + ONLY_IF_DATASET_MODIF=true +fi + if [ -z "$IMAGE_NAME" ]; then echo "PAS DE NOM DE MODULE" exit 1 @@ -14,11 +20,11 @@ RISOTTO_IMAGE_DIR="$RISOTTO_DIR/images" IMAGE_BASE_RISOTTO_BASE_DIR="$RISOTTO_IMAGE_DIR/image_bases" IMAGE_NAME_RISOTTO_IMAGE_DIR_TMP="$RISOTTO_IMAGE_DIR/tmp/$IMAGE_NAME" IMAGE_NAME_RISOTTO_IMAGE_DIR="$RISOTTO_IMAGE_DIR/$IMAGE_NAME" -IMAGE_DIR_RECIPIENT_IMAGE="/var/lib/risotto/images_files/$IMAGE_NAME" +IMAGE_DIR_RECIPIENT_IMAGE="$RISOTTO_DIR/images_files/$IMAGE_NAME" rm -f /var/log/risotto/build_image.log -mkdir -p "$RISOTTO_IMAGE_DIR" "$RISOTTO_IMAGE_DIR/tmp/" /var/log/risotto +mkdir -p "$RISOTTO_IMAGE_DIR" "$RISOTTO_IMAGE_DIR/tmp/" PKG="" BASE_DIR="" for script in $(ls "$IMAGE_DIR_RECIPIENT_IMAGE"/preinstall/*.sh 2> /dev/null); do @@ -93,7 +99,7 @@ function install_pkg() { if [ ! -f "$BASE_LOCK" ] || [ ! -d "$BASE_DIR" ]; then echo " - reinstallation de l'image de base" new_package_base - diff -u "$BASE_PKGS_FILE" "$BASE_PKGS_FILE".new && NEW_BASE=false || NEW_BASE=true + diff -u "$BASE_PKGS_FILE" "$BASE_PKGS_FILE".new &> /dev/null && NEW_BASE=false || NEW_BASE=true if [ ! -d "$BASE_DIR" ] || [ "$NEW_BASE" = true ]; then mkdir -p "$IMAGE_BASE_RISOTTO_BASE_DIR" rm -rf "$IMAGE_NAME_RISOTTO_IMAGE_DIR_TMP" @@ -121,7 +127,6 @@ if [ "$FUSION" = true ]; then dnf -y install "https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$RELEASEVER.noarch.rpm" --installroot="$IMAGE_NAME_RISOTTO_IMAGE_DIR_TMP" >> /var/log/risotto/build_image.log fi -# FIXME verifier s'il y a des modifs sur pre/post if [ -f "$IMAGE_NAME_RISOTTO_IMAGE_DIR".base.pkgs ] && [ -f "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs ]; then echo " - différence(s) avec les paquets de base" diff -u "$IMAGE_NAME_RISOTTO_IMAGE_DIR".base.pkgs "$BASE_PKGS_FILE" && INSTALL=false || INSTALL=true @@ -130,13 +135,18 @@ else INSTALL=true fi -new_package +if [ "$ONLY_IF_DATASET_MODIF" = false ] || [ ! -f "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs ]; then + new_package +else + cp --reflink=auto "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs.new +fi if [ "$INSTALL" = false ]; then echo " - différence(s) avec les paquets de l'image" diff -u "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs "$IMAGE_NAME_RISOTTO_IMAGE_DIR".pkgs.new && INSTALL=false || INSTALL=true fi find "$IMAGE_DIR_RECIPIENT_IMAGE" -type f -exec md5sum '{}' \; > "$IMAGE_NAME_RISOTTO_IMAGE_DIR".md5sum.new if [ "$INSTALL" = false ]; then + echo " - différence(s) du dataset" diff -u "$IMAGE_NAME_RISOTTO_IMAGE_DIR".md5sum "$IMAGE_NAME_RISOTTO_IMAGE_DIR".md5sum.new && INSTALL=false || INSTALL=true fi if [ "$INSTALL" = true ]; then @@ -146,7 +156,11 @@ if [ "$INSTALL" = true ]; then else VERSION=0 fi - make_changelog "$IMAGE_NAME" "$VERSION" "$OS_NAME" "$RELEASEVER" > "$IMAGE_NAME_RISOTTO_IMAGE_DIR"_"$RELEASEVER"_"$VERSION"_changelog.md + if [ -d "$IMAGE_NAME_RISOTTO_IMAGE_DIR" ]; then + cd "$IMAGE_NAME_RISOTTO_IMAGE_DIR" + make_changelog "$IMAGE_NAME" "$VERSION" "$OS_NAME" "$RELEASEVER" > "$IMAGE_NAME_RISOTTO_IMAGE_DIR"_"$RELEASEVER"_"$VERSION"_changelog.md + cd - > /dev/null + fi install_pkg sleep 2 diff --git a/ansible/sbin/compare_image b/ansible/sbin/compare_image index e6079f5..1c15e82 100755 --- a/ansible/sbin/compare_image +++ b/ansible/sbin/compare_image @@ -13,10 +13,24 @@ if [ ! -d "$dirname" ]; then exit 1 fi cd $dirname -find -type f | while read a; do - cfile="/var/lib/machines/$SRV/usr/share/factory/$a" +find -type f -not -path "./secrets/*" -not -path "./tmpfiles.d/*" -not -path "./sysusers.d/*" -not -path "./systemd/*" -not -path "./tests/*" -not -path "./etc/pki/*" | while read a; do + machine_path="/var/lib/machines/$SRV" + cfile="$machine_path/usr/share/factory/$a" if [ -f "$cfile" ]; then - diff -u "$cfile" "$a" + diff -u "$dirname/$a" "$cfile" + else + FIRST_LINE="$(head -n 1 $a)" + if [[ "$FIRST_LINE" == "#RISOTTO: file://"* ]]; then + other=${FIRST_LINE:16} + diff -u "$dirname/$a" "$machine_path$other" + elif [[ "$FIRST_LINE" == "#RISOTTO: https://"* ]]; then + other=${FIRST_LINE:10} + echo $other + wget -q $other -O /tmp/template.tmp + diff -u "$dirname/$a" /tmp/template.tmp + elif [ ! "$FIRST_LINE" = "#RISOTTO: do not compare" ]; then + echo "cannot find \"$cfile\" ($dirname/$a)" + fi fi done cd - > /dev/null diff --git a/ansible/sbin/make_changelog b/ansible/sbin/make_changelog index c2da6ea..4346652 100755 --- a/ansible/sbin/make_changelog +++ b/ansible/sbin/make_changelog @@ -90,7 +90,7 @@ def print_changelogs_markdown(packages): print(format_changelog_markdown(chl)) -def dnf_update(image_name): +def dnf_update(image_name, releasever): conf = Conf() # obsoletes are already listed conf.obsoletes = False @@ -102,7 +102,7 @@ def dnf_update(image_name): base.output = custom_output cli = Cli(base) image_dir = join(getcwd(), image_name) - cli.configure(['--setopt=install_weak_deps=False', '--nodocs', '--noplugins', '--installroot=' + image_dir, '--releasever', '35', 'check-update', '--changelog'], OptionParser()) + cli.configure(['--setopt=install_weak_deps=False', '--nodocs', '--noplugins', '--installroot=' + image_dir, '--releasever', releasever, 'check-update', '--changelog'], OptionParser()) logger = logging.getLogger("dnf") for h in logger.handlers: logger.removeHandler(h) @@ -146,7 +146,7 @@ type = "installe" list_packages('Les paquets ajoutés', new_pkg - ori_pkg, new_dict) print('# Les paquets mises à jour\n') if os_name == 'fedora': - dnf_update(image_name) + dnf_update(image_name, releasever) else: for filename in glob('*.deb'): unlink(filename) diff --git a/ansible/sbin/make_volatile b/ansible/sbin/make_volatile index 70620e9..31b7a4c 100755 --- a/ansible/sbin/make_volatile +++ b/ansible/sbin/make_volatile @@ -1,5 +1,5 @@ #!/bin/bash -e -if [ -z $ROOT]; then +if [ -z $ROOT ]; then echo "PAS DE ROOT" exit 1 fi diff --git a/ansible/sbin/test_images b/ansible/sbin/test_images new file mode 100755 index 0000000..66ab7ef --- /dev/null +++ b/ansible/sbin/test_images @@ -0,0 +1,30 @@ +#!/bin/bash + +QUIT_ON_ERROR=true +# QUIT_ON_ERROR=false +CONFIG_DIR="/var/lib/risotto/configurations" +INFO_DIR="/var/lib/risotto/machines_informations" +TEST_DIR="/var/lib/risotto/tests" +TEST_DIR_NAME="tests" + +if [ ! -d /var/lib/risotto/tests/ ]; then + echo "no tests directory" + exit 1 +fi + +py_test_option="-s" +if [ "$QUIT_ON_ERROR" = true ]; then + set -e + py_test_option="$py_test_option -x" +fi + +for nspawn in $(ls /etc/systemd/nspawn/*.nspawn); do + nspawn_file=$(basename $nspawn) + machine=${nspawn_file%.*} + image=$(cat $INFO_DIR/$machine.image) + imagedir=$TEST_DIR/$image + machine_test_dir=$CONFIG_DIR/$machine/$TEST_DIR_NAME + export MACHINE_TEST_DIR=$machine_test_dir + echo "- $machine" + py.test-3 $py_test_option "$imagedir" +done diff --git a/ansible/sbin/update_images b/ansible/sbin/update_images index c931a7b..92c545e 100755 --- a/ansible/sbin/update_images +++ b/ansible/sbin/update_images @@ -6,18 +6,19 @@ RISOTTO_IMAGE_DIR="$RISOTTO_DIR/images" # image configuration IMAGE_BASE_RISOTTO_BASE_DIR="$RISOTTO_IMAGE_DIR/image_bases" -rm -f $IMAGE_BASE_RISOTTO_BASE_DIR*.build - if [ -z "$1" ]; then - ls /var/lib/risotto/images_files/ | while read image; do - if [ -d /var/lib/risotto/images_files/"$image" ]; then - echo - echo "Install image $image" - /usr/local/sbin/build_image "$image" || true - fi - done + rm -f $IMAGE_BASE_RISOTTO_BASE_DIR*.build fi -#rm -f $IMAGE_BASE_RISOTTO_BASE_DIR*.build + +mkdir -p /var/log/risotto + +ls /var/lib/risotto/images_files/ | while read image; do + if [ -d /var/lib/risotto/images_files/"$image" ]; then + echo + echo "Install image $image" | tee -a /var/log/risotto/update_images.log + /usr/local/sbin/build_image "$image" "$1" | tee -a /var/log/risotto/update_images.log || (echo "PROBLEME" | tee -a /var/log/risotto/update_images.log; true) + fi +done MACHINES="" for nspawn in $(ls /etc/systemd/nspawn/*.nspawn); do @@ -25,10 +26,10 @@ for nspawn in $(ls /etc/systemd/nspawn/*.nspawn); do machine=${nspawn_file%.*} MACHINES="$MACHINES$machine " MACHINE_MACHINES_DIR="/var/lib/machines/$machine" - SHA_MACHINE="$RISOTTO_DIR/configurations/sha/$machine".sha - content=$(cat $SHA_MACHINE) - IMAGE_NAME_RISOTTO_IMAGE_NAME=${content##* } - diff -q "$IMAGE_NAME_RISOTTO_IMAGE_NAME".sha "$SHA_MACHINE" > /dev/null || ( + IMAGE_NAME_RISOTTO_IMAGE_NAME="$(cat $RISOTTO_DIR/machines_informations/$machine.image)" + MACHINE_INFO="$RISOTTO_DIR/machines_informations/" + VERSION_MACHINE="$MACHINE_INFO/$machine.version" + diff -q "$RISOTTO_IMAGE_DIR/$IMAGE_NAME_RISOTTO_IMAGE_NAME".version "$VERSION_MACHINE" &> /dev/null || ( echo "Reinstall machine $machine" machinectl stop $machine || true while true; do @@ -37,10 +38,12 @@ for nspawn in $(ls /etc/systemd/nspawn/*.nspawn); do done rm -rf "$MACHINE_MACHINES_DIR" mkdir "$MACHINE_MACHINES_DIR" - cd "$MACHINE_MACHINES_DIR" - tar xf "$IMAGE_NAME_RISOTTO_IMAGE_NAME" - cp -a "$IMAGE_NAME_RISOTTO_IMAGE_NAME".sha "$SHA_MACHINE" + cp -a --reflink=auto $RISOTTO_IMAGE_DIR/$IMAGE_NAME_RISOTTO_IMAGE_NAME/* $MACHINE_MACHINES_DIR + cp -a --reflink=auto "$RISOTTO_IMAGE_DIR/$IMAGE_NAME_RISOTTO_IMAGE_NAME".version "$VERSION_MACHINE" ) done -machinectl start $MACHINES -diagnose +if [ -z "$1" ]; then + machinectl start $MACHINES + diagnose +fi +exit 0 diff --git a/doc/authentification.svg b/doc/authentification.svg new file mode 100644 index 0000000..bdc4064 --- /dev/null +++ b/doc/authentification.svg @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IMAP + LDAP + + + + + ***** + + domaine + + + diff --git a/doc/example_smtp.png b/doc/example_smtp.png new file mode 100644 index 0000000000000000000000000000000000000000..fc98a998cec24aef9b1cb1f71227bd56eec0ae85 GIT binary patch literal 102822 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%0Db9NZX0>}<~c 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�z!7TT_Ov%`iBMW_$*dAp9sB$IC&Vy4szdg>l1({}xysYSHx;ul>q1?D_} zG+MdrMwcmo_++=ifsu3^bOV&`uhDoh?$^6vC#N==qKmf=ow^80g6n7EmZ!W=F4kg_ zx_CW>FDYs6^76*~4Wr?^J=ESD`}_N(05JZ@1D0uU=PE%ZYOIz)m3iBDSuj1oXQe}q zl9Gn4`h$$jM}3qjH2RQLcg~{uE?B*p=UcoaBE`OlfeSPFTJO(ed_k(>G95GJV03Hm5i8N{Pp2d0p6OmP`))Uya#E4 zHazzUd1RjAc$`dRV%-_CXbcu#%RkI}QlnIaM7vM_{?l2ElAJ*2JPJ^_{7y4IN9Y%0 zt^C0Pl7u;-4hVdyXv4dQr6wo%;gtmNnW;No@ZIEBy1YFexXS97>lZH?U2QtuRvX6O z-}dN&bGV0>kB|1Ke>HK5V0%Mv^^uMSA1{AZ>7hSlZ1wX76t9nc#(c@?*5kw9JpJZx zIaCAIx6E28j5Z~6hQ;V^-6CF0==Z$l2j!e>y$^JC?gKoN&)*2Wz0Nzf)}64%)ug zG1u8dKqG7y9yzSuNa=$H=zPaYo*G({g-3sIlvK*3X)q@nALM>ryQW(_$;i-e&`2o{ zpWnmgRgPMexm zyNYO@qV+?Y006|k2dcXH%wb428Vjp86tw;! zAt5{~^UMKv%;1Te-u(f$EvJavp3w0aY@V2TzWxkbaS=;}e5?|L3FZ$W!LE6}qxm}8 zm!7qhLh-!Az?z_2FP_bEak<#~qhPS{%foG=>cMuLpau<6k1&zyoO=_=^9@!%ZjRt~ zooehG`(9Wv<*^EInK1Szryr6KcfjS{kqytH(QC!P`DdOwBI-P4Vy%=t9q;DkZ16N$`DGiRtNlvPRD$A2E{Ak^(m8BV+H>E{JeCvWBZpq5i zSNIbl+qNlS<2h4U6Y7@RzH{Q+{@8x$LUA8fHXf1JP)V3h==h_mYVN{?hv~Minw(W~ z>((tN)qWrPdY1J2Fucibc{^8K`REUEkLAPKtXG14UkR5KJl4%6;YUHnM)(jG7zKjz zS6SWp-5(M5B)RiKT3`B%nYP5{QSplR4jemndco?H9+{oBeXC2aU4rRGDMrJrn@dG&NMb- zC3m}b)tC>D60=v28a?`OR|&?_8Znu}A~ zx9A?@4lpa-lPIkp)=IQB8ai13TNP0HfXhIRmFGX*j+Yd=Aa**y={SxinX4O(3LcS- zAWOE}4u4f%KB`j@FKhPcqX;1#HoC6>A5F_ul!syWsn3~U2?w#i4>NRhk7O!VzbTgz z??gT7?Doyr^Vp8lsWmaz!^6%>hXzc{~J@ zC#`3V4r6@wVKN7H9p}fgz0E4NYwXm}HeZ;M&pLG4Rg)weN#R1LuKnhflL*ifgD4N# zGd5xQ!PZD33*mm=WgXr4!mH^kIh{*KpFjM>pt$dqqXH@x&y{EhT8;aPbii%Nta^y+gG%^YbOHZF(JiO?_ykW4|rvvs~ zqt^(m)^&K{{%ljZ`c#h)11@Y86lTHdRj}I^*S+1>XA`L=pQRVhJXVhBJg(@?xkK@n zd6QJMII$yMh?+elaI9?rl$}JmdyZ8;`sF;c?YOH%ArX3usq|a`j`9(W18bniDrgni z|3Y_Uo?yAt%d0O~buJqx&fAx)FScKQ-a|g_V2zbpCfz6ig)z}|BC*;brw)c);NvJA zYW*4$$UGzjcnq%S7#p`>W#3|E1GHG}Rs5YjSSV1JTS~E|P-NxfVV7=~Bmy>Fc2-XN zdXDZQpXR~;@TO56gdgwhT&t$c(U`I7$%T8z_ldqYn7$-ZOHJl%8|)YCs$kb@w}%}W zEm<%7sngvtE|gj82z{!D284XL~$;F9VP-~{Hz^}xPdJa7Oc&CGc!F;4J7pQ0HV` z-LFiDT@F)=LG#^lw?Du}QNg}A;Jajjfpcr?%x$^rGQc7XZ3{;kqk#ZQ3qw04eEjp* zEikYTIP!kx6nGebA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IMAP (993)SMTP (587) + SMTPrelay(25) + LDAP + DNSRésolver + DNSautoritaire + + + + + + + + + + + diff --git a/sbin/risotto_auto_doc b/sbin/risotto_auto_doc index cf57604..d2eb407 100755 --- a/sbin/risotto_auto_doc +++ b/sbin/risotto_auto_doc @@ -77,6 +77,8 @@ def parse(applicationservice, elts, dico, providers_suppliers, hidden): values['type'] = child.type if hasattr(child, 'default'): default = child.default + if isinstance(default, objectspace.value): + default = '' if isinstance(default, list): default = '
'.join(default) values['values'] = default @@ -151,8 +153,8 @@ for applicationservice, applicationservice_data in applicationservices_data.item if not isdir(dirname) and not extra_dictionaries: continue rougailconfig['extra_dictionaries'] = extra_dictionaries - converted = RougailConvert(rougailconfig) - converted.load_dictionaries(just_doc=True) + converted = RougailConvert(rougailconfig, just_doc=True) + converted.load_dictionaries() converted.annotate() objectspace = converted.rougailobjspace if hasattr(objectspace.space, 'variables'): @@ -231,29 +233,46 @@ for applicationservice, applicationservice_data in applicationservices_data.item as_fh.write('\n- [+]: variable is multiple\n- **bold**: variable is mandatory\n') if applicationservice_data['used_by']: as_fh.write('\n## Used by\n\n') - for link in applicationservice_data['used_by']: - as_fh.write(f'- [{link}](../{link}/README.md)\n') + if len(applicationservice_data['used_by']) == 1: + link = applicationservice_data['used_by'][0] + as_fh.write(f'[{link}](../{link}/README.md)\n') + else: + for link in applicationservice_data['used_by']: + as_fh.write(f'- [{link}](../{link}/README.md)\n') linked = [] for provider, provider_as in providers_suppliers['providers'].items(): if not applicationservice in provider_as: continue for supplier in providers_suppliers['suppliers'][provider]: - if not linked: - as_fh.write('\n## Linked to\n\n') if supplier in linked: continue - as_fh.write(f'- [{supplier}](../{supplier}/README.md)\n') linked.append(supplier) + linked.sort() + if linked: + if len(linked) == 1: + as_fh.write('\n## Supplier\n\n') + as_fh.write(f'[{linked[0]}](../{linked[0]}/README.md)\n') + else: + as_fh.write('\n## Suppliers\n\n') + for supplier in linked: + as_fh.write(f'- [{supplier}](../{supplier}/README.md)\n') + linked = [] for supplier, supplier_as in providers_suppliers['suppliers'].items(): if not applicationservice in supplier_as: continue for provider in providers_suppliers['providers'][supplier]: - if not linked: - as_fh.write('\n## Linked to\n\n') if provider in linked: continue - as_fh.write(f'- [{provider}](../{provider}/README.md)\n') linked.append(provider) + linked.sort() + if linked: + if len(linked) == 1: + as_fh.write('\n## Provider\n\n') + as_fh.write(f'[{linked[0]}](../{linked[0]}/README.md)\n') + else: + as_fh.write('\n## Providers\n\n') + for provider in linked: + as_fh.write(f'- [{provider}](../{provider}/README.md)\n') with open('seed/README.md', 'w') as as_fh: @@ -275,3 +294,24 @@ with open('seed/README.md', 'w') as as_fh: for applicationservice in applicationservices_: applicationservice_data = applicationservices_data[applicationservice] as_fh.write(f' - [{applicationservice}]({applicationservice}/README.md): {applicationservice_data["description"]}\n') + as_fh.write('\n# Providers and suppliers\n\n') + providers = list(providers_suppliers['providers'].keys()) + providers.sort() + for provider in providers: + as_fh.write(f'- {provider}:\n') + if providers_suppliers['providers'][provider]: + if len(providers_suppliers['providers'][provider]) == 1: + applicationservice = providers_suppliers['providers'][provider][0] + as_fh.write(f' - Provider: [{applicationservice}]({applicationservice}/README.md)\n') + else: + as_fh.write(f' - Providers:\n') + for applicationservice in providers_suppliers['providers'][provider]: + as_fh.write(f' - [{applicationservice}]({applicationservice}/README.md)\n') + if providers_suppliers['suppliers']: + if len(providers_suppliers['suppliers'][provider]) == 1: + applicationservice = providers_suppliers['suppliers'][provider][0] + as_fh.write(f' - Supplier: [{applicationservice}]({applicationservice}/README.md)\n') + else: + as_fh.write(f' - Suppliers:\n') + for applicationservice in providers_suppliers['suppliers'][provider]: + as_fh.write(f' - [{applicationservice}]({applicationservice}/README.md)\n') diff --git a/sbin/risotto_templates b/sbin/risotto_templates index d91e462..f912872 100755 --- a/sbin/risotto_templates +++ b/sbin/risotto_templates @@ -12,14 +12,19 @@ async def main(): parser.add_argument('server_name') parser.add_argument('--nocache', action='store_true') parser.add_argument('--debug', action='store_true') + parser.add_argument('--copy_tests', action='store_true') + parser.add_argument('--template') args = parser.parse_args() if args.nocache: remove_cache() - config = await load() + config = await load(copy_tests=args.copy_tests, clean_directories=True) + print('fin') + print(await config.option('host_example_net.general.copy_tests').value.get()) try: await templates(args.server_name, config, + template=args.template ) except Exception as err: if args.debug: diff --git a/src/risotto/image.py b/src/risotto/image.py index 63e4c46..fbf3b2c 100644 --- a/src/risotto/image.py +++ b/src/risotto/image.py @@ -2,6 +2,7 @@ from shutil import copy2, copytree from os import listdir, makedirs from os.path import join, isdir, isfile, dirname from yaml import load as yaml_load, SafeLoader +from tiramisu.error import PropertiesOptionError # from .utils import RISOTTO_CONFIG @@ -181,11 +182,9 @@ class Modules: if isdir(extra_dir): cfg.extra_dictionaries.setdefault(extra, []).append(extra_dir) # manual - for type in ['image', 'install']: - manual_dir = join(as_dir, 'manual') - if isdir(join(manual_dir, type)): - cfg.manuals.append(manual_dir) - break + manual_dir = join(as_dir, 'manual', 'image') + if isdir(manual_dir): + cfg.manuals.append(manual_dir) # tests tests_dir = join(as_dir, 'tests') if isdir(tests_dir): @@ -218,6 +217,10 @@ def applicationservice_copy(src_file: str, async def valid_mandatories(config): mandatories = await config.value.mandatory() + await config.property.pop('mandatory') + hidden = {} + variables = {} + title = None if mandatories: server_name = None for mandatory in mandatories: @@ -225,10 +228,20 @@ async def valid_mandatories(config): var_server_name = await config.option(path_server_name).option.description() if server_name != var_server_name: server_name = var_server_name - print() - print(f'=== Missing variables for {server_name} ===') - print(f' - {path}') - # await config.property.pop('mandatory') - # await value_pprint(await config.value.dict(), config) - exit(1) - #raise Exception('configuration has mandatories variables without values') + title = f'=== Missing variables for {server_name} ===' + suboption = config.option(mandatory) + text = await suboption.option.doc() + msg = f' - {text} ({path})' + supplier = await suboption.information.get('supplier', None) + if supplier: + msg += f' you could add a service that provides {supplier}' + try: + await config.option(mandatory).value.get() + variables.setdefault(title, []).append(msg) + except PropertiesOptionError as err: + if 'hidden' not in err.proptype: + raise PropertiesOptionError(err) + hidden.setdefault(title, []).append(msg) + if not variables: + variables = hidden + return variables diff --git a/src/risotto/machine.py b/src/risotto/machine.py index a04a255..c204f5f 100644 --- a/src/risotto/machine.py +++ b/src/risotto/machine.py @@ -3,15 +3,15 @@ from .image import Applications, Modules, valid_mandatories, applicationservice_ from .rougail.annotator import calc_providers, calc_providers_global, calc_providers_dynamic, calc_providers_dynamic_follower, calc_providers_follower from rougail import RougailConfig, RougailConvert -from os import remove, makedirs, listdir -from os.path import isfile, isdir, abspath +from os import remove, makedirs, listdir, chmod +from os.path import isfile, isdir, abspath, join, dirname from json import dump as json_dump, load as json_load from yaml import load as yaml_load, SafeLoader # from tiramisu import Config, valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal, calc_value from rougail.utils import normalize_family from rougail import RougailSystemdTemplate -from shutil import rmtree +from shutil import copy2, copytree, rmtree def tiramisu_display_name(kls, @@ -31,7 +31,10 @@ TIRAMISU_CACHE = 'tiramisu_cache.py' VALUES_CACHE = 'values_cache.json' INFORMATIONS_CACHE = 'informations_cache.json' INSTALL_DIR = RISOTTO_CONFIG['directories']['dest'] -INSTALL_TEMPLATES_DIR = RISOTTO_CONFIG['directories']['dest_templates'] +INSTALL_CONFIG_DIR = 'configurations' +INSTALL_TMPL_DIR= 'templates' +INSTALL_IMAGES_DIR = 'images_files' +INSTALL_TESTS_DIR = 'tests' FUNCTIONS = {'calc_providers': calc_providers, 'calc_providers_global': calc_providers_global, 'calc_providers_dynamic': calc_providers_dynamic, @@ -47,10 +50,32 @@ FUNCTIONS = {'calc_providers': calc_providers, } -def re_create(dirname): - if isdir(dirname): - rmtree(dirname) - makedirs(dirname) +def copy(src_file, dst_file): + if isdir(src_file): + if not isdir(dst_file): + makedirs(dst_file) + for subfilename in listdir(src_file): + if not isfile(dst_file): + src = join(src_file, subfilename) + dst = join(dst_file, subfilename) + if isfile(src): + copy2(src, dst) + else: + copytree(src, dst) + elif not isfile(dst_file): + dst = dirname(dst_file) + if not isdir(dst): + makedirs(dst) + if isfile(src_file): + copy2(src_file, dst_file) + else: + copytree(src_file, dst_file) + + +def re_create(dir_name): + if isdir(dir_name): + rmtree(dir_name) + makedirs(dir_name) def remove_cache(): @@ -65,6 +90,8 @@ def remove_cache(): async def templates(server_name, config, just_copy=False, + copy_manuals=False, + template=None, ): subconfig = config.option(normalize_family(server_name)) try: @@ -77,23 +104,25 @@ async def templates(server_name, rougailconfig['variable_namespace'] = ROUGAIL_NAMESPACE rougailconfig['variable_namespace_description'] = ROUGAIL_NAMESPACE_DESCRIPTION rougailconfig['tmp_dir'] = 'tmp' - if not just_copy: - rougailconfig['destinations_dir'] = INSTALL_DIR - else: - rougailconfig['destinations_dir'] = INSTALL_TEMPLATES_DIR rougailconfig['templates_dir'] = await subconfig.information.get('templates_dir') rougailconfig['patches_dir'] = await subconfig.information.get('patches_dir') rougailconfig['functions_file'] = await subconfig.information.get('functions_files') - is_host = await subconfig.information.get('module') == 'host' + module = await subconfig.information.get('module') + is_host = module == 'host' if is_host: - host_install_dir = f'{ROUGAIL_NAMESPACE}.host_install_dir' - rougailconfig['tmpfile_dest_dir'] = await subconfig.option(host_install_dir).value.get() - rougailconfig['default_systemd_directory'] = '/usr/local/lib/systemd' + rougailconfig['systemd_tmpfile_delete_before_create'] = True + if just_copy: + raise Exception('cannot generate template with option just_copy for a host') else: - rougailconfig['tmpfile_dest_dir'] = '/usr/local/lib' - rougailconfig['default_systemd_directory'] = '/systemd' + rougailconfig['systemd_tmpfile_delete_before_create'] = False + #rougailconfig['systemd_tmpfile_factory_dir'] = '/usr/local/lib' + if not just_copy: + rougailconfig['destinations_dir'] = join(INSTALL_DIR, INSTALL_CONFIG_DIR, server_name) + else: + rougailconfig['destinations_dir'] = join(INSTALL_DIR, INSTALL_TMPL_DIR, server_name) re_create(rougailconfig['destinations_dir']) re_create(rougailconfig['tmp_dir']) + engine = RougailSystemdTemplate(subconfig, rougailconfig) if just_copy: # for all engine to none @@ -104,7 +133,10 @@ async def templates(server_name, ori_engines[eng] = engine.engines[eng] engine.engines[eng] = engine.engines['none'] try: - await engine.instance_files() + if not template: + await engine.instance_files() + else: + await engine.instance_file(template) except Exception as err: print() print(f'=== Configuration: {server_name} ===') @@ -114,22 +146,37 @@ async def templates(server_name, if just_copy: for eng, old_engine in ori_engines.items(): engine.engines[eng] = old_engine + secrets_dir = join(rougailconfig['destinations_dir'], 'secrets') + if isdir(secrets_dir): + chmod(secrets_dir, 0o700) + if copy_manuals and not is_host: + dest_dir = join(INSTALL_DIR, INSTALL_IMAGES_DIR, module) + if not isdir(dest_dir): + for manual in await subconfig.information.get('manuals_dirs'): + for filename in listdir(manual): + src_file = join(manual, filename) + dst_file = join(dest_dir, filename) + copy(src_file, dst_file) + copy_tests = await config.information.get('copy_tests') + + if copy_tests and not is_host: + dest_dir = join(INSTALL_DIR, INSTALL_TESTS_DIR, module) + if not isdir(dest_dir): + for tests in await subconfig.information.get('tests_dirs'): + for filename in listdir(tests): + src_file = join(tests, filename) + dst_file = join(dest_dir, filename) + copy(src_file, dst_file) class Loader: def __init__(self, - cache_file, - cache_values, - cache_informations, clean_directories, hide_secret, original_display_name, valid_mandatories, config_file=CONFIG_FILE, ): - self.cache_file = cache_file - self.cache_values = cache_values - self.cache_informations = cache_informations self.hide_secret = hide_secret self.original_display_name = original_display_name self.valid_mandatories = valid_mandatories @@ -139,9 +186,12 @@ class Loader: rmtree(INSTALL_DIR) makedirs(INSTALL_DIR) - def before(self): + def load_tiramisu_file(self): + """Load config file (servers.yml) and build tiramisu file with dataset informations + """ with open(self.config_file, 'r') as server_fh: self.servers_json = yaml_load(server_fh, Loader=SafeLoader) + # set global rougail configuration cfg = RougailConfig.copy() cfg['variable_namespace'] = ROUGAIL_NAMESPACE cfg['variable_namespace_description'] = ROUGAIL_NAMESPACE_DESCRIPTION @@ -151,29 +201,48 @@ class Loader: cfg['force_convert_dyn_option_description'] = True cfg['risotto_globals'] = {} - rougail = RougailConvert(cfg) + # initialise variables to store useful informations + # those variables are use during templating self.templates_dir = {} self.patches_dir = {} - functions_files = set() self.functions_files = {} + self.manuals_dirs = {} + self.tests_dirs = {} + self.modules = {} + + functions_files = set() applicationservices = Applications() zones = self.servers_json['zones'] - self.modules = {} + + rougail = RougailConvert(cfg) for host_name, datas in self.servers_json['hosts'].items(): - modules_name = {mod_datas['module'] for mod_datas in datas['servers'].values()} + # load modules associate to this host + modules_name = set() + for name, mod_datas in datas['servers'].items(): + if not 'module' in mod_datas: + raise Exception(f'module is mandatory for "{name}"') + modules_name.add(mod_datas['module']) + # load modules informations from config files modules = Modules(datas['applicationservices'], applicationservices, datas['applicationservice_provider'], modules_name, self.servers_json['modules'] ) + + # load host module_info = modules.get('host') cfg['risotto_globals'][host_name] = {'global:server_name': host_name, 'global:module_name': 'host', 'global:host_install_dir': abspath(INSTALL_DIR), } functions_files |= set(module_info.functions_file) - self.load_dictionaries(cfg, module_info, host_name, rougail) + self.load_dictionaries(cfg, + module_info, + host_name, + rougail, + ) + # load servers modules_info = {} for server_name, server_datas in datas['servers'].items(): module_info = modules.get(server_datas['module']) @@ -188,11 +257,15 @@ class Loader: } server_datas['server_name'] = values[0] functions_files |= set(module_info.functions_file) - self.load_dictionaries(cfg, module_info, values[0], rougail) + self.load_dictionaries(cfg, + module_info, + values[0], + rougail, + ) modules_info[module_info.module_name] = module_info.depends self.modules[host_name] = modules_info cfg['functions_file'] = list(functions_files) - self.tiram_obj = rougail.save(self.cache_file) + self.tiram_obj = rougail.save(TIRAMISU_CACHE) def load_dictionaries(self, cfg, module_info, server_name, rougail): cfg['dictionaries_dir'] = module_info.dictionaries_dir @@ -202,11 +275,14 @@ class Loader: self.templates_dir[server_name] = module_info.templates_dir self.patches_dir[server_name] = module_info.patches_dir self.functions_files[server_name] = module_info.functions_file + self.manuals_dirs[server_name] = module_info.manuals + self.tests_dirs[server_name] = module_info.tests - async def load(self): - optiondescription = FUNCTIONS.copy() + async def tiramisu_file_to_tiramisu(self): + # l + tiramisu_space = FUNCTIONS.copy() try: - exec(self.tiram_obj, None, optiondescription) + exec(self.tiram_obj, None, tiramisu_space) except Exception as err: print(self.tiram_obj) raise Exception(f'unknown error when load tiramisu object {err}') from err @@ -214,12 +290,13 @@ class Loader: display_name = None else: display_name = tiramisu_display_name - self.config = await Config(optiondescription['option_0'], + self.config = await Config(tiramisu_space['option_0'], display_name=display_name, ) - async def after(self): + async def load_values_and_informations(self): config = self.config + await config.property.read_write() await config.property.pop('validator') await config.property.pop('cache') load_zones(self.servers_json) @@ -238,18 +315,27 @@ class Loader: await information.set('templates_dir', self.templates_dir[server_name]) await information.set('patches_dir', self.patches_dir[server_name]) await information.set('functions_files', self.functions_files[server_name]) + await information.set('manuals_dirs', self.manuals_dirs[server_name]) + await information.set('tests_dirs', self.tests_dirs[server_name]) await self.set_values(server_name, config, datas) + await config.information.set('copy_tests', False) # FIXME only one host_name is supported await config.information.set('modules', self.modules[host_name]) # await config.information.set('modules', {module_name: module_info.depends for module_name, module_info in self.module_infos.items() if module_name in modules}) - await config.property.read_only() await config.property.add('cache') if self.valid_mandatories: - await valid_mandatories(config) - with open(self.cache_values, 'w') as fh: + messages = await valid_mandatories(config) + if messages: + msg = '' + for title, variables in messages.items(): + msg += '\n' + title + '\n' + msg += '\n'.join(variables) + raise Exception(msg) + await config.property.read_only() + with open(VALUES_CACHE, 'w') as fh: json_dump(await config.value.exportation(), fh) - with open(self.cache_informations, 'w') as fh: + with open(INFORMATIONS_CACHE, 'w') as fh: json_dump(await config.information.exportation(), fh) async def set_values(self, @@ -259,6 +345,8 @@ class Loader: ): if 'values' not in datas: return + if not isinstance(datas['values'], dict): + raise Exception(f'Values of "{server_name}" are not a dict: {datas["values"]}') server_path = normalize_family(server_name) await config.owner.set(self.config_file) for vpath, value in datas['values'].items(): @@ -275,19 +363,16 @@ class Loader: raise Exception(error_msg) from err await config.owner.set('user') - async def finish(self): - await self.config.property.read_only() - class LoaderCache(Loader): - def before(self): - with open(self.cache_file) as fh: + def load_tiramisu_file(self): + with open(TIRAMISU_CACHE) as fh: self.tiram_obj = fh.read() - async def after(self): - with open(self.cache_values, 'r') as fh: + async def load_values_and_informations(self): + with open(VALUES_CACHE, 'r') as fh: await self.config.value.importation(json_load(fh)) - with open(self.cache_informations, 'r') as fh: + with open(INFORMATIONS_CACHE, 'r') as fh: informations = json_load(fh) # null is not a valid key in json => 'null' informations[None] = informations.pop('null') @@ -298,21 +383,22 @@ async def load(clean_directories=False, hide_secret=False, original_display_name: bool=False, valid_mandatories: bool=True, + copy_tests: bool=False, ): if isfile(TIRAMISU_CACHE) and isfile(VALUES_CACHE) and isfile(INFORMATIONS_CACHE): loader_obj = LoaderCache else: loader_obj = Loader - loader = loader_obj(TIRAMISU_CACHE, - VALUES_CACHE, - INFORMATIONS_CACHE, - clean_directories, + loader = loader_obj(clean_directories, hide_secret, original_display_name, valid_mandatories, ) - loader.before() - await loader.load() - await loader.after() - await loader.finish() - return loader.config + loader.load_tiramisu_file() + await loader.tiramisu_file_to_tiramisu() + await loader.load_values_and_informations() + config = loader.config + await config.property.read_only() + await config.information.set('copy_tests', copy_tests) + await config.cache.reset() + return config diff --git a/src/risotto/rougail/annotator.py b/src/risotto/rougail/annotator.py index 3c01dbd..cffb004 100644 --- a/src/risotto/rougail/annotator.py +++ b/src/risotto/rougail/annotator.py @@ -135,8 +135,6 @@ class Annotator(Walk): objectspace: 'RougailObjSpace', *args): self.objectspace = objectspace -# self.convert_get_linked_information() -# self.convert_provider() self.set_suppliers() self.convert_providers() self.convert_suppliers() @@ -159,28 +157,9 @@ class Annotator(Walk): 'zone_names': self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name'], 'zones': set(self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name']) }) - - def convert_suppliers(self): - for supplier, data in self.suppliers.items(): - if supplier == 'Host': - continue - for s_dico in data: - if supplier not in self.providers: - continue - for p_dico in self.providers[supplier]: - common_zones = s_dico['zones'] & p_dico['zones'] - if common_zones: - for idx, zone in enumerate(p_dico['zone_names']): - if zone in common_zones: - break - dns = p_dico['server_names'][idx] -# dns = p_dico["dns"] - s_dico['option'].value = dns - new_value = self.objectspace.value(None) - new_value.name = dns - s_dico['option'].value = [new_value] - break - + if not hasattr(variable, 'information'): + variable.information = self.objectspace.information(variable.xmlfiles) + variable.information.supplier = variable.supplier def convert_providers(self): self.providers = {} @@ -194,6 +173,16 @@ class Annotator(Walk): server_names = [server_name] else: server_names = self.objectspace.rougailconfig['risotto_globals'][server_name]['global:server_names'] + if provider_name != 'Host' and not provider_name.startswith('Host:') and not provider_name.startswith('global:'): + p_data = {'option': variable, + 'dns': server_name, + 'path_prefix': nf_dns, + 'server_names': server_names, + 'zone_names': self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name'], + 'zones': set(self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name']), + } + else: + p_data = None if ':' in provider_name: key_name, key_type = provider_name.rsplit(':', 1) is_provider = False @@ -201,13 +190,7 @@ class Annotator(Walk): key_name = key_type = provider_name is_provider = True if provider_name != 'Host': - self.providers.setdefault(provider_name, []).append({'option': variable, - 'dns': server_name, - 'path_prefix': nf_dns, - 'server_names': server_names, - 'zone_names': self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name'], - 'zones': set(self.objectspace.rougailconfig['risotto_globals'][server_name]['global:zones_name']), - }) + self.providers.setdefault(provider_name, []).append(p_data) if key_name != 'global' and key_name not in self.suppliers: #warn(f'cannot find supplier "{key_name}" for "{server_name}"') continue @@ -270,16 +253,30 @@ class Annotator(Walk): fill.param.append(param) if key_name == 'global': param = self.objectspace.param(variable.xmlfiles) - if provider_name not in self.objectspace.rougailconfig['risotto_globals'][server_name]: - raise DictConsistencyError(f'cannot find provider "{provider_name}" for variable "{variable.name}"', 200, variable.xmlfiles) - param.text = self.objectspace.rougailconfig['risotto_globals'][server_name][provider_name] param.name = 'value' + if provider_name in self.objectspace.rougailconfig['risotto_globals'][server_name]: + value = self.objectspace.rougailconfig['risotto_globals'][server_name][provider_name] + param.text = value + if isinstance(value, bool): + param.type = 'boolean' + else: + param.text = provider_name + param.type = 'information' fill.param.append(param) else: # parse all supplier link to current provider for idx, data in enumerate(self.suppliers[key_name]): + if p_data: + common_zones = data['zones'] & p_data['zones'] + if not common_zones: + continue + for zidx, zone in enumerate(data['zone_names']): + if zone in common_zones: + break + dns = data['server_names'][zidx] + else: + dns = data['dns'] option = data['option'] - dns = data['dns'] # if not provider, get the true option that we want has value if not is_provider: path_prefix = data['path_prefix'] @@ -330,3 +327,24 @@ class Annotator(Walk): if not hasattr(self.objectspace.space.variables[nf_dns].constraints, 'fill'): self.objectspace.space.variables[nf_dns].constraints.fill = [] self.objectspace.space.variables[nf_dns].constraints.fill.append(fill) + + def convert_suppliers(self): + for supplier, data in self.suppliers.items(): + if supplier == 'Host': + continue + for s_dico in data: + if supplier not in self.providers: + continue + for p_dico in self.providers[supplier]: + common_zones = s_dico['zones'] & p_dico['zones'] + if not common_zones: + continue + for idx, zone in enumerate(p_dico['zone_names']): + if zone in common_zones: + break + dns = p_dico['server_names'][idx] + s_dico['option'].value = dns + new_value = self.objectspace.value(None) + new_value.name = dns + s_dico['option'].value = [new_value] + break