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

707 lines
30 KiB
Python

from lxml.etree import parse
from io import BytesIO
from os import urandom # , unlink
from os.path import isdir, isfile, join
from binascii import hexlify
from traceback import print_exc
from json import dumps
from typing import Dict, List, Optional, Any
from tiramisu import Storage, list_sessions, delete_session, Config, MetaConfig, MixConfig
from rougail import load as rougail_load
from ...controller import Controller
from ...register import register
from ...http import register as register_http
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
from .storage import storage_server, storage_servermodel
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)
self.modify_storage = Storage(engine='dictionary')
super().__init__()
def valid_user(self,
session_id: str,
risotto_context: Context,
type: str) -> None:
""" check if current user is the session owner
"""
if type == 'server':
storage = storage_server
else:
storage = storage_servermodel
username = risotto_context.username
if username != storage.get_session(session_id)['username']:
raise NotAllowedError()
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}
def get_session(self,
session_id: str,
type: str) -> Dict:
""" Get session information from storage
"""
if type == 'server':
return storage_server.get_session(session_id)
return storage_servermodel.get_session(session_id)
def get_session_informations(self,
session_id: str,
type: str) -> Dict:
""" format session with a session ID name
"""
session = self.get_session(session_id,
type)
return self.format_session(session_id,
session)
def format_session(self,
session_name: str,
session: Dict) -> Dict:
""" format session
"""
return {'session_id': session_name,
'id': session['id'],
'username': session['username'],
'timestamp': session['timestamp'],
'namespace': session['namespace'],
'mode': session['mode'],
'debug': session['debug']}
def list_sessions(self,
type: str) -> List:
ret = []
if type == 'server':
storage = storage_server
else:
storage = storage_servermodel
for session in storage.list_sessions():
ret.append(self.format_session(session['session_id'], session))
return ret
def load_dict(self,
session: Dict) -> Dict:
if not session['option']:
session['option'] = session['config'].option(session['namespace'])
return session['option'].dict(remotable='all')
@register(['v1.config.session.server.start', 'v1.config.session.servermodel.start'], None)
async def start_session(self,
risotto_context: Context,
id: int) -> Dict:
""" start a new config session for a server or a servermodel
"""
type = risotto_context.message.rsplit('.', 2)[-2]
server_list = getattr(self, type)
if id not in server_list:
raise Exception(_(f'cannot find {type} with id {id}'))
# check if a session already exists, in this case returns it
session_list = self.list_sessions(type)
for sess in session_list:
if sess['id'] == id and sess['username'] == risotto_context.username:
session_id = sess['session_id']
session = self.get_session(session_id, type)
return self.format_session(session_id, session)
# create a new session
if type == 'server':
storage = storage_server
else:
storage = storage_servermodel
while True:
session_id = 'z' + hexlify(urandom(23)).decode()
if not storage.has_session(session_id):
break
else:
print('session {} already exists'.format(session_id))
username = risotto_context.username
storage.add_session(session_id,
server_list[id],
id,
username,
self.modify_storage)
return self.get_session_informations(session_id,
type)
@register(['v1.config.session.server.list', 'v1.config.session.servermodel.list'], None)
async def list_session_server(self,
risotto_context: Context):
type = risotto_context.message.rsplit('.', 2)[-2]
return self.list_sessions(type)
@register(['v1.config.session.server.filter', 'v1.config.session.servermodel.filter'], None)
async def filter_session(self,
risotto_context: Context,
session_id: str,
namespace: str,
mode: str,
debug: Optional[bool]):
type = risotto_context.message.rsplit('.', 2)[-2]
session = self.get_session(session_id,
type)
if namespace is not None:
session['option'] = None
session['namespace'] = namespace
if type == 'server':
storage = storage_server
else:
storage = storage_servermodel
if mode is not None:
if mode not in ('basic', 'normal', 'expert'):
raise CallError(f'unknown mode {mode}')
storage.set_config_mode(session_id,
mode)
if debug is not None:
storage.set_config_debug(session_id,
debug)
return self.get_session_informations(session_id,
type)
@register(['v1.config.session.server.configure', 'v1.config.session.servermodel.configure'], None)
async def configure_session(self,
risotto_context: Context,
session_id: str,
action: str,
name: str,
index: int,
value: Any,
value_multi: Optional[List]) -> Dict:
type = risotto_context.message.rsplit('.', 2)[-2]
session = self.get_session(session_id,
type)
ret = {'session_id': session_id,
'name': name}
if index is not None:
ret['index'] = index
option = session['config'].option(name).option
if option.ismulti() and not option.isfollower():
value = value_multi
try:
update = {'name': name,
'action': action,
'value': value}
if index is not None:
update['index'] = index
if not session['option']:
session['option'] = session['config'].option(session['namespace'])
self.load_dict(session)
updates = {'updates': [update]}
session['option'].updates(updates)
ret['status'] = 'ok'
except Exception as err:
if DEBUG:
print_exc()
ret['message'] = str(err)
ret['status'] = 'error'
return ret
@register(['v1.config.session.server.validate', 'v1.config.session.servermodel.validate'], None)
async def validate_session(self,
risotto_context: Context,
session_id: str) -> Dict:
type = risotto_context.message.rsplit('.', 2)[-2]
session = self.get_session(session_id, type)
ret = {}
try:
session['config'].forcepermissive.option(session['namespace']).value.dict()
except Exception as err:
ret['status'] = 'error'
ret['message'] = str(err)
else:
if type == 'server':
mandatories = list(session['config'].forcepermissive.value.mandatory())
if mandatories:
ret['status'] = 'incomplete'
ret['mandatories'] = mandatories
else:
ret['status'] = 'ok'
else:
ret['status'] = 'ok'
return ret
@register(['v1.config.session.server.get', 'v1.config.session.servermodel.get'], None)
async def get_session_(self,
risotto_context: Context,
session_id: str) -> Dict:
type = risotto_context.message.rsplit('.', 2)[-2]
info = self.get_session_informations(session_id,
type)
info['content'] = session_id
session = self.get_session(session_id,
type)
if not session['option']:
session['option'] = session['config'].option(session['namespace'])
info['content'] = dumps(session['option'].value.dict(fullpath=True))
return info
@register(['v1.config.session.server.stop', 'v1.config.session.servermodel.stop'], None)
async def stop_session(self,
risotto_context: Context,
session_id: str,
save: bool) -> Dict:
type = risotto_context.message.rsplit('.', 2)[-2]
self.valid_user(session_id,
risotto_context,
type)
session = self.get_session(session_id,
type)
id_ = session['id']
if type == 'server':
storage = storage_server
if save:
storage.save_values(session_id)
if self.server[id_].option('creole.general.available_probes').value.get() == "oui":
self.publish('v1.config.configuration.server.updated', server_id=id_, deploy=False)
else:
storage = storage_servermodel
if save:
storage.save_values(session_id)
for probe in self.servermodel[id_].config.list():
# FIXME should use config.information.get('server_id')
name = probe.config.name()
if name.startswith('p_'):
server_id = int(name.rsplit('_', 1)[-1])
if self.server[server_id].option('creole.general.available_probes').value.get() == "oui":
self.publish('v1.config.configuration.server.updated', server_id=server_id)
storage.del_session(session_id)
return self.format_session(session_id, session)
@register_http('v1', '/config/server/{session_id}')
async def get_server_api(self,
request,
risotto_context: Context,
session_id: str) -> Dict:
self.valid_user(session_id,
risotto_context,
'server')
session = storage_server.get_session(session_id)
return self.load_dict(session)
@register_http('v1', '/config/servermodel/{session_id}')
async def get_servermodel_api(self,
request,
risotto_context: Context,
session_id: str) -> Dict:
self.valid_user(session_id,
risotto_context,
'servermodel')
session = storage_servermodel.get_session(session_id)
return self.load_dict(session)