432 lines
18 KiB
Python
432 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, RegistrationError
|
|
from ...logger import log
|
|
|
|
|
|
class Risotto(Controller):
|
|
servermodel = {}
|
|
server = {}
|
|
|
|
def __init__(self) -> None:
|
|
for dirname in [ROOT_CACHE_DIR, DATABASE_DIR, ROUGAIL_DTD_PATH]:
|
|
if not isdir(dirname):
|
|
raise RegistrationError(_(f'unable to find the cache dir "{dirname}"'))
|
|
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}
|