init
This commit is contained in:
parent
a29cdce51e
commit
3547a78605
4 changed files with 939 additions and 0 deletions
678
inventory.py
Normal file
678
inventory.py
Normal file
|
@ -0,0 +1,678 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from pathlib import Path
|
||||||
|
from yaml import safe_load
|
||||||
|
from json import dumps
|
||||||
|
from traceback import print_exc
|
||||||
|
from os import environ
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rich.tree import Tree
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from ansible.parsing.vault import VaultLib, PromptVaultSecret
|
||||||
|
from ansible.module_utils._text import to_bytes
|
||||||
|
|
||||||
|
from tiramisu import PasswordOption, Config
|
||||||
|
from tiramisu.error import ValueOptionError, PropertiesOptionError, LeadershipError
|
||||||
|
from tiramisu.i18n import _
|
||||||
|
import rougail
|
||||||
|
from rougail import Rougail, RougailConfig
|
||||||
|
from rougail.utils import normalize_family
|
||||||
|
from rougail.annotator import CONVERT_OPTION
|
||||||
|
|
||||||
|
|
||||||
|
NAMESPACE = "socle"
|
||||||
|
NAMESPACE_HOSTS = "hosts"
|
||||||
|
VERSION_FILE = None
|
||||||
|
|
||||||
|
|
||||||
|
def tiramisu_display_name(kls) -> str:
|
||||||
|
"""Replace the Tiramisu display_name function to display path + description"""
|
||||||
|
try:
|
||||||
|
doc = kls.impl_get_information("doc", None)
|
||||||
|
name = kls.impl_getname()
|
||||||
|
except AttributeError:
|
||||||
|
doc = kls.opt.impl_get_information('doc', None)
|
||||||
|
name = kls.opt.impl_getname()
|
||||||
|
comment = f" ({doc})" if doc and doc != name else ""
|
||||||
|
return f"{kls.impl_getpath()}{comment}"
|
||||||
|
|
||||||
|
|
||||||
|
rougail.tiramisu_display_name = tiramisu_display_name
|
||||||
|
|
||||||
|
|
||||||
|
class DSOFPasswordOption(PasswordOption):
|
||||||
|
def __init__(self,
|
||||||
|
*args,
|
||||||
|
min_len=12,
|
||||||
|
max_len=None,
|
||||||
|
forbidden_char=[],
|
||||||
|
**kwargs):
|
||||||
|
extra = {}
|
||||||
|
extra = {'min_len': min_len}
|
||||||
|
if max_len is not None:
|
||||||
|
extra['max_len'] = max_len
|
||||||
|
if forbidden_char:
|
||||||
|
extra['forbidden_char'] = set(forbidden_char)
|
||||||
|
super().__init__(*args, extra=extra, **kwargs)
|
||||||
|
|
||||||
|
def validate(self,
|
||||||
|
value: str) -> None:
|
||||||
|
super().validate(value)
|
||||||
|
if len(value) <= self.impl_get_extra('min_len'):
|
||||||
|
raise ValueError(f'il faut au minimum {self.impl_get_extra("min_len")} caractères')
|
||||||
|
max_len = self.impl_get_extra('max_len')
|
||||||
|
if max_len and len(value) > max_len:
|
||||||
|
raise ValueError(f'il faut au maximum {max_len} caractères')
|
||||||
|
if self.impl_get_extra("forbidden_char"):
|
||||||
|
forbidden_char = set(value) & self.impl_get_extra("forbidden_char")
|
||||||
|
if forbidden_char:
|
||||||
|
raise ValueError(f'ne doit pas avoir les caracteres speciaux {",".join(forbidden_char)}')
|
||||||
|
|
||||||
|
|
||||||
|
CONVERT_OPTION["secret"] = dict(opttype="DSOFPasswordOption")
|
||||||
|
|
||||||
|
|
||||||
|
class DSOFRougail(Rougail):
|
||||||
|
def get_config(self):
|
||||||
|
"""Get Tiramisu Config"""
|
||||||
|
if not self.config:
|
||||||
|
tiram_obj = self.converted.save(self.rougailconfig["tiramisu_cache"])
|
||||||
|
optiondescription = {}
|
||||||
|
exec(tiram_obj, {"DSOFPasswordOption": DSOFPasswordOption}, optiondescription) # pylint: disable=W0122
|
||||||
|
self.config = Config(
|
||||||
|
optiondescription["option_0"],
|
||||||
|
display_name=tiramisu_display_name,
|
||||||
|
)
|
||||||
|
self.config.property.read_write()
|
||||||
|
for mode in self.rougailconfig['modes_level']:
|
||||||
|
self.config.permissive.add(mode)
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
|
||||||
|
class Inventory:
|
||||||
|
###############################################
|
||||||
|
# Create TIRAMISU object
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def __init__(self, args, for_doc=False):
|
||||||
|
self.args = args
|
||||||
|
sub_inventory_dir = Path('inventaires') / 'rougail'
|
||||||
|
self.inside_git_dir = False
|
||||||
|
inventory_dir = None
|
||||||
|
if for_doc:
|
||||||
|
socle_inventory_dir = Path(__file__).parent.parent / 'inventaires' / "rougail"
|
||||||
|
if socle_inventory_dir.is_dir:
|
||||||
|
inventory_dir = str(socle_inventory_dir)
|
||||||
|
if not inventory_dir and not sub_inventory_dir.is_dir():
|
||||||
|
if self.args.debug:
|
||||||
|
print(f'cannot find {sub_inventory_dir}')
|
||||||
|
for git_file in [Path.cwd().parent / 'inventaires' / "rougail",
|
||||||
|
Path.cwd() / 'packaging' / 'inventaires' / "rougail",
|
||||||
|
]:
|
||||||
|
if git_file.is_dir():
|
||||||
|
if self.args.debug:
|
||||||
|
print('inside git dir !')
|
||||||
|
self.inside_git_dir = True
|
||||||
|
inventory_dir = git_file
|
||||||
|
break
|
||||||
|
if not inventory_dir:
|
||||||
|
inventory_dir = sub_inventory_dir
|
||||||
|
RougailConfig['dictionaries_dir'] = [inventory_dir]
|
||||||
|
RougailConfig['variable_namespace'] = NAMESPACE
|
||||||
|
RougailConfig['extra_dictionaries'][NAMESPACE_HOSTS] = ['inventaires/hosts/']
|
||||||
|
if for_doc:
|
||||||
|
project_name = Path().cwd().name.rsplit('-', 1)[0]
|
||||||
|
versions = {project_name: {'docker_version': None}}
|
||||||
|
else:
|
||||||
|
with VERSION_FILE.open() as fh:
|
||||||
|
versions = safe_load(fh)
|
||||||
|
for project, version in versions.items():
|
||||||
|
if "docker_version" not in version:
|
||||||
|
continue
|
||||||
|
project_name = self.get_project_name(project)
|
||||||
|
# get the first directory in ~/DSOF-SIx-xxxx/<version>/
|
||||||
|
if for_doc:
|
||||||
|
root_path = Path.cwd() / 'packaging'
|
||||||
|
elif self.inside_git_dir:
|
||||||
|
root_path = Path.cwd().parent.parent.parent / (project + '-packaging') / 'packaging'
|
||||||
|
else:
|
||||||
|
root_path = Path.home() / project / version['packaging_version']
|
||||||
|
if root_path.is_dir():
|
||||||
|
root_path = next(root_path.iterdir())
|
||||||
|
path = root_path / sub_inventory_dir
|
||||||
|
if path.is_dir():
|
||||||
|
RougailConfig['extra_dictionaries'][project_name] = [str(path)]
|
||||||
|
elif self.args.debug:
|
||||||
|
print(f'cannot find {path}')
|
||||||
|
#FIXME
|
||||||
|
#RougailConfig['tiramisu_cache'] = 'socle.py'
|
||||||
|
RougailConfig['functions_file'] = 'Rougail/functions.py'
|
||||||
|
self.errors = []
|
||||||
|
self.warnings = []
|
||||||
|
self.console = Console(force_terminal=True)
|
||||||
|
|
||||||
|
def get_project_name(self, project):
|
||||||
|
return normalize_family(project.split('-', 2)[-1])
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
rougail = DSOFRougail()
|
||||||
|
self.conf = rougail.get_config()
|
||||||
|
self.conf.property.read_write()
|
||||||
|
self.objectspace = rougail.converted
|
||||||
|
# self.objectspace.annotate()
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Read Ops file
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def load_custom(self):
|
||||||
|
if not CUSTOM_FILE or not CUSTOM_FILE.is_file():
|
||||||
|
if self.args.debug:
|
||||||
|
print(f'cannot find {CUSTOM_FILE}')
|
||||||
|
return
|
||||||
|
prompt = PromptVaultSecret(INVENTORY_PASSWORD, 'default', ["Vault password: "])
|
||||||
|
if not INVENTORY_PASSWORD:
|
||||||
|
prompt.load()
|
||||||
|
vault = VaultLib([('default', prompt)])
|
||||||
|
with CUSTOM_FILE.open('rb') as fh:
|
||||||
|
values = safe_load(vault.decrypt(fh.read()))
|
||||||
|
for key, value in values.items():
|
||||||
|
self.read_inventory(self.conf,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
CUSTOM_FILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_env(self):
|
||||||
|
if not CURRENT_ENV and self.inside_git_dir:
|
||||||
|
current_env = 'git'
|
||||||
|
else:
|
||||||
|
current_env = CURRENT_ENV
|
||||||
|
if not current_env:
|
||||||
|
self.errors.append(f"la variable d'environnement \"current_env\" est obligatoire")
|
||||||
|
return
|
||||||
|
self.conf.option(f"socle.env_name").value.set(current_env)
|
||||||
|
|
||||||
|
def set_versions(self):
|
||||||
|
if self.errors:
|
||||||
|
return
|
||||||
|
with VERSION_FILE.open() as fh:
|
||||||
|
versions = safe_load(fh)
|
||||||
|
for project, version in versions.items():
|
||||||
|
project_name = self.get_project_name(project)
|
||||||
|
if "docker_version" not in version:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
self.conf.option(f"{project_name}.compose.image_tag").value.set(version["docker_version"])
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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:
|
||||||
|
# ugly
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
paths, options = conf.get()._children
|
||||||
|
except:
|
||||||
|
paths, options = conf.get().opt._children
|
||||||
|
if '{{ suffix }}' in paths:
|
||||||
|
option = options[paths.index('{{ suffix }}')]
|
||||||
|
path = option._suffixes.params.args[0].option._path
|
||||||
|
# ugly, assume that dynamic variable are in parent family
|
||||||
|
if '{{ suffix }}' in path:
|
||||||
|
cnt = path.count('.')
|
||||||
|
sub_path = sub_conf._path
|
||||||
|
subcnt = sub_path.count('.')
|
||||||
|
path = sub_path.rsplit('.', subcnt - cnt + 1)[0] + '.' + path.rsplit('.', 1)[-1]
|
||||||
|
values = self.conf.option(path).value.get()
|
||||||
|
key = normalize_family(key)
|
||||||
|
values.append(key)
|
||||||
|
self.conf.option(path).value.set(values)
|
||||||
|
self.read_inventory(conf, key, value, file, index=index)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
if args.debug:
|
||||||
|
print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
if conf == self.conf:
|
||||||
|
path = NAMESPACE
|
||||||
|
else:
|
||||||
|
path = conf.path()
|
||||||
|
self.warnings.append(f'"{key}" est inconnu dans "{path}" mais est défini dans "{file}"')
|
||||||
|
return
|
||||||
|
except LeadershipError as err:
|
||||||
|
if args.debug:
|
||||||
|
print_exc()
|
||||||
|
self.errors.append(str(err))
|
||||||
|
return
|
||||||
|
if isoptiondescription:
|
||||||
|
if not sub_conf.isleadership():
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
print('pffff1')
|
||||||
|
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:
|
||||||
|
self.warnings.append(f'cannot find leader "{sub_conf.leader().path()} in {list(leader)}')
|
||||||
|
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.permissive.set(frozenset(['advanced']))
|
||||||
|
sub_conf.value.set(value)
|
||||||
|
except ValueOptionError as err:
|
||||||
|
if args.debug:
|
||||||
|
print_exc()
|
||||||
|
self.errors.append(str(err).replace('"', "'"))
|
||||||
|
except PropertiesOptionError as err:
|
||||||
|
if args.debug:
|
||||||
|
print_exc()
|
||||||
|
self.warnings.append(f'"{err}" mais est défini dans "{file}"')
|
||||||
|
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Host
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def get_hosts(self):
|
||||||
|
ret = {"_meta": {"hostvars": {}}, "all": {"children": ["ungrouped"]}}
|
||||||
|
if self.errors:
|
||||||
|
ret["_meta"]["hostvars"]["localhost"] = {'_errors': self.errors}
|
||||||
|
ret["ungrouped"] = {"hosts": ["localhost"]}
|
||||||
|
else:
|
||||||
|
inventories = {}
|
||||||
|
self.conf.property.read_only()
|
||||||
|
for line, value in self.conf.value.get().items():
|
||||||
|
self.parse_line(line,
|
||||||
|
line,
|
||||||
|
'',
|
||||||
|
value,
|
||||||
|
inventories,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
if NAMESPACE_HOSTS not in inventories:
|
||||||
|
return ret
|
||||||
|
hostnames = inventories[NAMESPACE_HOSTS]['hostnames']
|
||||||
|
ret_hosts = {}
|
||||||
|
for name, hosts in hostnames.items():
|
||||||
|
if 'hosts' in hosts:
|
||||||
|
for idx, host in enumerate(hosts['hosts']):
|
||||||
|
index = str(idx + 1)
|
||||||
|
if idx < 9:
|
||||||
|
index = '0' + index
|
||||||
|
host_name = hosts['prefix_name'] + index
|
||||||
|
ret_hosts.setdefault(name, {})[host_name] = host
|
||||||
|
ret.setdefault(name, {}).setdefault('hosts', []).append(host_name)
|
||||||
|
else:
|
||||||
|
ret["all"]["children"].append(name)
|
||||||
|
ret[name] = hosts
|
||||||
|
for hosts in ret_hosts.values():
|
||||||
|
for host, domain_name in hosts.items():
|
||||||
|
ret['_meta']['hostvars'][host] = {'ansible_host': domain_name}
|
||||||
|
ret['_meta']['hostvars'][host].update(inventories)
|
||||||
|
#if CURRENT_NAMESPACE != NAMESPACE:
|
||||||
|
# ret['_meta']['hostvars'][host][CURRENT_NAMESPACE] = inventories[CURRENT_NAMESPACE]
|
||||||
|
# print(ret_hosts)
|
||||||
|
# ret['_meta']['hostvars']['localhost'] = inventories
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def display_hosts(self):
|
||||||
|
self.conf.property.read_only()
|
||||||
|
hostnames = self.conf.option(NAMESPACE_HOSTS).option('hostnames')
|
||||||
|
ret_hosts = {}
|
||||||
|
for optiondescription in hostnames.list('optiondescription'):
|
||||||
|
hosts = optiondescription.value.get()
|
||||||
|
try:
|
||||||
|
prefix_name = optiondescription.option('prefix_name').value.get()
|
||||||
|
except AttributeError:
|
||||||
|
# it's a group
|
||||||
|
continue
|
||||||
|
for idx, host in enumerate(optiondescription.option('hosts').value.get()):
|
||||||
|
index = str(idx + 1)
|
||||||
|
if idx < 9:
|
||||||
|
index = '0' + index
|
||||||
|
host_name = prefix_name + index
|
||||||
|
ret_hosts[host_name] = host
|
||||||
|
|
||||||
|
print()
|
||||||
|
header = Table(show_header=False, title="Liste des adresses")
|
||||||
|
for key, value in ret_hosts.items():
|
||||||
|
header.add_row(key, value)
|
||||||
|
self.console.print(header)
|
||||||
|
###############################################
|
||||||
|
# Search unspecified mandatories variables
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def mandatory(self):
|
||||||
|
title = False
|
||||||
|
options_with_error = []
|
||||||
|
for option in self.conf.value.mandatory():
|
||||||
|
try:
|
||||||
|
option.value.get()
|
||||||
|
if not title:
|
||||||
|
self.errors.append("Les variables suivantes sont obligatoires mais n'ont pas de valeur :")
|
||||||
|
title = True
|
||||||
|
self.errors.append(f' - {option.doc()}')
|
||||||
|
except PropertiesOptionError:
|
||||||
|
options_with_error.append(option)
|
||||||
|
if not title:
|
||||||
|
for idx, option in enumerate(options_with_error):
|
||||||
|
if not idx:
|
||||||
|
self.errors.append("Les variables suivantes sont inaccessibles mais sont vides :")
|
||||||
|
self.errors.append(f' - {option.doc()}')
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Tiramisu to inventory
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
def rich(self, ret, tree):
|
||||||
|
for key, value in ret.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
subtree = tree.add(f":open_file_folder: {key}",
|
||||||
|
guide_style="bold bright_blue",
|
||||||
|
)
|
||||||
|
self.rich(value, subtree)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
subtree = tree.add(f":notebook: {key} :",
|
||||||
|
guide_style="bold bright_blue",
|
||||||
|
)
|
||||||
|
for val in value:
|
||||||
|
subtree.add(str(val))
|
||||||
|
else:
|
||||||
|
tree.add(f":notebook: {key} : {value}")
|
||||||
|
|
||||||
|
def rich_display(self):
|
||||||
|
header_variable = 'Variable\n'
|
||||||
|
header_variable += '[bright_blue]Variable non documentée[/bright_blue]\n'
|
||||||
|
header_variable += '[red1]Variable non documentée mais modifiée[/red1]'
|
||||||
|
if self.args.read_only:
|
||||||
|
header_variable += '\n[orange1]Variable non modifiable[/orange1]'
|
||||||
|
header_value = '[gold1]Valeur par défaut[/gold1]\n'
|
||||||
|
header_value += 'Valeur modifiée\n'
|
||||||
|
header_value += '([red1]Valeur par défaut originale[/red1])'
|
||||||
|
header = Table.grid(padding=1, collapse_padding=True)
|
||||||
|
header.pad_edge = False
|
||||||
|
header.add_row(header_variable, header_value)
|
||||||
|
header = Panel.fit(header, title="Légende")
|
||||||
|
self.console.print(header)
|
||||||
|
inventories = {}
|
||||||
|
if self.args.read_only:
|
||||||
|
self.conf.property.read_only()
|
||||||
|
else:
|
||||||
|
self.conf.property.read_write()
|
||||||
|
for line, value in self.conf.value.get().items():
|
||||||
|
self.parse_line(line,
|
||||||
|
line,
|
||||||
|
'',
|
||||||
|
value,
|
||||||
|
inventories,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
for_doc=True,
|
||||||
|
)
|
||||||
|
for warning in self.warnings:
|
||||||
|
self.console.print(Tree(f":warning: {warning}"))
|
||||||
|
if self.errors:
|
||||||
|
self.display_errors()
|
||||||
|
tree = Tree(":open_file_folder: Inventaire",
|
||||||
|
guide_style="bold bright_blue",
|
||||||
|
)
|
||||||
|
self.rich(inventories, tree)
|
||||||
|
self.console.print(tree)
|
||||||
|
self.display_hosts()
|
||||||
|
|
||||||
|
def display_errors(self):
|
||||||
|
tree = Tree(":stop_sign: ERREURS",
|
||||||
|
guide_style="bold bright_red",
|
||||||
|
)
|
||||||
|
for error in self.errors:
|
||||||
|
tree.add(error)
|
||||||
|
self.console.print(tree)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def parse_line(self,
|
||||||
|
full_path,
|
||||||
|
line,
|
||||||
|
parent_path,
|
||||||
|
value,
|
||||||
|
dico,
|
||||||
|
leadership,
|
||||||
|
family_hidden,
|
||||||
|
*,
|
||||||
|
for_doc=False,
|
||||||
|
):
|
||||||
|
if '.' in line:
|
||||||
|
# it's a dict
|
||||||
|
family, variable = line.split('.', 1)
|
||||||
|
current_path = parent_path
|
||||||
|
if current_path:
|
||||||
|
current_path += '.'
|
||||||
|
current_path += family
|
||||||
|
if for_doc:
|
||||||
|
if 'hidden' in self.conf.option(current_path).property.get() or family_hidden:
|
||||||
|
family_hidden = True
|
||||||
|
family = f'[orange1]{family}[/orange1]'
|
||||||
|
elif 'advanced' in self.conf.option(current_path).property.get():
|
||||||
|
family = f'[bright_blue]{family}[/bright_blue]'
|
||||||
|
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,
|
||||||
|
current_path,
|
||||||
|
value,
|
||||||
|
dico[family],
|
||||||
|
leadership,
|
||||||
|
family_hidden,
|
||||||
|
for_doc=for_doc,
|
||||||
|
)
|
||||||
|
elif leadership:
|
||||||
|
# it's a leadership
|
||||||
|
for idx, val in enumerate(value):
|
||||||
|
dic = {k.rsplit('.', 1)[-1]: v for k, v in val.items()}
|
||||||
|
if for_doc:
|
||||||
|
leader = True
|
||||||
|
for k, v in val.items():
|
||||||
|
if leader:
|
||||||
|
is_default = self.conf.option(k).owner.isdefault()
|
||||||
|
properties = self.conf.option(k).property.get()
|
||||||
|
else:
|
||||||
|
is_default = self.conf.option(k, idx).owner.isdefault()
|
||||||
|
properties = self.conf.option(k, idx).property.get()
|
||||||
|
if self.conf.option(k).type() == _('password') and not self.args.show_password:
|
||||||
|
v = "*" * 10
|
||||||
|
subpath = k.rsplit('.', 1)[-1]
|
||||||
|
if 'hidden' in properties or family_hidden:
|
||||||
|
subpath = f'[orange1]{subpath}[/orange1]'
|
||||||
|
elif 'advanced' in properties:
|
||||||
|
if isdefault:
|
||||||
|
subpath = f'[bright_blue]{subpath}[/bright_blue]'
|
||||||
|
else:
|
||||||
|
subpath = f'[red1]{subpath}[/red1]'
|
||||||
|
if is_default:
|
||||||
|
v = '[gold1]' + str(v) + '[/gold1]'
|
||||||
|
dico.append(f'{subpath}: {v}')
|
||||||
|
leader = False
|
||||||
|
else:
|
||||||
|
dico.append(dic)
|
||||||
|
else:
|
||||||
|
# it's a variable
|
||||||
|
is_default = self.conf.option(full_path).owner.isdefault()
|
||||||
|
default_value = None
|
||||||
|
if for_doc:
|
||||||
|
mod_is_red = False
|
||||||
|
if not is_default:
|
||||||
|
true_default_value = self.conf.option(full_path).value.default()
|
||||||
|
if true_default_value and true_default_value != value:
|
||||||
|
if isinstance(true_default_value, list):
|
||||||
|
default_value = [f' ([red1]{true}[/red1])' for true in true_default_value]
|
||||||
|
else:
|
||||||
|
default_value = f' ([red1]{true_default_value}[/red1])'
|
||||||
|
mod_is_red = True
|
||||||
|
if self.conf.option(full_path).type() == _('password') and not self.args.show_password:
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = ["*" * 10 for val in value]
|
||||||
|
else:
|
||||||
|
value = "*" * 10
|
||||||
|
if 'hidden' in self.conf.option(full_path).property.get() or family_hidden:
|
||||||
|
line = f'[orange1]{line}[/orange1]'
|
||||||
|
elif 'advanced' in self.conf.option(full_path).property.get():
|
||||||
|
if not mod_is_red:
|
||||||
|
line = f'[bright_blue]{line}[/bright_blue]'
|
||||||
|
else:
|
||||||
|
line = f'[red1]{line}[/red1]'
|
||||||
|
if is_default:
|
||||||
|
if isinstance(value, list):
|
||||||
|
dico[line] = ['[gold1]' + str(val) + '[/gold1]' for val in value]
|
||||||
|
else:
|
||||||
|
dico[line] = '[gold1]' + str(value) + '[/gold1]'
|
||||||
|
if (for_doc and not is_default) or not for_doc:
|
||||||
|
if default_value:
|
||||||
|
if isinstance(value, list):
|
||||||
|
len_value = len(value)
|
||||||
|
len_default_value = len(default_value)
|
||||||
|
len_values = max(len_value, len_default_value)
|
||||||
|
new_value = []
|
||||||
|
for idx in range(len_values):
|
||||||
|
new = ''
|
||||||
|
if idx < len_value:
|
||||||
|
new += value[idx]
|
||||||
|
if idx < len_default_value:
|
||||||
|
new += default_value[idx]
|
||||||
|
new_value.append(new)
|
||||||
|
value = new_value
|
||||||
|
else:
|
||||||
|
value = str(value) + default_value
|
||||||
|
dico[line] = value
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
inventory = Inventory(args)
|
||||||
|
inventory.load()
|
||||||
|
inventory.load_custom()
|
||||||
|
inventory.set_env()
|
||||||
|
inventory.set_versions()
|
||||||
|
if inventory.errors:
|
||||||
|
inventory.display_errors()
|
||||||
|
if args.hosts:
|
||||||
|
inventory.display_hosts()
|
||||||
|
elif args.list:
|
||||||
|
print(dumps(inventory.get_hosts(), ensure_ascii=False, indent=2)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not args.no_mandatory:
|
||||||
|
inventory.mandatory()
|
||||||
|
if args.host:
|
||||||
|
# inventory is already set during --list
|
||||||
|
print(dumps({}))
|
||||||
|
else:
|
||||||
|
inventory.rich_display()
|
||||||
|
|
||||||
|
|
||||||
|
def get_argsparse():
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument('--list', action='store_true')
|
||||||
|
parser.add_argument('--host', action='store')
|
||||||
|
parser.add_argument('--hosts', action='store_true')
|
||||||
|
parser.add_argument('--debug', action='store_true')
|
||||||
|
parser.add_argument('--read_only', action='store_true')
|
||||||
|
parser.add_argument('--no_mandatory', action='store_true')
|
||||||
|
parser.add_argument('--show_password', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.debug:
|
||||||
|
level = logging.DEBUG
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if 'CURRENT_ENV' not in environ:
|
||||||
|
CURRENT_ENV = environ.get('current_env')
|
||||||
|
else:
|
||||||
|
CURRENT_ENV = environ['CURRENT_ENV']
|
||||||
|
if 'VERSION_FILE' not in environ:
|
||||||
|
VERSION_FILE = Path('versions.yml')
|
||||||
|
else:
|
||||||
|
VERSION_FILE = Path(environ['VERSION_FILE'])
|
||||||
|
if 'INVENTORY_PASSWORD' in environ:
|
||||||
|
INVENTORY_PASSWORD = environ['INVENTORY_PASSWORD']
|
||||||
|
INVENTORY_PASSWORD = to_bytes(INVENTORY_PASSWORD, errors='strict', nonstring='simplerepr').strip()
|
||||||
|
else:
|
||||||
|
INVENTORY_PASSWORD = None
|
||||||
|
|
||||||
|
if CURRENT_ENV:
|
||||||
|
CUSTOM_FILE = Path.cwd().parent.parent / 'inventaires' / CURRENT_ENV / "custom.yml"
|
||||||
|
else:
|
||||||
|
CUSTOM_FILE = Path("custom.yml")
|
||||||
|
|
||||||
|
if not VERSION_FILE.is_file():
|
||||||
|
raise Exception(f'cannot find VERSION_FILE "{VERSION_FILE}"')
|
||||||
|
|
||||||
|
try:
|
||||||
|
args = get_argsparse()
|
||||||
|
except Exception as err:
|
||||||
|
print(dumps({'_errors': str(err)}, ensure_ascii=False, indent=2))
|
||||||
|
exit(1)
|
||||||
|
try:
|
||||||
|
main(args)
|
||||||
|
except Exception as err:
|
||||||
|
if args.debug:
|
||||||
|
print_exc()
|
||||||
|
if args.list or args.host:
|
||||||
|
print(dumps({'_errors': str(err)}, ensure_ascii=False, indent=2))
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
tree = Tree(":stop_sign: ERREURS",
|
||||||
|
guide_style="bold bright_red",
|
||||||
|
)
|
||||||
|
tree.add(str(err))
|
||||||
|
Console().print(tree)
|
||||||
|
exit(1)
|
169
src/rougail/user_data_file/__init__.py
Normal file
169
src/rougail/user_data_file/__init__.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from rougail import RougailConfig
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
from tiramisu.error import ValueOptionError, PropertiesOptionError, LeadershipError
|
||||||
|
|
||||||
|
|
||||||
|
class RougailUserDataFile:
|
||||||
|
def __init__(self,
|
||||||
|
conf,
|
||||||
|
*,
|
||||||
|
user_datas=None,
|
||||||
|
rougailconfig=None,
|
||||||
|
) -> None:
|
||||||
|
if rougailconfig is None:
|
||||||
|
rougailconfig = RougailConfig
|
||||||
|
self.rougailconfig = rougailconfig
|
||||||
|
self.filename = self.rougailconfig['file.filename']
|
||||||
|
self.yaml = YAML()
|
||||||
|
self.conf = conf
|
||||||
|
if user_datas:
|
||||||
|
self.errors = user_datas['errors']
|
||||||
|
self.warnings = user_datas['warnings']
|
||||||
|
else:
|
||||||
|
self.errors = []
|
||||||
|
self.warnings = []
|
||||||
|
|
||||||
|
|
||||||
|
def read(self,
|
||||||
|
) -> None:
|
||||||
|
with open(self.filename) as fh_config:
|
||||||
|
values = self.yaml.load(fh_config)
|
||||||
|
if not values:
|
||||||
|
return
|
||||||
|
for key, value in values.items():
|
||||||
|
self.parse(self.conf,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse(self,
|
||||||
|
conf,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
*,
|
||||||
|
index=None,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
sub_conf = conf.option(key, index)
|
||||||
|
except AttributeError as err:
|
||||||
|
self.errors.append(str(err))
|
||||||
|
return
|
||||||
|
# try:
|
||||||
|
# isoptiondescription = sub_conf.isoptiondescription()
|
||||||
|
# except AttributeError as err:
|
||||||
|
## if args.debug:
|
||||||
|
## print_exc()
|
||||||
|
# if index is not None:
|
||||||
|
# msg = f'cannot find {sub_conf.path()} with index {index}'
|
||||||
|
# else:
|
||||||
|
# msg = f'cannot find {sub_conf.path()}'
|
||||||
|
# self.errors.append(msg)
|
||||||
|
# return
|
||||||
|
## # ugly
|
||||||
|
## try:
|
||||||
|
## try:
|
||||||
|
## paths, options = conf.get()._children
|
||||||
|
## except:
|
||||||
|
## paths, options = conf.get().opt._children
|
||||||
|
## if '{{ suffix }}' in paths:
|
||||||
|
## option = options[paths.index('{{ suffix }}')]
|
||||||
|
## path = option._suffixes.params.args[0].option._path
|
||||||
|
## # ugly, assume that dynamic variable are in parent family
|
||||||
|
## if '{{ suffix }}' in path:
|
||||||
|
## cnt = path.count('.')
|
||||||
|
## sub_path = sub_conf._path
|
||||||
|
## subcnt = sub_path.count('.')
|
||||||
|
## path = sub_path.rsplit('.', subcnt - cnt + 1)[0] + '.' + path.rsplit('.', 1)[-1]
|
||||||
|
## values = self.conf.option(path).value.get()
|
||||||
|
## key = normalize_family(key)
|
||||||
|
## values.append(key)
|
||||||
|
## self.conf.option(path).value.set(values)
|
||||||
|
## self.parse(conf, key, value, index=index)
|
||||||
|
## return
|
||||||
|
## except Exception:
|
||||||
|
## if args.debug:
|
||||||
|
## print_exc()
|
||||||
|
## pass
|
||||||
|
##
|
||||||
|
## if conf == self.conf:
|
||||||
|
## path = NAMESPACE
|
||||||
|
## else:
|
||||||
|
## path = conf.path()
|
||||||
|
## self.warnings.append(f'"{key}" est inconnu dans "{path}" mais est défini dans "{self.filename}"')
|
||||||
|
## return
|
||||||
|
except LeadershipError as err:
|
||||||
|
# if args.debug:
|
||||||
|
# print_exc()
|
||||||
|
self.errors.append(str(err))
|
||||||
|
return
|
||||||
|
if sub_conf.isoptiondescription():
|
||||||
|
return self.parse_optiondescription(sub_conf,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
self.load(sub_conf,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def load(self,
|
||||||
|
conf,
|
||||||
|
value,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# sub_conf.permissive.set(frozenset(['advanced']))
|
||||||
|
conf.value.set(value)
|
||||||
|
except ValueOptionError as err:
|
||||||
|
# if args.debug:
|
||||||
|
# print_exc()
|
||||||
|
self.errors.append(str(err).replace('"', "'"))
|
||||||
|
except PropertiesOptionError as err:
|
||||||
|
# if args.debug:
|
||||||
|
# print_exc()
|
||||||
|
self.warnings.append(f'"{err}" but is defined in "{self.filename}"')
|
||||||
|
|
||||||
|
def parse_optiondescription(self,
|
||||||
|
conf,
|
||||||
|
value,
|
||||||
|
) -> None:
|
||||||
|
if conf.isleadership():
|
||||||
|
return self.parse_leadership(value,
|
||||||
|
conf,
|
||||||
|
)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
self.warnings.append(f'invalid value "{value}" for the optiondescription {conf.path()}')
|
||||||
|
return
|
||||||
|
for sub_key, sub_value in value.items():
|
||||||
|
self.parse(conf,
|
||||||
|
sub_key,
|
||||||
|
sub_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_leadership(self,
|
||||||
|
value,
|
||||||
|
conf,
|
||||||
|
):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
self.warnings.append(f'invalid value "{value}" for the leadership {conf.path()}')
|
||||||
|
return
|
||||||
|
leader_name = conf.leader().name()
|
||||||
|
leader_value = []
|
||||||
|
for leader in value:
|
||||||
|
if leader_name not in leader:
|
||||||
|
self.warnings.append(f'cannot find value for the leader "{conf.leader().path()} in {list(leader)}')
|
||||||
|
return
|
||||||
|
#leader_value.append(leader[leader_name])
|
||||||
|
leader_value.append(leader.pop(leader_name))
|
||||||
|
self.parse(conf,
|
||||||
|
leader_name,
|
||||||
|
leader_value,
|
||||||
|
)
|
||||||
|
for idx, sub_value in enumerate(value):
|
||||||
|
for sub_key, sub_value in sub_value.items():
|
||||||
|
# if sub_key == leader_name:
|
||||||
|
# continue
|
||||||
|
self.parse(conf,
|
||||||
|
sub_key,
|
||||||
|
sub_value,
|
||||||
|
index=idx,
|
||||||
|
)
|
36
src/rougail/user_data_file/cli.py
Normal file
36
src/rougail/user_data_file/cli.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
Cli code for Rougail-user-data-file
|
||||||
|
|
||||||
|
Silique (https://www.silique.fr)
|
||||||
|
Copyright (C) 2024
|
||||||
|
|
||||||
|
distribued with GPL-2 or later license
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""
|
||||||
|
from . import RougailUserDataFile
|
||||||
|
|
||||||
|
|
||||||
|
def run(rougailconfig,
|
||||||
|
config,
|
||||||
|
user_datas,
|
||||||
|
):
|
||||||
|
RougailUserDataFile(config,
|
||||||
|
user_datas=user_datas,
|
||||||
|
rougailconfig=rougailconfig,
|
||||||
|
).read()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('run',)
|
56
src/rougail/user_data_file/config.py
Normal file
56
src/rougail/user_data_file/config.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"""
|
||||||
|
Config file for Rougail-user-data
|
||||||
|
|
||||||
|
Silique (https://www.silique.fr)
|
||||||
|
Copyright (C) 2024
|
||||||
|
|
||||||
|
distribued with GPL-2 or later license
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
"""
|
||||||
|
# from .utils import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_rougail_config(*,
|
||||||
|
backward_compatibility=True,
|
||||||
|
) -> dict:
|
||||||
|
options = """
|
||||||
|
file:
|
||||||
|
description: Configuration rougail-user-data-file
|
||||||
|
disabled:
|
||||||
|
type: jinja
|
||||||
|
jinja: |
|
||||||
|
{% if 'file' not in step.user_data %}
|
||||||
|
disabled
|
||||||
|
{% endif %}
|
||||||
|
filename:
|
||||||
|
description: Filename with user data
|
||||||
|
alternative_name: ff
|
||||||
|
type: unix_filename
|
||||||
|
params:
|
||||||
|
allow_relative: True
|
||||||
|
test_existence: True
|
||||||
|
types:
|
||||||
|
- file
|
||||||
|
"""
|
||||||
|
return {'name': 'file',
|
||||||
|
'process': 'user data',
|
||||||
|
'options': options,
|
||||||
|
'level': 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('get_rougail_config')
|
||||||
|
|
Loading…
Reference in a new issue