from typing import Any from pytest import fixture, mark from os import makedirs, listdir, link, environ from os.path import isfile, isdir, join, abspath, basename from yaml import load, SafeLoader from json import dump from shutil import rmtree from traceback import print_exc from tiramisu import Config from tiramisu.error import PropertiesOptionError, LeadershipError from rougail import RougailConvert, RougailSystemdTemplate, RougailConfig FUNCTIONS = b"""from tiramisu import valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal as valid_differ, valid_not_equal, calc_value # ============================================================= # fork of risotto-setting/src/risotto_setting/config/config.py def get_password(**kwargs): return 'password' def get_ip(**kwargs): return '1.1.1.1' def get_chain(**kwargs): return 'chain' def get_certificate(**kwargs): return 'certificate' def get_private_key(**kwargs): return 'private_key' def get_linked_configuration(**kwargs): if 'test' in kwargs and kwargs['test']: return kwargs['test'][0] if kwargs.get('multi', False) is True: return ['configuration'] return 'configuration' def get_certificates(**kwargs): return ["XXXX", "YYYY", "ZZZZ"] def zone_information(*args, **kwargs): if args[1] == 'network': val = '192.168.0.0/24' else: val = '192.168.0.1' if kwargs.get('multi', False): val = [val] return val # ============================================================= """ def tiramisu_display_name(kls, dyn_name: 'Base'=None, suffix: str=None, ) -> str: if dyn_name is not None: name = dyn_name else: name = kls.impl_getname() return name GENERATED_DIR = 'generated' if isdir(GENERATED_DIR): rmtree(GENERATED_DIR) applications = {} test_ok = [] for distrib in listdir('seed'): distrib_dir = join('seed', distrib, 'applicationservice') if isdir(distrib_dir): for release in listdir(distrib_dir): release_dir = join(distrib_dir, release) if isdir(release_dir): for applicationservice in listdir(release_dir): applicationservice_dir = join(release_dir, applicationservice) if isdir(applicationservice_dir): if applicationservice in applications: raise Exception('multi applicationservice') applications[applicationservice] = applicationservice_dir if isdir(join(applicationservice_dir, 'dictionaries')): test_ok.append(applicationservice_dir) if 'APPLICATIONSERVICE_DIR' in environ: test_ok = [] if isdir(join(applicationservice_dir, 'dictionaries')): test_ok.append(applicationservice_dir) print(test_ok) @fixture(scope="module", params=test_ok) def test_dir(request): return request.param async def get_value(type: str, properties: list, test_values: Any, multi: bool, has_dependency: bool, path, min_number, ) -> list: if test_values is not None: values = list(test_values) has_dependency = True elif type == 'hostname': values = ['test'] elif type == 'domainname': values = ['test.test.com'] elif type == 'filename': values = ['/a'] elif type == 'ip': values = ['192.168.1.1'] elif type == 'ip_cidr': values = ['192.168.1.1/24'] elif type == 'network': values = ['192.168.1.0'] elif type == 'network_cidr': values = ['192.168.1.0/24'] elif type == 'netmask': values = ['255.255.255.0'] elif type in ['string', 'password']: values = ['string'] elif type == 'integer': if min_number is None: min_number = 0 values = [min_number, min_number + 1] elif type in 'port': values = ["80"] elif type == 'boolean': values = [True, False] elif type == 'url': values = ['http://value'] elif type == 'username': values = ['myname'] elif type == 'email': values = ['a@a.com'] elif type == 'date': values = ['2011-11-11'] elif type == 'float': values = [1.1] else: raise Exception(f'Unknown type {type}') if not has_dependency: values = [values[0]] if multi: values = [values] if 'mandatory' not in properties: if not multi: values.insert(0, None) else: values.insert(0, []) return values async def build_values(paths: list, paths_values: dict, ): if paths: path, option, values, _ = paths[0] for val in values: new_paths_values = paths_values.copy() new_paths_values[path] = val next_paths = paths[1:] if next_paths: async for value in build_values(next_paths, new_paths_values, ): yield value else: yield new_paths_values async def set_dep_paths(new_paths_no_deps, config, old_paths_values, new_paths_values, leader_followers): n = new_paths_no_deps.copy() for path, values in n.items(): dico = await config.value.dict() if path in dico: value = new_paths_no_deps[path][0] await set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps, is_dep=True) async def set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps, is_dep=False): setted = False if path not in old_paths_values or old_paths_values[path] != value: if isinstance(value, tuple): value = list(value) if not await config.option(path).option.isfollower(): if await config.option(path).option.isleader(): old_values = await config.unrestraint.option(path).value.get() if len(old_values) > len(value): await config.unrestraint.option(path).value.reset() leadership_path = path.rsplit('.', 1)[0] if leadership_path in leader_followers: for follower, values in leader_followers[leadership_path]: new_paths_no_deps[follower] = values.copy() await config.unrestraint.option(path).value.set(value) setted = True else: length = await config.unrestraint.option(path).value.len() if length: setted = True await config.unrestraint.option(path, length-1).value.set(value) if setted and is_dep: new_paths_no_deps[path].pop(0) if not new_paths_no_deps[path]: del new_paths_no_deps[path] async def get_values(config, new_paths, new_paths_no_deps, root_dir, leader_followers): old_paths_values = {} if not new_paths: yield await config.value.dict() async for new_paths_values in build_values(new_paths, {}): try: await config.property.read_write() copy_paths_values = new_paths_values.copy() for path, value in new_paths_values.items(): await set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps) if new_paths_no_deps: await set_dep_paths(new_paths_no_deps, config, old_paths_values, new_paths_values, leader_followers) await config.property.read_only() dico = await config.value.dict() old_paths_values = dico yield dico except Exception as err: print_exc() print('Tiramisu file ' + join(root_dir, 'dictionary.py')) print(f'test with {new_paths_values}') raise Exception(f'error {err}') def calc_depends(xmls, appname): application = join(applications[appname], 'applicationservice.yml') with open(application) as yaml: app = load(yaml, Loader=SafeLoader) if 'depends' in app and app['depends']: for xml in app['depends']: if xml not in xmls: xmls.insert(0, xml) calc_depends(xmls, xml) def reorder_new_paths(paths, new_paths=None, not_added_paths=None, added_paths=None): root = new_paths is None if root: new_paths = [] added_paths = [] new_not_added_paths = set() has_old_path = False for path, option, values, option_dependencies in paths: if path not in added_paths: if not_added_paths is not None and not option_dependencies & not_added_paths: #all dependencies are already managed, so added this paths new_paths.append((path, option, values, option_dependencies)) added_paths.append(path) else: has_old_path = True new_not_added_paths.add(path) if has_old_path: reorder_new_paths(paths, new_paths, new_not_added_paths, added_paths) if root: return new_paths async def populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config): for path in await unrestraint.value.dict(): if path in paths_done: continue properties = await config.option(path).property.get(only_raises=True, uncalculated=True, ) if properties: # unmodified path continue for unaccessible_path in unaccessible_paths: if path.startswith(unaccessible_path + '.'): break else: # this variable is not un unaccessible path option = unrestraint.option(path) current_dependencies = await option.option.dependencies() test_values = await option.information.get('test', None) if await option.option.isfollower(): ismulti = await option.option.issubmulti() else: ismulti = await option.option.ismulti() min_number = None type = await option.option.type() if test_values is None: if type == 'choice': test_values = await option.value.list() if not test_values: test_values = [None] elif type == 'integer': real_option = await option.option.get() min_number = real_option.impl_get_extra('min_number') elif type == 'domainname': real_option = await option.option.get() if real_option.impl_get_extra('_dom_type') in ['netbios', 'hostname']: type = 'hostname' elif type == 'ip': true_option = await option.option.get() if true_option.impl_get_extra('_cidr'): type = 'ip_cidr' elif type == 'network': true_option = await option.option.get() if true_option.impl_get_extra('_cidr'): type = 'network_cidr' else: test_values = tuple(test_values) full_properties = await option.property.get() for dependency in current_dependencies: dependencies.add(await dependency.option.path()) paths.append((path, full_properties, test_values, ismulti, type, option, min_number, )) paths_done.append(path) @mark.asyncio async def test_template(test_dir): if test_dir.startswith('seed/eole'): return # if test_dir != 'seed/eole/applicationservice/2020.1.1/eole-lemonldap-ng': # return # if test_dir in [ # 'seed/eole/applicationservice/2020.1.1/eole-proxy', # 'seed/eole/applicationservice/2020.1.1/eole-bareos', # ]: # return if not isdir(GENERATED_DIR): makedirs(GENERATED_DIR) test_name = basename(test_dir) root_dir = f'{GENERATED_DIR}/{test_name}' tmp_dir = f'{root_dir}/tmp' funcs_dir = f'{root_dir}/funcs' for dirname in [root_dir, tmp_dir, funcs_dir]: if isdir(dirname): rmtree(dirname) makedirs(dirname) print() print('>', test_dir) test_file = test_dir.rsplit('/', 1)[-1] xmls = [test_file] calc_depends(xmls, test_file) print(' o dependencies: ' + ', '.join(xmls)) with open(f'{root_dir}/dependencies.txt', 'w') as fh: dump(xmls, fh) dictionaries = [join(applications[xml], 'dictionaries') for xml in xmls if isdir(join(applications[xml], 'dictionaries'))] if 'risotto_setting.rougail' not in RougailConfig['extra_annotators']: RougailConfig['extra_annotators'].append('risotto_setting.rougail') RougailConfig['dictionaries_dir'] = dictionaries print(' o dictionaries: ' + ', '.join(dictionaries)) RougailConfig['extra_dictionaries'] = {} extra_dictionaries = [] for xml in xmls: extras_dir = join(applications[xml], 'extras') if isdir(extras_dir): for extra in listdir(extras_dir): extra_dir = join(extras_dir, extra) if isdir(extra_dir): RougailConfig['extra_dictionaries'].setdefault(extra, []).append(extra_dir) extra_dictionaries.append(extra_dir) if RougailConfig['extra_dictionaries']: print(' o extra dictionaries: ' + ', '.join(extra_dictionaries)) if isdir('templates'): rmtree('templates') makedirs('templates') func = f'{funcs_dir}/funcs.py' with open(func, 'wb') as fh: fh.write(FUNCTIONS) for xml in xmls: func_dir = join(applications[xml], 'funcs') if isdir(func_dir): for f in listdir(func_dir): if not f.startswith('__'): with open(join(func_dir, f), 'rb') as rfh: fh.write(rfh.read()) templates_dir = join(applications[xml], 'templates') if isdir(templates_dir): for f in listdir(templates_dir): template_file = join(templates_dir, f) dest_template_file = join('templates', f) if isfile(dest_template_file): raise Exception(f'duplicate template {f}') link(template_file, dest_template_file) RougailConfig['functions_file'] = func eolobj = RougailConvert() xml = eolobj.save(join(root_dir, 'dictionary.py')) optiondescription = {} try: exec(xml, None, optiondescription) except Exception as err: print('Tiramisu file ' + join(root_dir, 'dictionary.py')) print_exc() raise Exception(f'unknown error when load tiramisu object {err}') from err config = await Config(optiondescription['option_0'], display_name=tiramisu_display_name) has_services = False for option in await config.option.list('optiondescription'): if await option.option.name() == 'services': has_services = True break if not has_services: print(f' o number of tests: any template') rmtree(root_dir) return await config.property.read_write() paths = [] unrestraint = config.unrestraint # this paths in unaccessible, so variable inside are unaccessible too unaccessible_paths = [] dependencies = set() for option in await unrestraint.option.list('optiondescription', recursive=True, ): path = await option.option.path() properties = await config.option(path).property.get(only_raises=True, uncalculated=True, ) if properties: unaccessible_paths.append(path) for dependency in await option.option.dependencies(): dependencies.add(await dependency.option.path()) paths_done = [] await populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config) all_values = {} for path, full_properties, test_values, ismulti, type, option, min_number in paths: values = await get_value(type, full_properties, test_values, ismulti, path in dependencies, path, min_number, ) all_values[path] = values if await option.option.isfollower() or len(values) > 1: continue if await config.unrestraint.option(path).option.isleader(): old_values = await config.unrestraint.option(path).value.get() for idx in reversed(range(len(old_values))): await config.unrestraint.option(path).value.pop(idx) await config.unrestraint.option(path).value.set(values[0]) await populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config) new_paths = [] number_test = 1 nb_var = 0 for path, full_properties, test_values, ismulti, type, option, min_number in paths: if path in all_values: values = all_values[path] has_values = True else: values = await get_value(type, full_properties, test_values, ismulti, path in dependencies, path, min_number, ) has_values = False number_test *= len(values) if await option.option.isfollower() or len(values) > 1: option_dependencies = {await subopt.option.path() for subopt in await option.option.dependencies()} new_paths.append((path, option, values, option_dependencies)) nb_var += 1 elif not has_values: if await config.unrestraint.option(path).option.isleader(): old_values = await config.unrestraint.option(path).value.get() for idx in reversed(range(len(old_values))): await config.unrestraint.option(path).value.pop(idx) await config.unrestraint.option(path).value.set(values[0]) print(' o number of variables: {}'.format(len(paths))) new_paths = reorder_new_paths(new_paths) print(f' o number of tested variables: {nb_var}') print(f' o estimate number of tests: {number_test}') new_paths_no_deps = {} leader_followers = {} if number_test > 1001: number_test = 1 nb2 = 0 print(f' degraded mode...') new_paths_2 = [] for path, option, values, option_dependencies in new_paths: isfollower = await option.option.isfollower() if isfollower: has_dependencies = len(option_dependencies) > 1 else: has_dependencies = len(option_dependencies) > 0 if not has_dependencies: if isfollower: leadership_path = path.rsplit('.', 1)[0] leader_followers.setdefault(leadership_path, []).append((path, values.copy())) new_paths_no_deps[path] = values nb2 += len(values) - 1 else: number_test *= len(values) new_paths_2.append((path, option, values, option_dependencies)) new_paths = new_paths_2 print(' > degraded variables: {}'.format(len(new_paths_no_deps))) print(f' > new estimated number of tests: {number_test + nb2}') idx = 0 real_idx = 0 old_dico = {} async for dico in get_values(config, new_paths, new_paths_no_deps, root_dir, leader_followers): try: real_idx += 1 if str(real_idx).endswith('0'): print(f' ... {real_idx}') build_dir = f'{root_dir}/{idx}' dest_dir = f'{build_dir}/dest' mandatories = await config.value.mandatory() if old_dico == dico: continue for dirname in [build_dir, dest_dir]: makedirs(dirname) old_dico = dico if mandatories: print('Tiramisu file ' + join(root_dir, 'dictionary.py')) raise Exception(f'Mandatory option not configured {mandatories}') RougailConfig['functions_file'] = func RougailConfig['tmp_dir'] = tmp_dir RougailConfig['templates_dir'] = 'templates' RougailConfig['destinations_dir'] = dest_dir await launch(config, root_dir, dico) await config.value.dict() with open(f'{build_dir}/variables.txt', 'w') as fh: dump(await config.value.dict(leader_to_list=True), fh, indent=4, sort_keys=True) idx += 1 except Exception as err: print_exc() print('Tiramisu file ' + join(root_dir, 'dictionary.py')) print(f'test with {dico}') raise Exception(f'unknown error {err}') from err print(f' o number of tests: {idx}') if isdir('templates'): rmtree('templates') rmtree(tmp_dir) rmtree(funcs_dir) async def launch(config, root_dir, dico): try: await config.property.read_only() engine = RougailSystemdTemplate(config) await engine.instance_files() except Exception as err: print_exc() print('Tiramisu file ' + join(root_dir, 'dictionary.py')) print(f'test with {dico}') raise Exception(f'unknown error {err}') from err