from os import listdir, makedirs from os.path import join, isdir, isfile from shutil import rmtree from traceback import print_exc from typing import Dict from rougail import RougailConvert, RougailConfig, RougailUpgrade try: from tiramisu3 import Storage, Config except: from tiramisu import Storage, Config from .config import get_config from .utils import _, tiramisu_display_name from .logger import log from .dispatcher import get_dispatcher from .context import Context RougailConfig['variable_namespace'] = 'configuration' class Controller: """Common controller used to add a service in Risotto """ def __init__(self, test: bool, ) -> None: self.dispatcher = get_dispatcher() async def call(self, uri: str, risotto_context: Context, *args, **kwargs, ): """ a wrapper to dispatcher's call""" if args: raise ValueError(_(f'the URI "{uri}" can only be called with keyword arguments')) current_uri = risotto_context.paths[-1] current_module = risotto_context.module version, message = uri.split('.', 1) module = message.split('.', 1)[0] if current_module != module: raise ValueError(_(f'cannot call to external module ("{module}") to the URI "{uri}" from "{current_module}"')) return await self.dispatcher.call(version, message, risotto_context, **kwargs, ) async def publish(self, uri: str, risotto_context: Context, *args, **kwargs, ): """ a wrapper to dispatcher's publish""" if args: raise ValueError(_(f'the URI "{uri}" can only be published with keyword arguments')) version, message = uri.split('.', 1) await self.dispatcher.publish(version, message, risotto_context, **kwargs, ) async def check_role(self, uri: str, username: str, **kwargs: dict, ) -> None: # create a new config async with await Config(self.dispatcher.option) as config: await config.property.read_write() await config.option('message').value.set(uri) subconfig = config.option(uri) for key, value in kwargs.items(): try: await subconfig.option(key).value.set(value) except AttributeError: if get_config()['global']['debug']: print_exc() raise ValueError(_(f'unknown parameter in "{uri}": "{key}"')) except ValueOptionError as err: raise ValueError(_(f'invalid parameter in "{uri}": {err}')) await self.dispatcher.check_role(subconfig, username, uri, ) async def on_join(self, risotto_context, ): pass class TiramisuController(Controller): def __init__(self, test: bool, ) -> None: self.source_imported = None if not 'dataset_name' in vars(self): raise Exception(f'please specify "dataset_name" to "{self.__class__.__name__}"') self.tiramisu_cache_root_path = join(get_config()['cache']['root_path'], self.dataset_name) super().__init__(test) self.internal_source_name = get_config()['servermodel']['internal_source'] if not test: db_conf = get_config()['database']['tiramisu_dsn'] self.save_storage = Storage(engine='postgres') self.save_storage.setting(dsn=db_conf) if self.dataset_name != 'servermodel': self.optiondescription = None self.dispatcher.set_function('v1.setting.dataset.updated', None, TiramisuController.dataset_updated, self.__class__.__module__, ) async def on_join(self, risotto_context: Context, ) -> None: if isdir(self.tiramisu_cache_root_path): await self.load_datas(risotto_context) async def dataset_updated(self, risotto_context: Context, ) -> Dict: await self.gen_dictionaries(risotto_context) await self.load_datas(risotto_context) async def gen_dictionaries(self, risotto_context: Context, ) -> None: sources = await self.get_sources(risotto_context) source_imported = sources != [self.internal_source_name] if source_imported and self.source_imported is False: await self.load_datas(risotto_context) self.source_imported = source_imported if not self.source_imported: return self._aggregate_tiramisu_funcs(sources) self._convert_dictionaries_to_tiramisu(sources) async def get_sources(self, risotto_context: Context, ) -> None: return await self.call('v1.setting.source.list', risotto_context, ) def _aggregate_tiramisu_funcs(self, sources: list, ) -> None: dest_file = join(self.tiramisu_cache_root_path, 'funcs.py') if not isdir(self.tiramisu_cache_root_path): makedirs(self.tiramisu_cache_root_path) with open(dest_file, 'wb') as funcs: funcs.write(b"""try: from tiramisu3 import valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal as valid_differ, valid_not_equal, calc_value except: 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 """) for source in sources: root_path = join(source['source_directory'], self.dataset_name, ) if not isdir(root_path): continue for service in listdir(root_path): path = join(root_path, service, 'funcs', ) if not isdir(path): continue for filename in listdir(path): if not filename.endswith('.py'): continue filename_path = join(path, filename) with open(filename_path, 'rb') as fh: funcs.write(f'# {filename_path}\n'.encode()) funcs.write(fh.read()) funcs.write(b'\n') def _convert_dictionaries_to_tiramisu(self, sources: list, ) -> None: funcs_file = join(self.tiramisu_cache_root_path, 'funcs.py') tiramisu_file = join(self.tiramisu_cache_root_path, 'tiramisu.py') dictionaries_dir = join(self.tiramisu_cache_root_path, 'dictionaries') extras_dictionaries_dir = join(self.tiramisu_cache_root_path, 'extra_dictionaries') if isdir(dictionaries_dir): rmtree(dictionaries_dir) makedirs(dictionaries_dir) if isdir(extras_dictionaries_dir): rmtree(extras_dictionaries_dir) makedirs(extras_dictionaries_dir) extras = [] upgrade = RougailUpgrade() for source in sources: root_path = join(source['source_directory'], self.dataset_name, ) if not isdir(root_path): continue for service in listdir(root_path): # upgrade dictionaries path = join(root_path, service, 'dictionaries', ) if not isdir(path): continue upgrade.load_xml_from_folders(path, dictionaries_dir, RougailConfig['variable_namespace'], ) for service in listdir(root_path): # upgrade extra dictionaries path = join(root_path, service, 'extras', ) if not isdir(path): continue for namespace in listdir(path): extra_dir = join(path, namespace, ) if not isdir(extra_dir): continue extra_dictionaries_dir = join(extras_dictionaries_dir, namespace, ) if not isdir(extra_dictionaries_dir): makedirs(extra_dictionaries_dir) extras.append((namespace, [extra_dictionaries_dir])) upgrade.load_xml_from_folders(extra_dir, extra_dictionaries_dir, namespace, ) del upgrade config = RougailConfig.copy() config['functions_file'] = funcs_file config['dictionaries_dir'] = [dictionaries_dir] config['extra_dictionaries'] = {} for extra in extras: config['extra_dictionaries'][extra[0]] = extra[1] eolobj = RougailConvert(rougailconfig=config) eolobj.save(tiramisu_file) async def load(self, risotto_context: Context, name: str, to_deploy: bool=False, ) -> Config: if self.optiondescription is None: # use file in cache tiramisu_file = join(self.tiramisu_cache_root_path, 'tiramisu.py') if not isfile(tiramisu_file): raise Exception(_(f'unable to load the "{self.dataset_name}" configuration, is dataset loaded?')) with open(tiramisu_file) as fileio: tiramisu_locals = {} try: exec(fileio.read(), None, tiramisu_locals) except Exception as err: raise Exception(_(f'unable to load tiramisu file {tiramisu_file}: {err}')) self.optiondescription = tiramisu_locals['option_0'] del tiramisu_locals try: letter = self.dataset_name[0] if not to_deploy: session_id = f'{letter}_{name}' else: session_id = f'{letter}td_{name}' config = await Config(self.optiondescription, session_id=session_id, storage=self.save_storage, display_name=tiramisu_display_name, ) # change default rights await config.property.read_only() await config.permissive.add('basic') await config.permissive.add('normal') await config.permissive.add('expert') # set information and owner await config.owner.set(session_id) await config.information.set(f'{self.dataset_name}_name', name) except Exception as err: if get_config()['global']['debug']: print_exc() msg = _(f'unable to load config for {self.dataset_name} "{name}": {err}') await log.error_msg(risotto_context, None, msg, ) return config async def _deploy_configuration(self, dico: dict, ) -> None: config_std = dico['config_to_deploy'] config = dico['config'] # when deploy, calculate force_store_value ro = await config_std.property.getdefault('read_only', 'append') if 'force_store_value' not in ro: await config_std.property.read_write() if self.dataset_name == 'servermodel': # server_deployed should be hidden await config_std.forcepermissive.option('configuration.general.server_deployed').value.set(True) ro = frozenset(list(ro) + ['force_store_value']) rw = await config_std.property.getdefault('read_write', 'append') rw = frozenset(list(rw) + ['force_store_value']) await config_std.property.setdefault(ro, 'read_only', 'append') await config_std.property.setdefault(rw, 'read_write', 'append') await config_std.property.read_only() # copy informations from 'to deploy' configuration to configuration await config.information.importation(await config_std.information.exportation()) await config.value.importation(await config_std.value.exportation()) await config.permissive.importation(await config_std.permissive.exportation()) await config.property.importation(await config_std.property.exportation()) async def build_configuration(self, config: Config, ) -> dict: configuration = {} for option in await config.option.list('optiondescription'): name = await option.option.name() if name == 'services': continue if name == RougailConfig['variable_namespace']: fullpath = False flatten = True else: fullpath = True flatten = False configuration.update(await option.value.dict(leader_to_list=True, fullpath=fullpath, flatten=flatten)) return configuration