risotto/src/risotto/services/config/config.py

433 lines
18 KiB
Python

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}