From 847fbfc1e15c1530a314b2fe64df36fa29faa129 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 29 Nov 2019 16:38:33 +0100 Subject: [PATCH] import config-manager controller --- .../config.session.server.configure.yml | 0 .../config.session.server.filter.yml | 0 .../{old => }/config.session.server.get.yml | 0 .../{old => }/config.session.server.list.yml | 0 .../config.session.server.validate.yml | 0 .../config.session.servermodel.configure.yml | 0 .../config.session.servermodel.filter.yml | 0 .../config.session.servermodel.get.yml | 0 .../config.session.servermodel.list.yml | 0 .../config.session.servermodel.start.yml | 0 .../config.session.servermodel.stop.yml | 0 .../config.session.servermodel.validate.yml | 0 .../config.configuration.server.updated.yml | 0 src/risotto/config.py | 4 +- src/risotto/dispatcher.py | 99 ++-- src/risotto/http.py | 1 + src/risotto/message/message.py | 10 +- src/risotto/services/config/config.py | 548 +++++++++++++++++- src/risotto/services/config/storage.py | 151 +++++ 19 files changed, 746 insertions(+), 67 deletions(-) rename messages/v1/messages/{old => }/config.session.server.configure.yml (100%) rename messages/v1/messages/{old => }/config.session.server.filter.yml (100%) rename messages/v1/messages/{old => }/config.session.server.get.yml (100%) rename messages/v1/messages/{old => }/config.session.server.list.yml (100%) rename messages/v1/messages/{old => }/config.session.server.validate.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.configure.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.filter.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.get.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.list.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.start.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.stop.yml (100%) rename messages/v1/messages/{old => }/config.session.servermodel.validate.yml (100%) rename messages/v1/messages/{ => old}/config.configuration.server.updated.yml (100%) create mode 100644 src/risotto/services/config/storage.py diff --git a/messages/v1/messages/old/config.session.server.configure.yml b/messages/v1/messages/config.session.server.configure.yml similarity index 100% rename from messages/v1/messages/old/config.session.server.configure.yml rename to messages/v1/messages/config.session.server.configure.yml diff --git a/messages/v1/messages/old/config.session.server.filter.yml b/messages/v1/messages/config.session.server.filter.yml similarity index 100% rename from messages/v1/messages/old/config.session.server.filter.yml rename to messages/v1/messages/config.session.server.filter.yml diff --git a/messages/v1/messages/old/config.session.server.get.yml b/messages/v1/messages/config.session.server.get.yml similarity index 100% rename from messages/v1/messages/old/config.session.server.get.yml rename to messages/v1/messages/config.session.server.get.yml diff --git a/messages/v1/messages/old/config.session.server.list.yml b/messages/v1/messages/config.session.server.list.yml similarity index 100% rename from messages/v1/messages/old/config.session.server.list.yml rename to messages/v1/messages/config.session.server.list.yml diff --git a/messages/v1/messages/old/config.session.server.validate.yml b/messages/v1/messages/config.session.server.validate.yml similarity index 100% rename from messages/v1/messages/old/config.session.server.validate.yml rename to messages/v1/messages/config.session.server.validate.yml diff --git a/messages/v1/messages/old/config.session.servermodel.configure.yml b/messages/v1/messages/config.session.servermodel.configure.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.configure.yml rename to messages/v1/messages/config.session.servermodel.configure.yml diff --git a/messages/v1/messages/old/config.session.servermodel.filter.yml b/messages/v1/messages/config.session.servermodel.filter.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.filter.yml rename to messages/v1/messages/config.session.servermodel.filter.yml diff --git a/messages/v1/messages/old/config.session.servermodel.get.yml b/messages/v1/messages/config.session.servermodel.get.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.get.yml rename to messages/v1/messages/config.session.servermodel.get.yml diff --git a/messages/v1/messages/old/config.session.servermodel.list.yml b/messages/v1/messages/config.session.servermodel.list.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.list.yml rename to messages/v1/messages/config.session.servermodel.list.yml diff --git a/messages/v1/messages/old/config.session.servermodel.start.yml b/messages/v1/messages/config.session.servermodel.start.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.start.yml rename to messages/v1/messages/config.session.servermodel.start.yml diff --git a/messages/v1/messages/old/config.session.servermodel.stop.yml b/messages/v1/messages/config.session.servermodel.stop.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.stop.yml rename to messages/v1/messages/config.session.servermodel.stop.yml diff --git a/messages/v1/messages/old/config.session.servermodel.validate.yml b/messages/v1/messages/config.session.servermodel.validate.yml similarity index 100% rename from messages/v1/messages/old/config.session.servermodel.validate.yml rename to messages/v1/messages/config.session.servermodel.validate.yml diff --git a/messages/v1/messages/config.configuration.server.updated.yml b/messages/v1/messages/old/config.configuration.server.updated.yml similarity index 100% rename from messages/v1/messages/config.configuration.server.updated.yml rename to messages/v1/messages/old/config.configuration.server.updated.yml diff --git a/src/risotto/config.py b/src/risotto/config.py index 2088b3b..523ac4e 100644 --- a/src/risotto/config.py +++ b/src/risotto/config.py @@ -1,3 +1,5 @@ HTTP_PORT = 8080 MESSAGE_ROOT_PATH = 'messages' -DEBUG = False +ROOT_CACHE_DIR = 'cache' +DEBUG = True +DATABASE_DIR = 'database' diff --git a/src/risotto/dispatcher.py b/src/risotto/dispatcher.py index 15106c6..80006ba 100644 --- a/src/risotto/dispatcher.py +++ b/src/risotto/dispatcher.py @@ -47,7 +47,7 @@ class RegisterDispatcher: def get_function_args(): function_args = self.get_function_args(function) # risotto_context is a special argument, remove it - if function_args[0] == 'risotto_context': + if function_args and function_args[0] == 'risotto_context': function_args = function_args[1:] return set(function_args) @@ -122,7 +122,7 @@ class RegisterDispatcher: if not Config(self.option).option(uri).option.type() == 'message': raise RegistrationError(_(f'{uri} is not a valid message')) except AttributeError: - raise RegistrationError(_(f'{uri} is not a valid message')) + raise RegistrationError(_(f'the message {uri} not exists')) # create an uris' version if needed if version not in self.uris: @@ -139,7 +139,7 @@ class RegisterDispatcher: # True if first argument is the risotto_context function_args = self.get_function_args(function) - if function_args[0] == 'risotto_context': + if function_args and function_args[0] == 'risotto_context': inject_risotto_context = True function_args.pop(0) else: @@ -162,17 +162,18 @@ class RegisterDispatcher: self.uris[version][uri] = dico else: # if event - if notification and notification is not undefined: - raise RegistrationError(_(f'notification not supported yet')) # valid function's arguments self._valid_event_params(version, uri, function, module_name) # register this function if uri not in self.uris[version]: self.uris[version][uri] = [] - self.uris[version][uri].append({'module': module_name, - 'function': function, - 'arguments': function_args, - 'risotto_context': inject_risotto_context}) + dico = {'module': module_name, + 'function': function, + 'arguments': function_args, + 'risotto_context': inject_risotto_context} + if notification and notification is not undefined: + dico['notification'] = notification + self.uris[version][uri].append(dico) def set_module(self, module_name, module): """ register and instanciate a new module @@ -269,42 +270,45 @@ class Dispatcher(RegisterDispatcher): uri:str, context: Context, kwargs: Dict): - if not isinstance(returns, dict): + if isinstance(returns, dict): + returns = [returns] + if not isinstance(returns, list): module_name = function.__module__.split('.')[-2] function_name = function.__name__ - err = _(f'function {module_name}.{function_name} has to return a dict') + err = _(f'function {module_name}.{function_name} has to return a dict or a list') log.error_msg(version, uri, context, kwargs, 'call', err) raise CallError(str(err)) response = self.messages[uri]['response'] if response is None: raise Exception('hu?') else: - config = Config(response, display_name=lambda self, dyn_name: self.impl_getname()) - config.property.read_write() - try: - for key, value in returns.items(): - config.option(key).value.set(value) - except AttributeError: - module_name = function.__module__.split('.')[-2] - function_name = function.__name__ - err = _(f'function {module_name}.{function_name} return the unknown parameter "{key}"') - log.error_msg(version, uri, context, kwargs, 'call', err) - raise CallError(str(err)) - except ValueError: - module_name = function.__module__.split('.')[-2] - function_name = function.__name__ - err = _(f'function {module_name}.{function_name} return the parameter "{key}" with an unvalid value "{value}"') - log.error_msg(version, uri, context, kwargs, 'call', err) - raise CallError(str(err)) - config.property.read_only() - try: - config.value.dict() - except Exception as err: - module_name = function.__module__.split('.')[-2] - function_name = function.__name__ - err = _(f'function {module_name}.{function_name} return an invalid response {err}') - log.error_msg(version, uri, context, kwargs, 'call', err) - raise CallError(str(err)) + for ret in returns: + config = Config(response, display_name=lambda self, dyn_name: self.impl_getname()) + config.property.read_write() + try: + for key, value in ret.items(): + config.option(key).value.set(value) + except AttributeError: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return the unknown parameter "{key}"') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + except ValueError: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return the parameter "{key}" with an unvalid value "{value}"') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + config.property.read_only() + try: + config.value.dict() + except Exception as err: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return an invalid response {err}') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) async def call(self, version, uri, risotto_context, public_only=False, **kwargs): @@ -351,10 +355,15 @@ class Dispatcher(RegisterDispatcher): # notification if obj.get('notification'): notif_version, notif_message = obj['notification'].split('.', 1) - await self.publish(notif_version, - notif_message, - new_context, - **returns) + if not isinstance(returns, list): + send_returns = [returns] + else: + send_returns = returns + for ret in send_returns: + await self.publish(notif_version, + notif_message, + new_context, + **ret) return returns async def publish(self, version, uri, risotto_context, public_only=False, **kwargs): @@ -404,6 +413,14 @@ class Dispatcher(RegisterDispatcher): function_name = function.__name__ log.info_msg(version, uri, new_context, kwargs,'publish', info_msg) + # notification + if obj.get('notification'): + notif_version, notif_message = obj['notification'].split('.', 1) + await self.publish(notif_version, + notif_message, + new_context, + **returns) + dispatcher = Dispatcher() diff --git a/src/risotto/http.py b/src/risotto/http.py index 3eb1740..ce60e98 100644 --- a/src/risotto/http.py +++ b/src/risotto/http.py @@ -45,6 +45,7 @@ async def api(request): if not tiramisu: config = Config(get_messages(load_shortarg=True, only_public=True)[1]) + config.property.read_write() tiramisu = config.option.dict(remotable='none') return Response(text=dumps(tiramisu)) diff --git a/src/risotto/message/message.py b/src/risotto/message/message.py index 6d52f03..944e53c 100644 --- a/src/risotto/message/message.py +++ b/src/risotto/message/message.py @@ -464,15 +464,19 @@ def _parse_responses(message_def, raise Exception('multi response with name {} in {}'.format(name, file_path)) names.append(name) + kwargs = {'name': name, + 'doc': obj.description.strip().rstrip()} + type_ = obj.type + if type_.startswith('[]'): + kwargs['multi'] = True + type_ = type_[2:] option = {'String': StrOption, 'Number': IntOption, 'Boolean': BoolOption, # FIXME - 'File': StrOption}.get(obj.type) + 'File': StrOption}.get(type_) if not option: raise Exception(f'unknown param type {obj.type}') - kwargs = {'name': name, - 'doc': obj.description.strip().rstrip()} if hasattr(obj, 'default'): kwargs['default'] = obj.default else: diff --git a/src/risotto/services/config/config.py b/src/risotto/services/config/config.py index 3e7051c..d8dc476 100644 --- a/src/risotto/services/config/config.py +++ b/src/risotto/services/config/config.py @@ -1,32 +1,536 @@ +#!/usr/bin/env python3 +#import logging +#from lxml.etree import parse +#from io import StringIO +#from autobahn.wamp.exception import ApplicationError +#import asyncio +from tiramisu import Storage, MixConfig, delete_session +#from tiramisu.error import PropertiesOptionError +# +#from os import urandom, unlink +#from os.path import isfile, join +#from binascii import hexlify +#from json import dumps, loads +#from aiohttp.web import HTTPForbidden + +#from creole.loader import PopulateTiramisuObjects +#from zephir.controller import ZephirCommonController, run +#from zephir.http import register as register_http +#from zephir.wamp import register as register_wamp +#from zephir.config import DEBUG +##from eolegenconfig import webapi +#from eolegenconfig.lib import storage +#from eolegenconfig import lib +#from zephir.i18n import _ from ...controller import Controller from ...dispatcher import register +from ...config import ROOT_CACHE_DIR, DATABASE_DIR +from ...context import Context +from .storage import storage_server, storage_servermodel + + class Risotto(Controller): - @register('v1.config.configuration.server.updated') - async def server_created(self, server_id): - print('pouet ' + str(server_id)) + servermodel = {} + # FIXME : should be renamed to probe + server = {} - @register('v1.config.session.server.start', None) - async def get_configuration(self, risotto_context, id): - #stop = await self.call('v1.config.session.server.stop', risotto_context, sessionid=str(id)) - #await self.publish('v1.config.configuration.server.updated', risotto_context, server_id=1, deploy=True) - return {'id': id, - 'sessionid': 'sess', - 'username': risotto_context.username, - 'timestamp': 0, - 'namespace': 'creole', - 'mode': 'basic', - 'debug': False} + def __init__(self, *args, **kwargs): + # add root and statics + # FIXME + #default_storage.setting(engine='sqlite3', dir_database='/srv/database') + self.save_storage = Storage(engine='sqlite3', dir_database=DATABASE_DIR) + self.modify_storage = Storage(engine='dictionary') + super().__init__(*args, **kwargs) - @register('v1.config.session.server.stop', None) - async def get_configuration2(self, sessionid, save): - return {'stop': sessionid} + def valid_user(self, sessionid, risotto_context): + username = risotto_context.username + if username != storage.get_username(sessionid): + raise HTTPForbidden() + + async def onJoin(self, *args, **kwargs): + await super().onJoin(*args, **kwargs) + await asyncio.sleep(1) + await self.load_servermodels() + await self.load_servers() + + async def load_servermodels(self): + print('Load servermodels') + try: + servermodels = await self.call('v1.servermodel.list') + except ApplicationError as err: + print(_('cannot load servermodel list: {}').format(str(err))) + return + for servermodel in servermodels: + try: + await self.load_servermodel(servermodel['servermodelid'], servermodel['servermodelname']) + except ApplicationError as err: + if DEBUG: + print('Error, cannot load servermodel {}: {}'.format(servermodel['servermodelname'], err)) + for servermodel in servermodels: + if 'servermodelparentsid' in servermodel: + for servermodelparentid in servermodel['servermodelparentsid']: + self.servermodel_legacy(servermodel['servermodelname'], servermodel['servermodelid'], servermodelparentid) + + + async def load_servermodel(self, servermodelid, servermodelname): + logging.getLogger().setLevel(logging.INFO) + cache_file = join(ROOT_CACHE_DIR, str(servermodelid)+".xml") + creolefunc_file = join(ROOT_CACHE_DIR, str(servermodelid)+".creolefuncs") + print('Load servermodel {} ({})'.format(servermodelname, servermodelid)) + + if isfile(cache_file): + fileio = open(cache_file) + else: + servermodel = await self.call('v1.servermodel.describe', + servermodelid=servermodelid, + inheritance=False, + resolvdepends=False, + schema=True, + creolefuncs=True) + fileio = StringIO() + fileio.write(servermodel['schema']) + fileio.seek(0) + + with open(cache_file, 'w') as cache: + cache.write(servermodel['schema']) + with open(creolefunc_file, 'w') as cache: + cache.write(servermodel['creolefuncs']) + del servermodel + xmlroot = parse(fileio).getroot() + tiramisu_objects = PopulateTiramisuObjects() + tiramisu_objects.parse_dtd('/srv/src/creole/data/creole.dtd') + tiramisu_objects.make_tiramisu_objects(xmlroot, creolefunc_file) + config = tiramisu_objects.build(persistent=True, + session_id='v_{}'.format(servermodelid), + meta_config=True) + + config.owner.set('v_{}'.format(servermodelname)) + config.information.set('servermodel_id', servermodelid) + config.information.set('servermodel_name', servermodelname) + + self.servermodel[servermodelid] = config + + def servermodel_legacy(self, servermodel_name, servermodel_id, servermodel_parent_id): + if servermodel_parent_id is None: + return + if not self.servermodel.get(servermodel_parent_id): + if DEBUG: + print(f'Servermodel with id {servermodel_parent_id} not loaded, skipping legacy for servermodel {servermodel_name} ({servermodel_id})') + return + servermodel_parent = self.servermodel[servermodel_parent_id] + servermodel_parent_name = servermodel_parent.information.get('servermodel_name') + if DEBUG: + print(f'Create legacy of servermodel {servermodel_name} ({servermodel_id}) with parent {servermodel_parent_name} ({servermodel_parent_id})') + 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: + print(str(err)) + + + async def load_servers(self): + print('Load servers') + try: + risotto_context = Context() + risotto_context.username = 'root' + servers = await self.call('v1.server.list', risotto_context) + except ApplicationError as err: + print(_('cannot load server list: {}').format(str(err))) + return + for server in servers: + try: + self.load_server(server['serverid'], server['servername'], server['servermodelid']) + await self._load_env(server['serverid']) + except Exception as err: + print('Unable to load server {} ({}): {}'.format(server['servername'], server['serverid'], err)) + + def load_server(self, serverid, servername, servermodelid): + if serverid in self.server: + return + print('Load server {} ({})'.format(servername, serverid)) + if not servermodelid in self.servermodel: + raise ValueError(f'unable to find servermodel with id {servermodelid}') + metaconfig = self.servermodel[servermodelid].config.new('p_{}'.format(serverid), + persistent=True, + type='metaconfig') + metaconfig.information.set('server_id', serverid) + metaconfig.information.set('server_name', servername) + metaconfig.owner.set('probe') + config = metaconfig.config.new('s_{}'.format(serverid), + persistent=True) + config.owner.set(servername) + config = metaconfig.config.new('std_{}'.format(serverid), + persistent=True) + config.owner.set(servername) + + if 'disabled' not in config.property.get(): + # has to be read_only + ro = list(config.property.getdefault('read_only', 'append')) + if 'force_store_value' in ro: + # force_store_value is not allowed for new server (wait when configuration is deploy) + ro.remove('force_store_value') + config.property.setdefault(frozenset(ro), 'read_only', 'append') + rw = list(config.property.getdefault('read_write', 'append')) + rw.remove('force_store_value') + config.property.setdefault(frozenset(rw), 'read_write', 'append') + config.property.read_only() + + self.server[serverid] = metaconfig + + async def _load_env(self, server_id): + metaconfig = self.server[server_id] + old_informations = {} + for old_information in metaconfig.information.list(): + old_informations[old_information] = metaconfig.information.get(old_information) + metaconfig.config.reset() + for old_information, old_value in old_informations.items(): + metaconfig.information.set(old_information, old_value) + risotto_context = Context() + risotto_context.username = 'root' + server = await self.call('v1.server.describe', risotto_context=risotto_context, serverid=server_id, environment=True) + for key, value in server['serverenvironment'].items(): + metaconfig.unrestraint.option(key).value.set(value) + if server['serverenvironment']: + metaconfig.unrestraint.option('creole.general.available_probes').value.set("oui") + else: + metaconfig.unrestraint.option('creole.general.available_probes').value.set("non") + +# @register('v1.server.created', None) +# async def server_created(self, serverid, servername, servermodelid): +# self.load_server(serverid, servername, servermodelid) +# +# @register('v1.server.deleted', None) +# async def server_deleted(self, serverid): +# metaconfig = self.server[serverid] +# # remove config inside metaconfig +# for config in metaconfig.config.list(): +# metaconfig.config.pop(config.config.name()) +# delete_session(config.config.name()) +# del config +# # delete config to parents +# for parent in metaconfig.config.parents(): +# parent.config.pop(metaconfig.config.name()) +# # delete metaconfig +# delete_session(metaconfig.config.name()) +# del self.server[serverid] +# del metaconfig + +# @register('v1.server.environment.updated', "v1.config.configuration.server.updated") +# async def env_updated(self, server_id): +# await self._load_env(server_id) +# self.publish('v1.config.configuration.server.updated', server_id=server_id, deploy=False) +# return {'server_id': server_id, 'deploy': True} + +# @register('v1.servermodel.created', None) +# async def servermodel_created(self, servermodels): +# 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', None) +# async def servermodel_updated(self, servermodels): +# for servermodel in servermodels: +# servermodelid = servermodel['servermodelid'] +# servermodelname = servermodel['servermodelname'] +# servermodelparentsid = servermodel.get('servermodelparentsid') +# print('Reload servermodel {} ({})'.format(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) +# # load servers in servermodel +# for subconfig in old_servermodel.config.list(): +# if not isinstance(subconfig, MixConfig): +# name = subconfig.config.name() +# try: +# old_servermodel.config.pop(name) +# except: +# pass +# server_id = subconfig.information.get('server_id') +# server_name = subconfig.information.get('server_name') +# del self.server[server_id] +# self.load_server(server_id, server_name, servermodelid) +# else: +# 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_configuration3(self, server_id, deploy): - return {'get': server_id} + async def get_configuration(self, server_id, deploy): + return {'configuration': (server_id, deploy)} - @register('v1.config.configuration.server.deploy') - async def get_configuration4(self, server_id): - return {'deploy': server_id} + @register('v1.config.configuration.server.deploy', 'v1.config.configuration.server.updated') + async def deploy_configuration(self, server_id): + """Copy values, permissions, permissives from config 'to deploy' to active config + """ + metaconfig = self.server[server_id] + config_std = metaconfig.config("std_{}".format(server_id)) + + 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') + + config = metaconfig.config("s_{}".format(server_id)) + 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} + +# SESSION +#__________________________________________________________________ + def get_session(self, session_id, type): + if type == 'server': + return storage_server.get_session(session_id) + return storage_servermodel.get_session(session_id) + + def get_session_informations(self, session_id, type): + session = self.get_session(session_id, type) + return self.format_session(session_id, session) + + def format_session(self, session_name, session): + return {'sessionid': 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): + ret = [] + if type == 'server': + storage = storage_server + else: + storage = storage_servermodel + for session in storage.list_sessions(): + ret.append(self.format_session(session['sessionid'], session)) + return ret + + def load_dict(self, session): + if not session['option']: + session['option'] = session['config'].option(session['namespace']) + return session['option'].dict(remotable='all') + +# start + async def start_session(self, risotto_context, id, type, server_list): + if id not in server_list: + raise Exception(_(f'cannot find {type} with id {id}')) + + session_id = '' + session_list = self.list_sessions(type) + for sess in session_list: + if sess['id'] == id and sess['username'] == risotto_context.username: + session_id = sess['sessionid'] + session = self.get_session(session_id, type) + return self.format_session(session_id, session) + else: + session_id = '' + + if session_id == '': + 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], type, id, username, self.modify_storage) + return self.get_session_informations(session_id, type) + + @register('v1.config.session.server.start', None) + async def start_session_server(self, risotto_context, id): + return await self.start_session(risotto_context, id, 'server', self.server) + + @register('v1.config.session.servermodel.start', None) + async def start_session_servermodel(self, risotto_context, id): + return await self.start_session(risotto_context, id, 'servermodel', self.servermodel) + +# list + @register('v1.config.session.server.list', None) + async def list_session_server(self): + return self.list_sessions('server') + + @register('v1.config.session.servermodel.list', None) + async def list_session_servermodel(self): + return self.list_sessions('servermodel') + +# filter + async def filter_session(self, session_id, type, namespace, mode, debug): + 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 Exception(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.filter', None) + async def filter_session_server(self, session_id, namespace, mode, debug): + return await self.filter_session(session_id, 'server', namespace, mode, debug) + + @register('v1.config.session.servermodel.filter', None) + async def filter_session_servermodel(self, session_id, namespace, mode, debug): + return await self.filter_session(session_id, 'servermodel', namespace, mode, debug) + +# configure + async def configure_session(self, session_id, type, action, name, index, value): + session = self.get_session(session_id, type) + ret = {'session_id': session_id, + 'name': name} + if index is not None: + ret['index'] = index + 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: + import traceback + traceback.print_exc() + ret['message'] = str(err) + ret['status'] = 'error' + return ret + + @register('v1.config.session.server.configure', None) + async def configure_session_server(self, session_id, action, name, index, value): + return await self.configure_session(session_id, 'server', action, name, index, value) + + @register('v1.config.session.servermodel.configure', None) + async def configure_session_servermodel(self, session_id, action, name, index, value): + return await self.configure_session(session_id, 'servermodel', action, name, index, value) + +# validate + async def validate_session(self, session_id, type): + 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.validate', None) + async def validate_session_server(self, session_id): + return await self.validate_session(session_id, 'server') + + @register('v1.config.session.servermodel.validate', None) + async def validate_session_servermodel(self, session_id): + return await self.validate_session(session_id, 'servermodel') + +# get + async def get_session_(self, session_id, type): + info = self.get_session_informations(session_id, type) + info['content'] = session_id + return info + + @register('v1.config.session.server.get', None) + async def get_session_server(self, session_id): + return await self.get_session_(session_id, 'server') + + @register('v1.config.session.servermodel.get', None) + async def get_session_servermodel(self, session_id): + return await self.get_session_(session_id, 'servermodel') + +# stop + async def stop_session(self, risotto_context, session_id, type, save): + session = self.get_session(session_id, type) + if save: + await self._post_save_config(risotto_context, None, session_id) + if type == 'server': + storage = storage_server + else: + storage = storage_servermodel + storage.del_session(session_id, type) + return self.format_session(session_id, session) + + @register('v1.config.session.server.stop', None) + async def stop_session_server(self, risotto_context, sessionid, save): + return await self.stop_session(sessionid, 'server', save) + + @register('v1.config.session.servermodel.stop', None) + async def stop_session_servermodel(self, risotto_context, sessionid, save): + return await self.stop_session(risotto_context, sessionid, 'servermodel', save) + +# GEN_CONFIG +#__________________________________________________________________ + + async def _post_save_config(self, risotto_context, request, sessionid): + self.valid_user(sessionid, risotto_context) + lib.save_values(sessionid, 'save') + id_ = storage.get_id(sessionid) + if storage.get_type(sessionid) == 'server': + 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: + 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) + return {} diff --git a/src/risotto/services/config/storage.py b/src/risotto/services/config/storage.py new file mode 100644 index 0000000..4026cba --- /dev/null +++ b/src/risotto/services/config/storage.py @@ -0,0 +1,151 @@ +class StorageError(Exception): + pass + + +class Storage(object): + __slots__ = ('sessions',) + + def __init__(self): + self.sessions = {} + + def has_session(self, id_): + return id_ in self.sessions + + def config_exists(self, id_): + return self.sessions[id_]['config_exists'] + + def add_session(self, sessionid, orig_config, server_id, username, storage): + for session in self.sessions.values(): + if session['id'] == server_id: + raise Storage(_(f'{username} already edits this configuration')) + prefix_id = "{}_".format(sessionid) + config_server, orig_config = self.transform_orig_config(orig_config) + config_id = "{}{}".format(prefix_id, config_server) + meta = orig_config.config.deepcopy(session_id=config_id, storage=storage, metaconfig_prefix=prefix_id) + config = meta + while True: + try: + children = list(config.config.list()) + except: + break + if children: + config = children[0] + else: + break + config.property.read_write() + self.set_owner(self, config) + orig_values = config.value.exportation() + config.information.set('orig_values', orig_values) + config_exists = False + for owner in orig_values[3]: + if isinstance(owner, list): + if set(owner) != {'forced'}: + config_exists = True + break + elif owner != 'forced': + config_exists = True + break + self.sessions[sessionid] = {'config': config, + # do not delete meta, so keep it! + 'meta': meta, + 'orig_config': orig_config, + 'id': server_id, + 'timestamp': time.time(), + 'username': username, + 'option': None, + 'namespace': 'creole', + 'config_exists': config_exists} + self.set_config_mode(sessionid, 'normal') + self.set_config_debug(sessionid, False) + + def list_sessions(self): + for sessionid, session in self.sessions.items(): + yield {'sessionid': sessionid, + 'id': session['id'], + 'timestamp': session['timestamp'], + 'username': session['username'], + 'namespace': session['namespace'], + 'mode': session['mode'], + 'debug': session['debug']} + + def del_session(self, id_): + del self.sessions[id_] + + def get_session(self, id_): + if id_ not in self.sessions: + raise GenConfigError('please start a session before') + return self.sessions[id_] + + def save_values(self, id_): + config = self.sessions[id_]['config'] + server_id = self.sessions[id_]['id'] + orig_config = self.sessions[id_]['orig_config'] + values = config.value.exportation() + orig_config.value.importation(values) + orig_config.permissive.importation(config.permissive.exportation()) + # current values become old values in diff_config + config.information.set('orig_values', values) + + def get_username(self, id_): + return self.get_session(id_)['username'] + + def get_id(self, id_): + return self.get_session(id_)['id'] + + def set_config_mode(self, id_, mode): + """ Define which edition mode to select + :param id_: session id + :type id_: `str` + :param mode: possible values = ['basic', 'normal', 'expert'] + :type mode: `str` + :returns: session mode value + :type :`bool` + """ + config = self.get_session(id_)['config'] + for mode_level in modes.values(): + if modes[mode] < mode_level: + config.property.add(mode_level.name) + else: + config.property.pop(mode_level.name) + # store mode in session in case config object gets reloader + self.sessions[id_]['mode'] = mode + + def set_config_debug(self, id_, is_debug): + """ Enable/Disable debug mode + :param id_: session id + :type id_: `str` + :param is_debug: True to enable debug mode + :type is_debug: `bool` + :returns: session debug value + :type :`bool` + """ + config = self.get_session(id_)['config'] + if is_debug: + config.property.pop('hidden') + else: + config.property.add('hidden') + self.sessions[id_]['debug'] = is_debug + return is_debug + + +class StorageServer(Storage): + def transform_orig_config(self, orig_config): + config_server = "std_{}".format(server_id) + orig_config = orig_config.config(config_server) + return config_server, orig_config + + def set_owner(self, config): + config.owner.set(username) + + +class StorageServermodel(Storage): + def transform_orig_config(self, orig_config): + config_server = "v_{}".format(server_id) + return config_server, orig_config + + def set_owner(self, config): + config.owner.set('servermodel_' + username) + + +storage_server = StorageServer() +storage_servermodel = StorageServermodel()