rougail/src/rougail/template/base.py

542 lines
21 KiB
Python
Raw Normal View History

2021-01-30 08:15:26 +01:00
"""Template langage for Rougail
Created by:
EOLE (http://eole.orion.education.fr)
Copyright (C) 2005-2018
Forked by:
Cadoles (http://www.cadoles.com)
Copyright (C) 2019-2021
distribued with GPL-2 or later license
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2019-12-02 10:31:55 +01:00
"""
from shutil import copy
import logging
2019-12-22 14:46:16 +01:00
from typing import Dict, Any
2019-12-02 10:31:55 +01:00
from subprocess import call
2022-01-09 18:43:54 +01:00
from os import listdir, makedirs, getcwd, chdir, unlink, rmdir
2021-02-20 10:41:53 +01:00
from os.path import dirname, join, isfile, isdir, abspath
2019-12-02 10:31:55 +01:00
2020-08-07 17:17:42 +02:00
try:
from tiramisu3 import Config, undefined
2020-12-26 15:15:51 +01:00
from tiramisu3.error import PropertiesOptionError # pragma: no cover
2020-12-26 17:06:56 +01:00
except ModuleNotFoundError: # pragma: no cover
from tiramisu import Config, undefined
2020-08-08 11:03:06 +02:00
from tiramisu.error import PropertiesOptionError
2019-12-02 10:31:55 +01:00
2021-02-20 10:41:53 +01:00
from ..config import RougailConfig
from ..error import FileNotFound, TemplateError
from ..i18n import _
from ..utils import load_modules
from . import engine as engines
ENGINES = {}
for engine in engines.__all__:
ENGINES[engine] = getattr(engines, engine)
2019-12-02 10:31:55 +01:00
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
2020-07-08 16:32:10 +02:00
2022-01-26 13:01:40 +01:00
INFORMATIONS = {'files': ['source', 'mode', 'engine', 'included'],
'overrides': ['name', 'source', 'engine'],
}
2022-01-26 13:01:40 +01:00
DEFAULT = {'files': ['owner', 'group'],
'overrides': [],
}
2021-02-14 10:10:48 +01:00
class RougailLeaderIndex:
2021-01-18 17:46:21 +01:00
"""This object is create when access to a specified Index of the variable
"""
2020-12-26 15:15:51 +01:00
def __init__(self,
value,
follower,
index,
) -> None:
2019-12-02 10:31:55 +01:00
self._value = value
2020-12-26 15:15:51 +01:00
self._follower = follower
2019-12-02 10:31:55 +01:00
self._index = index
def __getattr__(self, name):
2020-12-26 15:15:51 +01:00
if name not in self._follower:
raise AttributeError(f'unable to find follower "{name}"')
2020-12-26 15:15:51 +01:00
value = self._follower[name]
if isinstance(value, PropertiesOptionError):
raise AttributeError(f'unable to access to follower "{name}": {value}')
2020-12-26 15:15:51 +01:00
return value
2019-12-02 10:31:55 +01:00
def __getitem__(self, name):
return self.__getattr__(name)
def __contains__(self, name):
if self._follower.__contains__(name):
value = self._follower[name]
return not isinstance(value, PropertiesOptionError)
return False
2021-01-02 21:22:59 +01:00
def __str__(self):
return str(self._value)
2020-12-26 15:15:51 +01:00
def __lt__(self, value):
return self._value.__lt__(value)
2019-12-02 10:31:55 +01:00
2020-12-26 15:15:51 +01:00
def __le__(self, value):
return self._value.__le__(value)
2019-12-02 10:31:55 +01:00
def __eq__(self, value):
2020-12-26 15:15:51 +01:00
return self._value.__eq__(value)
2019-12-02 10:31:55 +01:00
def __ne__(self, value):
2020-12-26 15:15:51 +01:00
return self._value.__ne__(value)
2019-12-02 10:31:55 +01:00
def __gt__(self, value):
2020-12-26 15:15:51 +01:00
return self._value.__gt__(value)
2019-12-02 10:31:55 +01:00
def __ge__(self, value):
return self._value >= value
2020-12-26 15:15:51 +01:00
def __add__(self, value):
return self._value.__add__(value)
def __radd__(self, value):
return value + self._value
2021-02-14 10:10:48 +01:00
class RougailLeader:
2021-01-18 17:46:21 +01:00
"""Implement access to leader and follower variable
For examples: %%leader, %%leader[0].follower1
"""
2020-12-26 15:15:51 +01:00
def __init__(self,
2021-04-12 15:04:15 +02:00
leader_name,
2020-12-26 15:15:51 +01:00
value,
) -> None:
self._value = value
2021-04-12 15:04:15 +02:00
self._follower = {leader_name: value}
2020-12-26 15:15:51 +01:00
def __getitem__(self, index):
"""Get a leader.follower at requested index.
2019-12-02 10:31:55 +01:00
"""
2020-12-26 15:15:51 +01:00
followers = {key: values[index] for key, values in self._follower.items()}
2021-02-14 10:10:48 +01:00
return RougailLeaderIndex(self._value[index],
2020-12-26 15:15:51 +01:00
followers,
index,
)
def __iter__(self):
"""Iterate over leader.follower.
2019-12-02 10:31:55 +01:00
2020-12-26 15:15:51 +01:00
Return synchronised value of leader.follower.
"""
for index in range(len(self._value)):
yield self.__getitem__(index)
2019-12-02 10:31:55 +01:00
2020-12-26 15:15:51 +01:00
def __len__(self):
return len(self._value)
2019-12-02 10:31:55 +01:00
2020-12-26 15:15:51 +01:00
def __contains__(self, value):
return self._value.__contains__(value)
2019-12-02 10:31:55 +01:00
async def _add_follower(self,
config,
name: str,
path: str,
):
2021-01-18 17:46:21 +01:00
"""Add a new follower
"""
2020-12-26 15:15:51 +01:00
self._follower[name] = []
for index in range(len(self._value)):
try:
value = await config.option(path, index).value.get()
except PropertiesOptionError as err:
value = err
self._follower[name].append(value)
2019-12-02 10:31:55 +01:00
2021-02-14 10:10:48 +01:00
class RougailExtra:
2021-01-18 17:46:21 +01:00
"""Object that implement access to extra variable
For example %%extra1.family.variable
"""
2019-12-22 15:35:50 +01:00
def __init__(self,
suboption: Dict,
) -> None:
self._suboption = suboption
2019-12-22 15:35:50 +01:00
def __getattr__(self,
2021-01-02 21:22:59 +01:00
key: str,
) -> Any:
try:
return self._suboption[key]
except KeyError:
2021-03-19 10:31:29 +01:00
raise AttributeError(f'unable to find extra "{key}"')
2019-12-22 15:35:50 +01:00
def __getitem__(self,
key: str,
) -> Any:
return self.__getattr__(key)
def __iter__(self):
return iter(self._suboption.values())
2021-03-19 10:31:29 +01:00
def items(self):
return self._suboption.items()
def __str__(self):
2022-01-26 13:01:40 +01:00
suboptions = {}
for key, value in self._suboption.items():
suboptions[key] = str(value)
return f'<extra object with: {suboptions}>'
2021-03-19 10:31:29 +01:00
2019-12-22 15:35:50 +01:00
class RougailBaseTemplate:
2019-12-02 10:31:55 +01:00
"""Engine to process Creole cheetah template
"""
2021-01-19 19:28:29 +01:00
def __init__(self, # pylint: disable=R0913
2019-12-02 10:31:55 +01:00
config: Config,
2021-02-16 12:08:45 +01:00
rougailconfig: RougailConfig=None,
) -> None:
2021-02-16 12:08:45 +01:00
if rougailconfig is None:
rougailconfig = RougailConfig
2019-12-02 10:31:55 +01:00
self.config = config
2021-02-16 12:08:45 +01:00
self.destinations_dir = abspath(rougailconfig['destinations_dir'])
self.tmp_dir = abspath(rougailconfig['tmp_dir'])
self.templates_dir = []
templates_dir = rougailconfig['templates_dir']
if not isinstance(templates_dir, list):
templates_dir = [templates_dir]
for templ_dir in templates_dir:
self.templates_dir.append(abspath(templ_dir))
2021-02-16 12:08:45 +01:00
self.patches_dir = abspath(rougailconfig['patches_dir'])
2019-12-02 10:31:55 +01:00
eos = {}
2021-02-16 12:08:45 +01:00
functions_file = rougailconfig['functions_file']
if not isinstance(functions_file, list):
functions_file = [functions_file]
for function in functions_file:
if isfile(function):
eosfunc = load_modules(function)
for func in dir(eosfunc):
if not func.startswith('_'):
eos[func] = getattr(eosfunc, func)
2019-12-02 10:31:55 +01:00
self.eosfunc = eos
2020-07-06 20:58:11 +02:00
self.rougail_variables_dict = {}
2021-02-16 12:08:45 +01:00
self.rougailconfig = rougailconfig
self.log = log
self.engines = ENGINES
2020-01-13 19:44:24 +01:00
2019-12-02 10:31:55 +01:00
def patch_template(self,
filename: str,
2021-12-10 23:35:44 +01:00
templates_dir: str,
) -> None:
2019-12-02 10:31:55 +01:00
"""Apply patch to a template
"""
2021-02-14 10:10:48 +01:00
patch_cmd = ['patch', '-d', self.tmp_dir, '-N', '-p1', '-f']
2019-12-02 10:31:55 +01:00
patch_no_debug = ['-s', '-r', '-', '--backup-if-mismatch']
2021-02-14 10:10:48 +01:00
patch_file = join(self.patches_dir, f'{filename}.patch')
if isfile(patch_file):
self.log.info(_("Patching template '{filename}' with '{patch_file}'"))
2021-02-14 10:10:48 +01:00
ret = call(patch_cmd + patch_no_debug + ['-i', patch_file])
2020-12-26 15:15:51 +01:00
if ret: # pragma: no cover
2021-02-14 10:10:48 +01:00
patch_cmd_err = ' '.join(patch_cmd + ['-i', patch_file])
msg = _(f"Error applying patch: '{patch_file}'\n"
2021-01-18 17:46:21 +01:00
f"To reproduce and fix this error {patch_cmd_err}")
self.log.error(_(msg))
2021-12-10 23:35:44 +01:00
copy(join(templates_dir, filename), self.tmp_dir)
2019-12-02 10:31:55 +01:00
def prepare_template(self,
filename: str,
templates_dir: str,
) -> None:
2019-12-02 10:31:55 +01:00
"""Prepare template source file
"""
self.log.info(_("Copy template: '{filename}' -> '{self.tmp_dir}'"))
2021-02-14 10:10:48 +01:00
if not isdir(self.tmp_dir):
raise TemplateError(_(f'cannot find tmp_dir {self.tmp_dir}'))
copy(join(templates_dir, filename), self.tmp_dir)
2021-12-10 23:35:44 +01:00
self.patch_template(filename, templates_dir)
2019-12-02 10:31:55 +01:00
def instance_file(self,
filevar: Dict,
2021-03-19 10:31:29 +01:00
type_: str,
service_name: str,
2022-01-09 18:43:54 +01:00
) -> str:
2020-02-16 21:27:42 +01:00
"""Run templatisation on one file
2019-12-02 10:31:55 +01:00
"""
self.log.info(_("Instantiating file '{filename}'"))
2019-12-22 14:46:16 +01:00
if 'variable' in filevar:
variable = filevar['variable']
else:
variable = None
filenames = filevar.get('name')
if not isinstance(filenames, list):
filenames = [filenames]
2021-02-20 10:41:53 +01:00
if variable and not isinstance(variable, list):
variable = [variable]
2021-02-14 10:10:48 +01:00
if not isdir(self.destinations_dir):
raise TemplateError(_(f'cannot find destinations_dir {self.destinations_dir}'))
2022-01-09 18:43:54 +01:00
destfilenames = []
for idx, filename, in enumerate(filenames):
if variable:
2019-12-22 14:46:16 +01:00
var = variable[idx]
else:
var = None
2021-05-13 22:30:58 +02:00
func = f'get_data_{type_}'
2021-02-26 22:22:38 +01:00
data = getattr(self, func)(filevar,
filename,
service_name,
variable,
idx,
)
if data is None:
continue
2022-01-09 18:43:54 +01:00
filename, source, true_destfilename, var = data
destfilename = join(self.destinations_dir, true_destfilename[1:])
makedirs(dirname(destfilename), exist_ok=True)
self.log.info(_(f"{filevar['engine']} processing: '{destfilename}'"))
self.engines[filevar['engine']].process(filename=filename,
source=source,
2022-01-09 18:43:54 +01:00
true_destfilename=true_destfilename,
destfilename=destfilename,
destdir=self.destinations_dir,
variable=var,
2022-01-09 18:43:54 +01:00
index=idx,
rougail_variables_dict=self.rougail_variables_dict,
eosfunc=self.eosfunc,
)
2022-01-09 18:43:54 +01:00
self.process(true_destfilename,
destfilename,
filevar.get('mode'),
filevar.get('owner'),
filevar.get('group'),
)
destfilenames.append(destfilename)
return destfilenames
2019-12-02 10:31:55 +01:00
2020-02-16 21:27:42 +01:00
async def instance_files(self) -> None:
"""Run templatisation on all files
2019-12-02 10:31:55 +01:00
"""
try:
ori_dir = getcwd()
except FileNotFoundError:
ori_dir = None
chdir(self.tmp_dir)
2020-01-13 19:44:24 +01:00
for option in await self.config.option.list(type='all'):
namespace = await option.option.name()
is_variable_namespace = namespace == self.rougailconfig['variable_namespace']
if namespace == 'services':
is_service_namespace = 'root'
else:
is_service_namespace = False
2021-02-14 10:10:48 +01:00
self.rougail_variables_dict[namespace] = await self.load_variables(option,
is_variable_namespace,
is_service_namespace,
2021-02-14 10:10:48 +01:00
)
for templates_dir in self.templates_dir:
for template in listdir(templates_dir):
self.prepare_template(template,
templates_dir,
)
2022-01-09 18:43:54 +01:00
files_to_delete = []
2021-02-21 16:58:56 +01:00
for included in (True, False):
for service_obj in await self.config.option('services').list('all'):
2021-05-14 07:00:55 +02:00
service_name = await service_obj.option.description()
2021-02-21 19:48:30 +01:00
if await service_obj.option('activate').value.get() is False:
2021-11-26 22:46:34 +01:00
if included is False and not await service_obj.information.get('undisable', False):
self.desactive_service(service_name)
2021-02-21 19:48:30 +01:00
continue
2021-05-13 22:30:58 +02:00
if not included:
engine = await service_obj.information.get('engine', None)
if engine:
self.instance_file({'engine': engine},
'service',
service_name,
)
target_name = await service_obj.information.get('target', None)
if target_name:
self.target_service(service_name,
target_name,
engine is None,
)
2021-02-21 19:48:30 +01:00
for fills in await service_obj.list('optiondescription'):
type_ = await fills.option.name()
for fill_obj in await fills.list('all'):
fill = await fill_obj.value.dict()
2022-01-26 13:01:40 +01:00
self.get_default(type_, fill, fill_obj)
await self.get_informations(type_, fill, fill_obj)
2021-02-21 16:58:56 +01:00
if 'included' in fill:
if (fill['included'] == 'no' and included is True) or \
(fill['included'] != 'no' and included is False):
continue
2021-02-21 16:58:56 +01:00
elif included is True:
continue
if fill['activate']:
2022-01-09 18:43:54 +01:00
destfilenames = self.instance_file(fill,
type_,
service_name,
)
if included and fill.get('included', 'no') == 'content':
files_to_delete.extend(destfilenames)
else:
2022-01-09 18:43:54 +01:00
self.log.debug(_(f"Instantiation of file '{fill['name']}' disabled"))
self.post_instance_service(service_name)
2022-01-09 18:43:54 +01:00
for filename in files_to_delete:
unlink(filename)
parent = filename
while True:
parent = dirname(parent)
if listdir(parent):
break
rmdir(parent)
self.post_instance()
if ori_dir is not None:
chdir(ori_dir)
2019-12-02 10:31:55 +01:00
2022-01-26 13:01:40 +01:00
def get_default(self,
type_: str,
dico: dict,
obj: 'Option',
) -> None:
for key in DEFAULT.get(type_, []):
default_key = f'default_{type_}_{key}'
if default_key in RougailConfig:
default_value = RougailConfig[default_key]
else:
default_value = undefined
dico[key] = dico.get(key, default_value)
async def get_informations(self,
type_: str,
dico: dict,
obj: 'Option',
) -> None:
for key in INFORMATIONS.get(type_, []):
default_key = f'default_{type_}_{key}'
if default_key in RougailConfig:
default_value = RougailConfig[default_key]
else:
default_value = undefined
dico[key] = await obj.information.get(key, default_value)
2021-02-21 19:48:30 +01:00
def desactive_service(self,
2021-05-13 22:30:58 +02:00
*args,
2021-02-21 19:48:30 +01:00
):
raise NotImplementedError(_('cannot desactivate a service'))
2021-05-13 22:30:58 +02:00
def target_service(self,
service_name: str,
*args,
):
raise NotImplementedError(_('cannot use target for the service {service_name}'))
def post_instance_service(self,
*args,
): # pragma: no cover
2021-02-26 22:22:38 +01:00
pass
2022-01-09 18:43:54 +01:00
def process(self,
*args,
): # pragma: no cover
raise NotImplementedError(_('cannot processed'))
2021-02-20 16:07:38 +01:00
def post_instance(self): # pragma: no cover
pass
2021-05-13 22:30:58 +02:00
def get_data_ip(self,
*args,
) -> None: # pragma: no cover
raise NotImplementedError(_('cannot instanciate this service type ip'))
2021-05-13 22:30:58 +02:00
def get_data_files(self,
*args,
) -> None: # pragma: no cover
raise NotImplementedError(_('cannot instanciate this service type file'))
2021-05-13 22:30:58 +02:00
def get_data_service(self,
*args,
) -> None: # pragma: no cover
raise NotImplementedError(_('cannot instanciate this service'))
def get_data_overrides(self,
*args,
) -> None: # pragma: no cover
raise NotImplementedError(_('cannot instanciate this service type override'))
2021-02-14 10:10:48 +01:00
async def load_variables(self,
optiondescription,
is_variable_namespace: str,
is_service_namespace: str,
2021-02-14 10:10:48 +01:00
) -> RougailExtra:
"""Load all variables and set it in RougailExtra objects
2021-01-18 17:46:21 +01:00
"""
2021-02-14 10:10:48 +01:00
variables = {}
2021-01-18 17:46:21 +01:00
for option in await optiondescription.list('all'):
if await option.option.isoptiondescription():
if await option.option.isleadership():
for idx, suboption in enumerate(await option.list('all')):
if idx == 0:
2021-02-14 10:10:48 +01:00
leader_name = await suboption.option.name()
2021-04-12 15:04:15 +02:00
leader = RougailLeader(leader_name, await suboption.value.get())
leadership_name = await option.option.name()
2021-02-14 10:10:48 +01:00
if is_variable_namespace:
self.rougail_variables_dict[await suboption.option.name()] = leader
2021-01-18 17:46:21 +01:00
else:
await leader._add_follower(self.config,
await suboption.option.name(),
await suboption.option.path(),
)
2021-11-23 08:51:03 +01:00
variables[leadership_name] = RougailExtra({leader_name: leader})
2021-01-18 17:46:21 +01:00
else:
if is_service_namespace == 'root':
new_is_service_namespace = 'service_name'
elif is_service_namespace == 'service_name':
new_is_service_namespace = await option.option.name()
elif is_service_namespace in INFORMATIONS:
# remove 's'
new_is_service_namespace = is_service_namespace[:-1]
else:
new_is_service_namespace = is_service_namespace
2021-02-14 10:10:48 +01:00
subfamilies = await self.load_variables(option,
is_variable_namespace,
new_is_service_namespace,
2021-02-14 10:10:48 +01:00
)
variables[await option.option.name()] = subfamilies
2021-01-18 17:46:21 +01:00
else:
2021-02-14 10:10:48 +01:00
if is_variable_namespace:
2021-02-18 17:00:12 +01:00
value = await option.value.get()
self.rougail_variables_dict[await option.option.name()] = value
2021-02-20 10:41:53 +01:00
if await option.option.issymlinkoption() and await option.option.isfollower():
value = []
path = await option.option.path()
for index in range(await option.value.len()):
value.append(await self.config.option(path, index).value.get())
else:
value = await option.value.get()
variables[await option.option.name()] = value
if isinstance(is_service_namespace, str) and is_service_namespace + 's' in INFORMATIONS:
2022-01-26 13:01:40 +01:00
self.get_default(is_service_namespace + 's',
variables,
optiondescription,
)
await self.get_informations(is_service_namespace + 's',
variables,
optiondescription,
)
2021-02-14 10:10:48 +01:00
return RougailExtra(variables)