risotto/src/risotto/controller.py

333 lines
14 KiB
Python

from os import listdir, makedirs
from os.path import join, isdir, isfile
from shutil import rmtree
from traceback import print_exc
from typing import Dict
from rougail import RougailConvert, RougailConfig, RougailUpgrade
try:
from tiramisu3 import Storage, Config
except:
from tiramisu import Storage, Config
from .config import get_config
from .utils import _, tiramisu_display_name
from .logger import log
from .dispatcher import get_dispatcher
from .context import Context
RougailConfig['variable_namespace'] = 'configuration'
class Controller:
"""Common controller used to add a service in Risotto
"""
def __init__(self,
test: bool,
) -> None:
self.dispatcher = get_dispatcher()
async def call(self,
uri: str,
risotto_context: Context,
*args,
**kwargs,
):
""" a wrapper to dispatcher's call"""
if args:
raise ValueError(_(f'the URI "{uri}" can only be called with keyword arguments'))
current_uri = risotto_context.paths[-1]
current_module = risotto_context.module
version, message = uri.split('.', 1)
module = message.split('.', 1)[0]
if current_module != module:
raise ValueError(_(f'cannot call to external module ("{module}") to the URI "{uri}" from "{current_module}"'))
return await self.dispatcher.call(version,
message,
risotto_context,
**kwargs,
)
async def publish(self,
uri: str,
risotto_context: Context,
*args,
**kwargs,
):
""" a wrapper to dispatcher's publish"""
if args:
raise ValueError(_(f'the URI "{uri}" can only be published with keyword arguments'))
version, message = uri.split('.', 1)
await self.dispatcher.publish(version,
message,
risotto_context,
**kwargs,
)
async def check_role(self,
uri: str,
username: str,
**kwargs: dict,
) -> None:
# create a new config
async with await Config(self.dispatcher.option) as config:
await config.property.read_write()
await config.option('message').value.set(uri)
subconfig = config.option(uri)
for key, value in kwargs.items():
try:
await subconfig.option(key).value.set(value)
except AttributeError:
if get_config()['global']['debug']:
print_exc()
raise ValueError(_(f'unknown parameter in "{uri}": "{key}"'))
except ValueOptionError as err:
raise ValueError(_(f'invalid parameter in "{uri}": {err}'))
await self.dispatcher.check_role(subconfig,
username,
uri,
)
async def on_join(self,
risotto_context,
):
pass
class TiramisuController(Controller):
def __init__(self,
test: bool,
) -> None:
if not 'dataset_name' in vars(self):
raise Exception(f'please specify "dataset_name" to "{self.__class__.__name__}"')
self.tiramisu_cache_root_path = join(get_config()['cache']['root_path'], self.dataset_name)
super().__init__(test)
if not test:
db_conf = get_config()['database']['tiramisu_dsn']
self.save_storage = Storage(engine='postgres')
self.save_storage.setting(dsn=db_conf)
if self.dataset_name != 'servermodel':
self.optiondescription = None
self.dispatcher.set_function('v1.setting.dataset.updated',
None,
TiramisuController.dataset_updated,
self.__class__.__module__,
)
async def on_join(self,
risotto_context: Context,
) -> None:
if isdir(self.tiramisu_cache_root_path):
await self.load_datas(risotto_context)
async def dataset_updated(self,
risotto_context: Context,
) -> Dict:
await self.gen_dictionaries(risotto_context)
await self.load_datas(risotto_context)
async def gen_dictionaries(self,
risotto_context: Context,
) -> None:
sources = await self.get_sources(risotto_context)
self._aggregate_tiramisu_funcs(sources)
self._convert_dictionaries_to_tiramisu(sources)
async def get_sources(self,
risotto_context: Context,
) -> None:
return await self.call('v1.setting.source.list',
risotto_context,
)
def _aggregate_tiramisu_funcs(self,
sources: list,
) -> None:
dest_file = join(self.tiramisu_cache_root_path, 'funcs.py')
if not isdir(self.tiramisu_cache_root_path):
makedirs(self.tiramisu_cache_root_path)
with open(dest_file, 'wb') as funcs:
funcs.write(b"""try:
from tiramisu3 import valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal as valid_differ, valid_not_equal, calc_value
except:
from tiramisu import valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal as valid_differ, valid_not_equal, calc_value
""")
for source in sources:
root_path = join(source['source_directory'],
self.dataset_name,
)
if not isdir(root_path):
continue
for service in listdir(root_path):
path = join(root_path,
service,
'funcs',
)
if not isdir(path):
continue
for filename in listdir(path):
if not filename.endswith('.py'):
continue
filename_path = join(path, filename)
with open(filename_path, 'rb') as fh:
funcs.write(f'# {filename_path}\n'.encode())
funcs.write(fh.read())
funcs.write(b'\n')
def _convert_dictionaries_to_tiramisu(self, sources: list) -> None:
funcs_file = join(self.tiramisu_cache_root_path, 'funcs.py')
tiramisu_file = join(self.tiramisu_cache_root_path, 'tiramisu.py')
dictionaries_dir = join(self.tiramisu_cache_root_path, 'dictionaries')
extras_dictionaries_dir = join(self.tiramisu_cache_root_path, 'extra_dictionaries')
if isdir(dictionaries_dir):
rmtree(dictionaries_dir)
makedirs(dictionaries_dir)
if isdir(extras_dictionaries_dir):
rmtree(extras_dictionaries_dir)
makedirs(extras_dictionaries_dir)
extras = []
upgrade = RougailUpgrade()
for source in sources:
root_path = join(source['source_directory'],
self.dataset_name,
)
if not isdir(root_path):
continue
for service in listdir(root_path):
# upgrade dictionaries
path = join(root_path,
service,
'dictionaries',
)
if not isdir(path):
continue
upgrade.load_xml_from_folders(path,
dictionaries_dir,
RougailConfig['variable_namespace'],
)
for service in listdir(root_path):
# upgrade extra dictionaries
path = join(root_path,
service,
'extras',
)
if not isdir(path):
continue
for namespace in listdir(path):
extra_dir = join(path,
namespace,
)
if not isdir(extra_dir):
continue
extra_dictionaries_dir = join(extras_dictionaries_dir,
namespace,
)
if not isdir(extra_dictionaries_dir):
makedirs(extra_dictionaries_dir)
extras.append((namespace, [extra_dictionaries_dir]))
upgrade.load_xml_from_folders(extra_dir,
extra_dictionaries_dir,
namespace,
)
del upgrade
config = RougailConfig.copy()
config['functions_file'] = funcs_file
config['dictionaries_dir'] = [dictionaries_dir]
config['extra_dictionaries'] = {}
for extra in extras:
config['extra_dictionaries'][extra[0]] = extra[1]
eolobj = RougailConvert(rougailconfig=config)
eolobj.save(tiramisu_file)
async def load(self,
risotto_context: Context,
name: str,
to_deploy: bool=False,
) -> Config:
if self.optiondescription is None:
# use file in cache
tiramisu_file = join(self.tiramisu_cache_root_path, 'tiramisu.py')
if not isfile(tiramisu_file):
raise Exception(_(f'unable to load the "{self.dataset_name}" configuration, is dataset loaded?'))
with open(tiramisu_file) as fileio:
tiramisu_locals = {}
try:
exec(fileio.read(), None, tiramisu_locals)
except Exception as err:
raise Exception(_(f'unable to load tiramisu file {tiramisu_file}: {err}'))
self.optiondescription = tiramisu_locals['option_0']
del tiramisu_locals
try:
letter = self.dataset_name[0]
if not to_deploy:
session_id = f'{letter}_{name}'
else:
session_id = f'{letter}td_{name}'
config = await Config(self.optiondescription,
session_id=session_id,
storage=self.save_storage,
display_name=tiramisu_display_name,
)
# change default rights
await config.property.read_only()
await config.permissive.add('basic')
await config.permissive.add('normal')
await config.permissive.add('expert')
# set information and owner
await config.owner.set(session_id)
await config.information.set(f'{self.dataset_name}_name', name)
except Exception as err:
if get_config()['global']['debug']:
print_exc()
msg = _(f'unable to load config for {self.dataset_name} "{name}": {err}')
await log.error_msg(risotto_context,
None,
msg,
)
return config
async def _deploy_configuration(self,
dico: dict,
) -> None:
config_std = dico['config_to_deploy']
config = dico['config']
# when deploy, calculate force_store_value
ro = await config_std.property.getdefault('read_only', 'append')
if 'force_store_value' not in ro:
await config_std.property.read_write()
if self.dataset_name == 'servermodel':
# server_deployed should be hidden
await config_std.forcepermissive.option('configuration.general.server_deployed').value.set(True)
ro = frozenset(list(ro) + ['force_store_value'])
rw = await config_std.property.getdefault('read_write', 'append')
rw = frozenset(list(rw) + ['force_store_value'])
await config_std.property.setdefault(ro, 'read_only', 'append')
await config_std.property.setdefault(rw, 'read_write', 'append')
await config_std.property.read_only()
# copy informations from 'to deploy' configuration to configuration
await config.information.importation(await config_std.information.exportation())
await config.value.importation(await config_std.value.exportation())
await config.permissive.importation(await config_std.permissive.exportation())
await config.property.importation(await config_std.property.exportation())
async def build_configuration(self,
config: Config,
) -> dict:
configuration = {}
for option in await config.option.list('optiondescription'):
name = await option.option.name()
if name == 'services':
continue
if name == RougailConfig['variable_namespace']:
fullpath = False
flatten = True
else:
fullpath = True
flatten = False
configuration.update(await option.value.dict(leader_to_list=True, fullpath=fullpath, flatten=flatten))
return configuration