from lxml.etree import parse from io import BytesIO from os import unlink from os.path import isdir, isfile, join from traceback import print_exc from typing import Dict, List from tiramisu import Storage, delete_session, MetaConfig, MixConfig from rougail import load as rougail_load from ...controller import Controller from ...register import register from ...config import DATABASE_DIR, ROUGAIL_DTD_PATH, get_config from ...context import Context from ...utils import _ from ...error import CallError, RegistrationError from ...logger import log class Risotto(Controller): def __init__(self, test) -> None: global conf_storage self.cache_root_path = join(get_config().get('cache').get('root_path'), 'servermodel') for dirname in [self.cache_root_path, DATABASE_DIR]: if not isdir(dirname): raise RegistrationError(_(f'unable to find the cache dir "{dirname}"')) if not test: self.save_storage = Storage(engine='sqlite3', dir_database=DATABASE_DIR) self.servermodel = {} self.server = {} super().__init__(test) async def on_join(self, risotto_context: Context) -> None: """ pre-load servermodel and server """ await self.load_servermodels(risotto_context) await self.load_servers(risotto_context) async def load_servermodels(self, risotto_context: Context) -> None: """ load all available servermodels """ await log.info_msg(risotto_context, None, 'Load servermodels') servermodels = await self.call('v1.servermodel.list', risotto_context) # load each servermodels for servermodel in servermodels: try: await self.load_servermodel(risotto_context, servermodel['servermodel_id'], servermodel['servermodel_name']) except CallError as err: pass # do link to this servermodel for servermodel in servermodels: if 'servermodel_parents_id' in servermodel: for servermodelparentid in servermodel['servermodel_parents_id']: await self.servermodel_legacy(risotto_context, servermodel['servermodel_name'], servermodel['servermodel_id'], servermodelparentid) def get_funcs_filename(self, servermodel_id: int): return join(self.cache_root_path, str(servermodel_id), "funcs.py") async def load_servermodel(self, risotto_context: Context, servermodel_id: int, servermodel_name: str) -> None: """ Loads a servermodel """ cache_file = join(self.cache_root_path, str(servermodel_id), "dictionaries.xml") funcs_file = self.get_funcs_filename(servermodel_id) await log.info_msg(risotto_context, None, f'Load servermodel {servermodel_name} ({servermodel_id})') # use file in cache with open(cache_file) as fileio: xmlroot = parse(fileio).getroot() try: self.servermodel[servermodel_id] = await self.build_metaconfig(servermodel_id, servermodel_name, xmlroot, funcs_file) except Exception as err: if get_config().get('global').get('debug'): print_exc() msg = _(f'unable to load {servermodel_name}: {err}') await log.error_msg(risotto_context, None, msg) async def build_metaconfig(self, servermodel_id: int, servermodel_name: str, xmlroot: str, funcs_file: str) -> MetaConfig: """ Build metaconfig for a servermodel """ # build tiramisu's session ID session_id = f'v_{servermodel_id}' optiondescription = rougail_load(xmlroot, ROUGAIL_DTD_PATH, funcs_file) # build servermodel metaconfig (v_xxx.m_v_xxx) metaconfig = await MetaConfig([], optiondescription=optiondescription, persistent=True, session_id=session_id, storage=self.save_storage) mixconfig = await MixConfig(children=[], optiondescription=optiondescription, persistent=True, session_id='m_' + session_id, storage=self.save_storage) await metaconfig.config.add(mixconfig) # change default rights ro_origin = await metaconfig.property.getdefault('read_only', 'append') ro_append = frozenset(ro_origin - {'force_store_value'}) rw_origin = await metaconfig.property.getdefault('read_write', 'append') rw_append = frozenset(rw_origin - {'force_store_value'}) await metaconfig.property.setdefault(ro_append, 'read_only', 'append') await metaconfig.property.setdefault(rw_append, 'read_write', 'append') await metaconfig.property.read_only() await metaconfig.permissive.add('basic') await metaconfig.permissive.add('normal') await metaconfig.permissive.add('expert') # set informtion and owner await metaconfig.owner.set('v_{}'.format(servermodel_name)) await metaconfig.information.set('servermodel_id', servermodel_id) await metaconfig.information.set('servermodel_name', servermodel_name) # return configuration return metaconfig async def servermodel_legacy(self, risotto_context: Context, servermodel_name: str, servermodel_id: int, servermodel_parent_id: int) -> None: """ Make link between parent and children """ if servermodel_parent_id is None: return if not self.servermodel.get(servermodel_parent_id): msg = _(f'Servermodel with id {servermodel_parent_id} not loaded, skipping legacy for servermodel {servermodel_name} ({servermodel_id})') await log.error_msg(risotto_context, None, msg) return servermodel_parent = self.servermodel[servermodel_parent_id] servermodel_parent_name = await servermodel_parent.information.get('servermodel_name') msg = _(f'Create legacy of servermodel {servermodel_name} ({servermodel_id}) with parent {servermodel_parent_name} ({servermodel_parent_id})') await log.info_msg(risotto_context, None, msg) # do link mix = await servermodel_parent.config.get('m_v_' + str(servermodel_parent_id)) try: await mix.config.add(self.servermodel[servermodel_id]) except Exception as err: await log.error_msg(risotto_context, None, str(err)) async def load_servers(self, risotto_context: Context) -> None: """ load all available servers """ await log.info_msg(risotto_context, None, f'Load servers') # get all servers servers = await self.call('v1.server.list', risotto_context) # loads servers for server in servers: try: await self.load_server(risotto_context, server['server_id'], server['server_name'], server['server_servermodel_id']) except Exception as err: if get_config().get('global').get('debug'): print_exc() server_name = server['server_name'] server_id = server['server_id'] msg = _(f'unable to load server {server_name} ({server_id}): {err}') await log.error_msg(risotto_context, None, msg) async def load_server(self, risotto_context: Context, server_id: int, server_name: str, server_servermodel_id: int) -> None: """ Loads a server """ if server_id in self.server: return await log.info_msg(risotto_context, None, f'Load server {server_name} ({server_id})') if not server_servermodel_id in self.servermodel: msg = f'unable to find servermodel with id {server_servermodel_id}' await log.error_msg(risotto_context, None, msg) raise CallError(msg) # check if server was already created session_id = f's_{server_id}' # get the servermodel's metaconfig metaconfig = self.servermodel[server_servermodel_id] # create server configuration and server 'to deploy' configuration and store it self.server[server_id] = {'server': await self.build_config(session_id, server_id, server_name, metaconfig), 'server_to_deploy': await self.build_config(f'std_{server_id}', server_id, server_name, metaconfig), 'funcs_file': self.get_funcs_filename(server_servermodel_id)} async def build_config(self, session_id: str, server_id: int, server_name: str, metaconfig: MetaConfig) -> None: """ build server's config """ config = await metaconfig.config.new(session_id, storage=self.save_storage, persistent=True) await config.information.set('server_id', server_id) await config.information.set('server_name', server_name) await config.owner.set(server_name) await config.property.read_only() return config @register('v1.server.created') async def server_created(self, risotto_context: Context, server_id: int, server_name: str, server_servermodel_id: int) -> None: """ Loads server's configuration when a new server is created """ await self.load_server(risotto_context, server_id, server_name, server_servermodel_id) @register('v1.server.deleted') async def server_deleted(self, server_id: int) -> None: # delete config to it's parents for server_type in ['server', 'server_to_deploy']: config = self.server[server_id]['server'] for parent in await config.config.parents(): await parent.config.pop(await config.config.name()) delete_session(storage=self.save_storage, session_id=await config.config.name()) # delete metaconfig del self.server[server_id] @register('v1.servermodel.created') async def servermodel_created(self, risotto_context: Context, servermodel_id: int, servermodel_name: str, servermodel_parents_id: List[int]) -> None: """ when servermodels are created, load it and do link """ await self.load_and_link_servermodel(risotto_context, servermodel_id, servermodel_name, servermodel_parents_id) async def load_and_link_servermodel(self, risotto_context: Context, servermodel_id: int, servermodel_name: str, servermodel_parents_id: List[int]) -> None: await self.load_servermodel(risotto_context, servermodel_id, servermodel_name) if servermodel_parents_id is not None: for servermodelparentid in servermodel_parents_id: await self.servermodel_legacy(risotto_context, servermodel_name, servermodel_id, servermodelparentid) async def servermodel_delete(self, servermodel_id: int) -> List[MetaConfig]: metaconfig = self.servermodel.pop(servermodel_id) mixconfig = await metaconfig.config.list()[0] children = [] for child in await mixconfig.config.list(): children.append(child) await mixconfig.config.pop(await child.config.name()) await metaconfig.config.pop(await mixconfig.config.name()) delete_session(storage=self.save_storage, session_id=await mixconfig.config.name()) del mixconfig for parent in await metaconfig.config.parents(): await parent.config.pop(await metaconfig.config.name()) delete_session(storage=self.save_storage, session_id=await metaconfig.config.name()) return children # # @register('v1.servermodel.updated') # async def servermodel_updated(self, # risotto_context: Context, # servermodel_id: int, # servermodel_name: str, # servermodel_parents_id: List[int]) -> None: # log.info_msg(risotto_context, # None, # f'Reload servermodel {servermodel_name} ({servermodel_id})') # # unlink cache to force download new aggregated file # cache_file = join(self.cache_root_path, str(servermodel_id)+".xml") # if isfile(cache_file): # unlink(cache_file) # # # store all informations # if servermodel_id in self.servermodel: # old_values = await self.servermodel[servermodel_id].value.exportation() # old_permissives = await self.servermodel[servermodel_id].permissive.exportation() # old_properties = await self.servermodel[servermodel_id].property.exportation() # children = await self.servermodel_delete(servermodel_id) # else: # old_values = None # # # create new one # await self.load_and_link_servermodel(risotto_context, # servermodel_id, # servermodel_name, # servermodel_parents_id) # # # migrates informations # if old_values is not None: # await self.servermodel[servermodel_id].value.importation(old_values) # await self.servermodel[servermodel_id].permissive.importation(old_permissives) # await self.servermodel[servermodel_id].property.importation(old_properties) # for child in children: # await self.servermodel_legacy(risotto_context, # await child.information.get('servermodel_name'), # await child.information.get('servermodel_id'), # servermodel_id) @register('v1.config.configuration.server.get') async def get_configuration(self, risotto_context: Context, server_name: str, deployed: bool) -> bytes: server = await self.call('v1.server.describe', risotto_context, server_name=server_name) server_id = server['server_id'] if server_id not in self.server: msg = _(f'cannot find server with id {server_id}') await log.error_msg(risotto_context, None, msg) raise CallError(msg) if deployed: server = self.server[server_id]['server'] else: server = self.server[server_id]['server_to_deploy'] await server.property.read_only() try: configuration = await server.value.dict(fullpath=True, leader_to_list=True) except: if deployed: msg = _(f'No configuration available for server {server_id}') else: msg = _(f'No undeployed configuration available for server {server_id}') await log.error_msg(risotto_context, None, msg) raise CallError(msg) return {'server_name': server_name, 'deployed': deployed, 'configuration': configuration} @register('v1.config.configuration.server.deploy', 'v1.config.configuration.server.updated') async def deploy_configuration(self, risotto_context: Context, server_name: str) -> Dict: """Copy values, permissions, permissives from config 'to deploy' to active config """ server = await self.call('v1.server.describe', risotto_context, server_name=server_name) server_id = server['server_id'] # FIXME is server_to_deploy working? config = self.server[server_id]['server'] config_std = self.server[server_id]['server_to_deploy'] # when deploy, calculate force_store_value ro = await config_std.property.getdefault('read_only', 'append') if 'force_store_value' not in ro: ro = frozenset(list(ro) + ['force_store_value']) await config_std.property.setdefault(ro, 'read_only', 'append') rw = await config_std.property.getdefault('read_write', 'append') rw = frozenset(list(rw) + ['force_store_value']) await config_std.property.setdefault(rw, 'read_write', 'append') await config_std.property.add('force_store_value') # copy informations from server 'to deploy' configuration to server configuration 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()) return {'server_id': server_id, 'server_name': server_name, 'deployed': True}