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 0000000..fc98a99 Binary files /dev/null and b/doc/example_smtp.png differ diff --git a/doc/example_smtp.svg b/doc/example_smtp.svg new file mode 100644 index 0000000..9263a3b --- /dev/null +++ b/doc/example_smtp.svg @@ -0,0 +1,466 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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