from lxml.etree import parse from io import BytesIO from os.path import isdir, isfile, join from traceback import print_exc from json import dumps from typing import Dict 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 ROOT_CACHE_DIR, DATABASE_DIR, DEBUG, ROUGAIL_DTD_PATH from ...context import Context from ...utils import _ from ...error import CallError, NotAllowedError, RegistrationError from ...logger import log if not isdir(ROOT_CACHE_DIR): raise RegistrationError(_(f'unable to find the cache dir "{ROOT_CACHE_DIR}"')) class Risotto(Controller): servermodel = {} server = {} def __init__(self) -> None: self.save_storage = Storage(engine='sqlite3', dir_database=DATABASE_DIR) super().__init__() 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 """ 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['servermodelid'], servermodel['servermodelname']) except CallError as err: pass # do link to this servermodel for servermodel in servermodels: if 'servermodelparentsid' in servermodel: for servermodelparentid in servermodel['servermodelparentsid']: self.servermodel_legacy(servermodel['servermodelname'], servermodel['servermodelid'], servermodelparentid) def get_funcs_filename(self, servermodelid: int): return join(ROOT_CACHE_DIR, str(servermodelid)+".creolefuncs") async def load_servermodel(self, risotto_context: Context, servermodelid: int, servermodelname: str) -> None: """ Loads a servermodel """ cache_file = join(ROOT_CACHE_DIR, str(servermodelid)+".xml") funcs_file = self.get_funcs_filename(servermodelid) log.info_msg(risotto_context, None, f'Load servermodel {servermodelname} ({servermodelid})') # use file in cache if found, otherwise retrieve it in servermodel context if isfile(cache_file): fileio = open(cache_file) else: servermodel = await self.call('v1.servermodel.describe', risotto_context, servermodelid=servermodelid, inheritance=False, resolvdepends=False, schema=True, creolefuncs=True) fileio = BytesIO() fileio.write(servermodel['schema'].encode()) fileio.seek(0) with open(cache_file, 'w') as cache: cache.write(servermodel['schema']) with open(funcs_file, 'w') as cache: cache.write(servermodel['creolefuncs']) del servermodel # loads tiramisu config and store it xmlroot = parse(fileio).getroot() self.servermodel[servermodelid] = self.build_metaconfig(servermodelid, servermodelname, xmlroot, funcs_file) def build_metaconfig(self, servermodelid: int, servermodelname: str, xmlroot: str, funcs_file: str) -> MetaConfig: """ Build metaconfig for a servermodel """ # build tiramisu's session ID session_id = f'v_{servermodelid}' optiondescription = rougail_load(xmlroot, ROUGAIL_DTD_PATH, funcs_file) # build servermodel metaconfig (v_xxx.m_v_xxx) metaconfig = MetaConfig([], optiondescription=optiondescription, persistent=True, session_id=session_id, storage=self.save_storage) mixconfig = MixConfig(children=[], optiondescription=optiondescription, persistent=True, session_id='m_' + session_id, storage=self.save_storage) metaconfig.config.add(mixconfig) # change default rights ro_origin = metaconfig.property.getdefault('read_only', 'append') ro_append = frozenset(ro_origin - {'force_store_value'}) rw_origin = metaconfig.property.getdefault('read_write', 'append') rw_append = frozenset(rw_origin - {'force_store_value'}) metaconfig.property.setdefault(ro_append, 'read_only', 'append') metaconfig.property.setdefault(rw_append, 'read_write', 'append') metaconfig.property.read_only() metaconfig.permissive.add('basic') metaconfig.permissive.add('normal') metaconfig.permissive.add('expert') # set informtion and owner metaconfig.owner.set('v_{}'.format(servermodelname)) metaconfig.information.set('servermodel_id', servermodelid) metaconfig.information.set('servermodel_name', servermodelname) # return configuration return metaconfig def servermodel_legacy(self, 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): if DEBUG: msg = _(f'Servermodel with id {servermodel_parent_id} not loaded, skipping legacy for servermodel {servermodel_name} ({servermodel_id})') log.error_msg(risotto_context, None, msg) return servermodel_parent = self.servermodel[servermodel_parent_id] servermodel_parent_name = servermodel_parent.information.get('servermodel_name') if DEBUG: msg = _(f'Create legacy of servermodel {servermodel_name} ({servermodel_id}) with parent {servermodel_parent_name} ({servermodel_parent_id})') log.info_msg(risotto_context, None, msg) # do link mix = servermodel_parent.config.get('m_v_' + str(servermodel_parent_id)) try: mix.config.add(self.servermodel[servermodel_id]) except Exception as err: if DEBUG: log.error_msg(risotto_context, None, str(err)) async def load_servers(self, risotto_context: Context) -> None: """ load all available servers """ 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: self.load_server(risotto_context, server['server_id'], server['servername'], server['servermodelid']) except Exception as err: if DEBUG: print_exc() servername = server['servername'] server_id = server['server_id'] msg = _(f'unable to load server {servername} ({server_id}): {err}') log.error_msg(risotto_context, None, msg) def load_server(self, risotto_context: Context, server_id: int, servername: str, servermodelid: int) -> None: """ Loads a server """ if server_id in self.server: return log.info_msg(risotto_context, None, f'Load server {servername} ({server_id})') if not servermodelid in self.servermodel: msg = f'unable to find servermodel with id {servermodelid}' 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[servermodelid] # create server configuration and server 'to deploy' configuration and store it self.server[server_id] = {'server': self.build_config(session_id, server_id, servername, metaconfig), 'server_to_deploy': self.build_config(f'std_{server_id}', server_id, servername, metaconfig), 'funcs_file': self.get_funcs_filename(servermodelid)} def build_config(self, session_id: str, server_id: int, servername: str, metaconfig: MetaConfig) -> None: """ build server's config """ config = metaconfig.config.new(session_id, storage=self.save_storage, persistent=True) config.information.set('server_id', server_id) config.information.set('server_name', servername) config.owner.set(servername) config.property.read_only() return config @register('v1.server.created') async def server_created(self, risotto_context: Context, server_id: int, servername: str, servermodelid: int) -> None: """ Loads server's configuration when a new server is created """ self.load_server(risotto_context, server_id, servername, servermodelid) @register('v1.server.deleted') async def server_deleted(self, server_id: int) -> None: # delete config to it's parents for config in self.server[server_id].values(): for parent in config.config.parents(): parent.config.pop(config.config.name()) delete_session(config.config.name()) # delete metaconfig del self.server[server_id] @register('v1.servermodel.created') async def servermodel_created(self, servermodels) -> None: """ when servermodels are created, load it and do link """ for servermodel in servermodels: await self.load_servermodel(servermodel['servermodelid'], servermodel['servermodelname']) for servermodel in servermodels: if 'servermodelparentsid' in servermodel: for servermodelparentid in servermodel['servermodelparentsid']: self.servermodel_legacy(servermodel['servermodelname'], servermodel['servermodelid'], servermodelparentid) @register('v1.servermodel.updated') async def servermodel_updated(self, risotto_context: Context, servermodels) -> None: for servermodel in servermodels: servermodelid = servermodel['servermodelid'] servermodelname = servermodel['servermodelname'] servermodelparentsid = servermodel.get('servermodelparentsid') log.info_msg(risotto_context, None, f'Reload servermodel {servermodelname} ({servermodelid})') # unlink cache to force download new aggregated file cache_file = join(ROOT_CACHE_DIR, str(servermodelid)+".xml") if isfile(cache_file): unlink(cache_file) # get current servermodel old_servermodel = self.servermodel[servermodelid] # create new one await self.load_servermodel(servermodelid, servermodelname) # migrate all informations self.servermodel[servermodelid].value.importation(old_servermodel.value.exportation()) self.servermodel[servermodelid].permissive.importation(old_servermodel.permissive.exportation()) self.servermodel[servermodelid].property.importation(old_servermodel.property.exportation()) # remove link to legacy if servermodelparentsid: for servermodelparentid in servermodelparentsid: mix = self.servermodel[servermodelparentid].config.get('m_v_' + str(servermodelparentid)) try: mix.config.pop(old_servermodel.config.name()) except: # if mix config is reloaded too pass # add new link self.servermodel_legacy(servermodelname, servermodelid, servermodelparentid) # reload servers or servermodels in servermodel for subconfig in old_servermodel.config.list(): if not isinstance(subconfig, MixConfig): # a server name = subconfig.config.name() if name.startswith('str_'): continue server_id = subconfig.information.get('server_id') server_name = subconfig.information.get('server_name') try: old_servermodel.config.pop(name) old_servermodel.config.pop(f'std_{server_id}') except: pass del self.server[server_id] self.load_server(risotto_context, server_id, server_name, servermodelid) else: # a servermodel for subsubconfig in subconfig.config.list(): name = subsubconfig.config.name() try: subconfig.config.pop(name) except: pass self.servermodel_legacy(subsubconfig.information.get('servermodel_name'), subsubconfig.information.get('servermodel_id'), servermodelid) @register('v1.config.configuration.server.get', None) async def get_configuration(self, server_id: int, deploy: bool) -> bytes: if server_id not in self.server: msg = _(f'cannot find server with id {server_id}') log.error_msg(risotto_context, None, msg) raise CallError(msg) if deploy: server = self.server[server_id]['server'] else: server = self.server[server_id]['server_to_deploy'] server.property.read_only() try: dico = server.value.dict(fullpath=True) except: if deploy: msg = _(f'No configuration available for server {server_id}') else: msg = _(f'No undeployed configuration available for server {server_id}') log.error_msg(risotto_context, None, msg) raise CallError(msg) return dumps(dico).encode() @register('v1.config.configuration.server.deploy', 'v1.config.configuration.server.updated') async def deploy_configuration(self, server_id: int) -> Dict: """Copy values, permissions, permissives from config 'to deploy' to active config """ config = self.server[server_id]['server'] config_std = self.server[server_id]['server_to_deploy'] # when deploy, calculate force_store_value ro = config_std.property.getdefault('read_only', 'append') if 'force_store_value' not in ro: ro = frozenset(list(ro) + ['force_store_value']) config_std.property.setdefault(ro, 'read_only', 'append') rw = config_std.property.getdefault('read_write', 'append') rw = frozenset(list(rw) + ['force_store_value']) config_std.property.setdefault(rw, 'read_write', 'append') config_std.property.add('force_store_value') # copy informations from server 'to deploy' configuration to server configuration config.value.importation(config_std.value.exportation()) config.permissive.importation(config_std.permissive.exportation()) config.property.importation(config_std.property.exportation()) return {'server_id': server_id, 'deploy': True}