commit 16cfea3a235c76296f07ed6f63448ca2e75f1e19 Author: Emmanuel Garette Date: Mon Dec 18 11:34:29 2023 +0100 [init] Discover Rougail diff --git a/Ansible/inventory/host.yml b/Ansible/inventory/host.yml new file mode 100644 index 0000000..2fbb50c --- /dev/null +++ b/Ansible/inventory/host.yml @@ -0,0 +1 @@ +localhost diff --git a/Ops/group_vars/.gitkeep b/Ops/group_vars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Ops/host.yml b/Ops/host.yml new file mode 100644 index 0000000..2fbb50c --- /dev/null +++ b/Ops/host.yml @@ -0,0 +1 @@ +localhost diff --git a/README.md b/README.md new file mode 100644 index 0000000..88bba52 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +A repository with comparisons between a Rougail catalog and Ansible inventories. + +Each commit presents a step of the tutorial. + +You need a working version of rougail: + +```bash +python -m venv venv +. venv/bin/activate +pip install rougail +git clone https://forge.cloud.silique.fr/stove/rougail-tutorials.git +cd rougail-tutorials +``` + + + +Proxy configuration + + - [tutorial 1.0] a single string variable + - [tutorial 1.1] advanced hostname variable + - [tutorial 1.2] a port variable + - [tutorial 1.3] default variable + - [tutorial 1.4] a variable with multiple values + - [tutorial 1.5] variable is not mandatory + +Apero + + - [tutorial 2.0] a leadership family + - [tutorial 2.1] a choice option + - [tutorial 2.2] calculed variable + +OMOGEN + + - [tutorial 3.0] a validator + +NFS configuration + + - [tutorial 4.0] a boolean variable + - [tutorial 4.1] conditional disabled variable + - [tutorial 4.2] conditional disabled + hidden diff --git a/Rougail/inventory.py b/Rougail/inventory.py new file mode 100755 index 0000000..1ce3147 --- /dev/null +++ b/Rougail/inventory.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +from rougail import Rougail, RougailConfig +from argparse import ArgumentParser +from pathlib import Path +#from pprint import pprint +from yaml import safe_load, dump +from json import dumps +from tiramisu.error import ValueOptionError, PropertiesOptionError +from traceback import print_exc +import tabulate as tabulate_module +from tabulate import tabulate + + +INV_DIR = 'inventory' +OPS_INV_DIR = 'Ops/group_vars' +ROUGAIL_VARIABLE_TYPE = 'https://forge.cloud.silique.fr/stove/rougail/src/branch/main/doc/variable/README.md#le-type-de-la-variable' + + +class Inventory: + +############################################### +# Create TIRAMISU object +############################################### + + def __init__(self): + tabulate_module.PRESERVE_WHITESPACE = True + RougailConfig['dictionaries_dir'] = ['Rougail/socle'] + RougailConfig['variable_namespace'] = 'socle' + RougailConfig['tiramisu_cache'] = 'socle.py' + catalog_dir = Path('Rougail') + for extra in catalog_dir.iterdir(): + if not extra.is_dir() or \ + extra.name in ['socle', 'host', 'jinja_cache', INV_DIR]: + continue + RougailConfig['extra_dictionaries'][extra.name] = [extra.name] + RougailConfig['functions_file'] = 'Rougail/functions.py' + self.errors = [] + self.warnings = [] + + def load(self): + rougail = Rougail() + self.conf = rougail.get_config() + self.conf.property.read_write() + self.objectspace = rougail.converted +# self.objectspace.annotate() + +############################################### +# Read Ops file +############################################### + + def load_inventory(self): + inventory_dir = Path(OPS_INV_DIR) + if not inventory_dir.is_dir(): + return + with open('Ops/host.yml') as fh: + hostnames = fh.read().strip().split('\n') + self.conf.option('socle.hostnames').value.set(hostnames) + for file in inventory_dir.iterdir(): + if file.suffix not in ['.yml', '.yaml']: + continue + with open(file) as fh: + values = safe_load(fh) + if not isinstance(values, dict): + continue + for key, value in values.items(): + self.read_inventory(self.conf.option('socle'), + key, + value, + file, + ) + + def read_inventory(self, + conf, + key, + value, + file, + *, + index=None, + ): + sub_conf = conf.option(key, index) + try: + isoptiondescription = sub_conf.isoptiondescription() + except AttributeError as err: + self.warnings.append(f'"{key}" est inconnu dans "{conf.path()}" mais est défini dans "{file}"') + return + if isoptiondescription: + if not sub_conf.isleadership(): + if not isinstance(value, dict): + print('pffff') + return + for sub_key, sub_value in value.items(): + self.read_inventory(sub_conf, + sub_key, + sub_value, + file, + ) + else: + if not isinstance(value, list): + return + leader_option = sub_conf.leader().name() + leader_value = [] + for leader in value: + if leader_option not in leader: + print('pffff') + return + leader_value.append(leader[leader_option]) + self.read_inventory(sub_conf, + leader_option, + leader_value, + file, + ) + for idx, sub_value in enumerate(value): + for sub_key, sub_value in sub_value.items(): + if sub_key == leader_option: + continue + self.read_inventory(sub_conf, + sub_key, + sub_value, + file, + index=idx, + ) + else: + try: + sub_conf.value.set(value) + except ValueOptionError as err: + self.errors.append(str(err).replace('"', "'")) + except PropertiesOptionError as err: + self.warnings.append(f'"{err}" mais est défini dans "{file}"') + +############################################### +# Host +############################################### + + def get_hosts(self): + return self.conf.option('socle.hostnames').value.get() + +############################################### +# Search unspecified mandatories variables +############################################### + + def mandatory(self): + if self.errors: + return + for idx, option in enumerate(self.conf.value.mandatory()): + if not idx: + self.errors.append("Les variables suivantes sont obligatoires mais n'ont pas de valeur :") + self.errors.append(f' - {option.doc()}') + +############################################### +# Tiramisu to inventory +############################################### + + def display(self): + ret = {} + if self.errors: + ret['_errors'] = self.errors + else: + self.conf.property.read_only() + for line, value in self.conf.value.get().items(): + self.parse_line(line, + line, + value, + ret, + False, + ) + if len(ret) == 1: + ret = ret[list(ret)[0]] + if self.warnings: + ret['_warnings'] = self.warnings + print(dumps(ret)) + + def parse_line(self, + full_path, + line, + value, + dico, + leadership, + ): + if '.' in line: + # it's a dict + family, variable = line.split('.', 1) + if '.' not in variable and self.conf.option(full_path.rsplit('.', 1)[0]).isleadership(): + dico.setdefault(family, []) + leadership = True + else: + dico.setdefault(family, {}) + leadership = False + self.parse_line(full_path, + variable, + value, + dico[family], + leadership, + ) + elif leadership: + # it's a leadership + for val in value: + dico.append({k.rsplit('.', 1)[-1]: v for k, v in val.items()}) + else: + try: + dico[line] = value + except ValueOptionError as err: + self.errors.append(str(err).replace('"', "'")) + +############################################### +# DOC +############################################### + + def display_doc(self): + examples_mini = {} + examples_all = {} + print(f'---\ngitea: none\ninclude_toc: true\n---\n') + print(f'# Variables') + for family_path in self.objectspace.parents['.']: + self.display_families(family_path, + family_path, + 2, + False, + examples_mini, + examples_all, + ) + if examples_mini: + print(f'# Example with mandatories variables') + print() + print('```') + print(dump(examples_mini, default_flow_style=False, sort_keys=False), end='') + print('```') + print() + print(f'# Example with all variables') + print() + print('```') + print(dump(examples_all, default_flow_style=False, sort_keys=False), end='') + print('```') + + def display_families(self, + family_path, + true_family_path, + level, + family_type, + examples_mini, + examples_all, + ): + variables = [] + for idx, child_name in enumerate(self.objectspace.parents[true_family_path]): + if child_name in self.objectspace.variables: + properties = self.objectspace.properties[child_name] + if ('hidden' in properties and properties['hidden'] is True) or \ + ('disabled' in properties and properties['disabled'] is True): + continue + variable = self.objectspace.paths[child_name] + variables.append(self.display_variable(variable, + level, + family_type, + family_path, + idx, + examples_mini, + examples_all, + )) + else: + if variables: + print(tabulate(variables, headers=['Parameter', 'Comment'], tablefmt="github")) + print() + variables = [] + + family = self.objectspace.paths[child_name] + if family.type == 'dynamic': + for value in family.variable.default: + sub_family_path = f'{family_path}.{family.name.replace("{{ suffix }}", value)}' + family_name = sub_family_path.rsplit('.', 1)[-1] + examples_mini[family_name] = {} + examples_all[family_name] = {} + self.display_family(family, + sub_family_path, + level, + ) + self.display_families(sub_family_path, + child_name, + level + 1, + family.type, + examples_mini[family_name], + examples_all[family_name], + ) + if not examples_mini[family_name]: + del examples_mini[family_name] + else: + if family.type == 'leadership': + examples_mini[family.name] = [] + examples_all[family.name] = [] + else: + examples_mini[family.name] = {} + examples_all[family.name] = {} + self.display_family(family, + child_name, + level, + ) + sub_family_path = f'{family_path}.{family.name}' + self.display_families(sub_family_path, + child_name, + level + 1, + family.type, + examples_mini[family.name], + examples_all[family.name], + ) + if not examples_mini[family.name]: + del examples_mini[family.name] + if variables: + print(tabulate(variables, headers=['Parameter', 'Comment'], tablefmt="github")) + print() + + def display_family(self, + family, + family_path, + level, + ): + display_path = family_path.split(".", 1)[-1] + if family.name != family.description: + title = f'{family.description} ({display_path})' + else: + title = f'{display_path}' + print('#' * level, title) + print() + informations = self.objectspace.informations.get(family.path) + if 'help' in informations: + print(informations['help']) + print() + if family.type == 'leadership': + print('This family is a leadership.') + print() + + def display_variable(self, + variable, + level, + family_type, + family_path, + index, + examples_mini, + examples_all, + ): + parameter = f'**{variable.name}**' + subparameter = [] + if 'mandatory' in self.objectspace.properties[variable.path]: + subparameter.append('mandatory') + mandatory = True + else: + mandatory = False + if variable.path in self.objectspace.multis: + if family_type == 'leadership' and index: + multi = self.objectspace.multis[variable.path] == 'submulti' + else: + multi = True + else: + multi = False + if multi: + subparameter.append('multiple') + if subparameter: + parameter += "
`" + "`, `".join(subparameter) + '`' + parameter += f'
**Type:** [`{variable.type}`]({ROUGAIL_VARIABLE_TYPE})' + comment = [] + if variable.name != variable.description: + comment.append(variable.description + '.') + informations = self.objectspace.informations.get(variable.path) + if 'help' in informations: + help_ = ' '.join([h.strip() for h in informations['help'].split('\n')]) + if not help_.endswith('.'): + help_ += '.' + comment.append(help_) + if variable.default is not None: + default = variable.default + comment.append(f'**Default:** {default}') + if variable.type == 'choice': + if isinstance(variable.choices, list): + comment.append(f'**Choices:** {", ".join(variable.choices)}') + else: + comment.append(f'**Choices:** see variable "{variable.choices.variable.split(".", 1)[-1]}"') + #choice + example_mini = None + example_all = None + properties = self.conf.option(f'{family_path}.{variable.name}').property.get() + if 'hidden' in properties or 'disabled' in properties: + pass + elif variable.test: + example = variable.test + if not multi: + example = example[0] + title = 'Example' + if mandatory: + example_mini = example + example_all = example + else: + if mandatory: + example_mini = example + example_all = example + example = ', '.join(example) + if len(variable.test) > 1: + title = 'Examples' + else: + title = 'Example' + comment.append(f'**{title}:** {example}') + elif variable.default is not None: + example = variable.default + example_all = example + else: + example = 'xxx' + if mandatory: + example_mini = example + example_all = example + if family_type == 'leadership': + if not index: + if example_mini is not None: + for mini in example_mini: + examples_mini.append({variable.name: mini}) + if example_all is not None: + for mall in example_all: + examples_all.append({variable.name: mall}) + else: + if example_mini is not None: + for idx in range(0, len(examples_mini)): + examples_mini[idx][variable.name] = example_mini + if example_all is not None: + for idx in range(0, len(examples_all)): + examples_all[idx][variable.name] = example_all + else: + if example_mini is not None: + examples_mini[variable.name] = example_mini + if example_all is not None: + examples_all[variable.name] = example_all + parameter += ' ' * (250 - len(parameter)) + comment = '
'.join(comment) + comment += ' ' * (250 - len(comment)) + return parameter, comment + +############################################### +# MAIN +############################################### + +def main(args): + inventory = Inventory() + inventory.load() + if args.list: + inventory.load_inventory() + print(dumps({'group': { + 'hosts': inventory.get_hosts(), + 'vars': {} + } + }) + ) + elif args.doc: + inventory.display_doc() + else: + inventory.load_inventory() + inventory.mandatory() + inventory.display() + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument('--list', action='store_true') + parser.add_argument('--host', action='store') + parser.add_argument('--debug', action='store_true') + parser.add_argument('--doc', action='store_true') + args = parser.parse_args() + try: + main(args) + except Exception as err: + if args.debug: + print_exc() + if args.doc: + exit(f'ERROR: {err}') + + print(dumps({'_errors': str(err)})) diff --git a/Rougail/socle/00_hosts.yml b/Rougail/socle/00_hosts.yml new file mode 100644 index 0000000..0f08b1e --- /dev/null +++ b/Rougail/socle/00_hosts.yml @@ -0,0 +1,7 @@ +--- +version: '1.0' +hostnames: + multi: true + type: hostname + params: + allow_without_dot: true diff --git a/filter_plugins/warn_me.py b/filter_plugins/warn_me.py new file mode 100644 index 0000000..27ec212 --- /dev/null +++ b/filter_plugins/warn_me.py @@ -0,0 +1,10 @@ +#!/usr/bin/python +from ansible.utils.display import Display + +class FilterModule(object): + def filters(self): return {'warn_me': self.warn_filter} + + def warn_filter(self, messages, **kwargs): + for message in messages: + Display().warning(message) + return '' diff --git a/inventory b/inventory new file mode 100755 index 0000000..2439560 --- /dev/null +++ b/inventory @@ -0,0 +1,4 @@ +#!/bin/bash +export ANSIBLE_DISPLAY_SKIPPED_HOSTS=0 +ansible-playbook -i ./Rougail/inventory.py playbook.yml -e "_type=rougail" +ansible-playbook -i ./Ansible/inventory -i Ops playbook.yml -e "_type=ansible" diff --git a/next.sh b/next.sh new file mode 100755 index 0000000..71755c3 --- /dev/null +++ b/next.sh @@ -0,0 +1,13 @@ +#!/bin/bash +BRANCH=1.0 +CURRENT_LOG=$(git log --oneline|wc -l) +MAIN_LOG=$(git log --oneline $BRANCH|wc -l) +if [ $CURRENT_LOG = $MAIN_LOG ]; then + PAGE=$CURRENT_LOG + PAGE=$((PAGE-3)) +else + PAGE=$((MAIN_LOG-CURRENT_LOG-2)) +fi +git checkout $BRANCH 2> /dev/null +git checkout HEAD~$PAGE +git log --oneline -n1 diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..8f2161f --- /dev/null +++ b/playbook.yml @@ -0,0 +1,41 @@ +--- +- name: " ++++ {{ _type | upper }} ++++ " + hosts: localhost + connection: local + tasks: + # Verify if Tiramisu makes some error + - name: Validation Rougail + ansible.builtin.debug: + when: "_type == 'rougail' and '_warnings' in vars and vars['_warnings'] | warn_me" + - name: Validation Rougail + ansible.builtin.assert: + that: + - "'_errors' not in vars" + fail_msg: "{%if '_errors' in vars %}{{ vars._errors }}{%endif%}" + when: "_type == 'rougail'" + + # Verify ansible variables + - name: Validation Ansible + include_tasks: "Ansible/asserts/{{ item }}.yml" + when: "_type == 'ansible' and ('Ansible/asserts/' + item + '.yml') is file" + loop: + - apero + - database + - nfs + - omogen + - proxy + - name: Validation Ansible + include_tasks: "Ansible/asserts/proxy.yml" + when: "_type == 'ansible'" + + # Display inventory that startswith env_ + - name: Display inventory + ansible.builtin.debug: + msg: "{{ item }}" + when: item.key.startswith('env_') + loop: "{{ vars | dict2items }}" + + # Display some ansible variables + - name: "Display" + include_tasks: "Ansible/asserts/display.yml" + when: "_type == 'ansible' and 'Ansible/asserts/display.yml' is file"