rougail/src/rougail/template.py

514 lines
18 KiB
Python

# -*- coding: utf-8 -*-
"""
Gestion du mini-langage de template
On travaille sur les fichiers cibles
"""
import imp
import sys
from shutil import copy
import logging
from typing import Dict, Any
from subprocess import call
from os import listdir, unlink, makedirs
from os.path import dirname, basename, join, split, isfile, isdir
from tempfile import mktemp
from Cheetah import Parser
# l'encoding du template est déterminé par une regexp (encodingDirectiveRE dans Parser.py)
# il cherche un ligne qui ressemble à '#encoding: utf-8
# cette classe simule le module 're' et retourne toujours l'encoding utf-8
# 6224
class FakeEncoding():
def groups(self):
return ('utf-8',)
def search(self, *args):
return self
Parser.encodingDirectiveRE = FakeEncoding()
from Cheetah.Template import Template as ChtTemplate
from Cheetah.NameMapper import NotFound as CheetahNotFound
from tiramisu import Config
from tiramisu.error import PropertiesOptionError
from .config import patch_dir
from .error import FileNotFound, TemplateError, TemplateDisabled
from .i18n import _
from .utils import normalize_family
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
class IsDefined:
"""
filtre permettant de ne pas lever d'exception au cas où
la variable Creole n'est pas définie
"""
def __init__(self, context):
self.context = context
def __call__(self, varname):
if '.' in varname:
splitted_var = varname.split('.')
if len(splitted_var) != 2:
msg = _("Group variables must be of type master.slave")
raise KeyError(msg)
master, slave = splitted_var
if master in self.context:
return slave in self.context[master].slave.keys()
return False
else:
return varname in self.context
class CreoleGet:
def __init__(self, context):
self.context = context
def __call__(self, varname):
return self.context[varname]
def __getitem__(self, varname):
"""For bracket and dotted notation
"""
return self.context[varname]
def __contains__(self, varname):
"""Check variable existence in context
"""
return varname in self.context
@classmethod
def cl_compile(kls, *args, **kwargs):
kwargs['compilerSettings'] = {'directiveStartToken' : '%',
'cheetahVarStartToken' : '%%',
'EOLSlurpToken' : '%',
'PSPStartToken' : 'µ' * 10,
'PSPEndToken' : 'µ' * 10,
'commentStartToken' : 'µ' * 10,
'commentEndToken' : 'µ' * 10,
'multiLineCommentStartToken' : 'µ' * 10,
'multiLineCommentEndToken' : 'µ' * 10}
return kls.old_compile(*args, **kwargs)
ChtTemplate.old_compile = ChtTemplate.compile
ChtTemplate.compile = cl_compile
class CreoleClient:
def __init__(self,
config: Config):
self.config = config
class CheetahTemplate(ChtTemplate):
"""classe pour personnaliser et faciliter la construction
du template Cheetah
"""
def __init__(self,
filename: str,
context,
eosfunc: Dict,
config: Config,
current_container: str,
destfilename,
variable):
"""Initialize Creole CheetahTemplate
"""
extra_context = {'is_defined' : IsDefined(context),
# 'creole_client' : CreoleClient(config),
# 'current_container':CreoleGet(current_container),
'normalize_family': normalize_family,
'rougail_filename': destfilename
}
if variable:
extra_context['rougail_variable'] = variable
ChtTemplate.__init__(self,
file=filename,
searchList=[context, eosfunc, extra_context])
class CreoleLeader:
def __init__(self, value, slave=None, index=None):
"""
On rend la variable itérable pour pouvoir faire:
for ip in iplist:
print ip.network
print ip.netmask
print ip
index is used for CreoleLint
"""
self._value = value
if slave is not None:
self.slave = slave
else:
self.slave = {}
self._index = index
def __getattr__(self, name):
"""Get slave variable or attribute of master value.
If the attribute is a name of a slave variable, return its value.
Otherwise, returns the requested attribute of master value.
"""
if name in self.slave:
value = self.slave[name]
if isinstance(value, PropertiesOptionError):
raise AttributeError()
return value
else:
return getattr(self._value, name)
def __getitem__(self, index):
"""Get a master.slave at requested index.
"""
ret = {}
for key, values in self.slave.items():
ret[key] = values[index]
return CreoleLeader(self._value[index], ret, index)
def __iter__(self):
"""Iterate over master.slave.
Return synchronised value of master.slave.
"""
for i in range(len(self._value)):
ret = {}
for key, values in self.slave.items():
ret[key] = values[i]
yield CreoleLeader(self._value[i], ret, i)
def __len__(self):
"""Delegate to master value
"""
return len(self._value)
def __repr__(self):
"""Show CreoleLeader as dictionary.
The master value is stored under 'value' key.
The slaves are stored under 'slave' key.
"""
return repr({'value': self._value, 'slave': self.slave})
def __eq__(self, value):
return value == self._value
def __ne__(self, value):
return value != self._value
def __lt__(self, value):
return self._value < value
def __le__(self, value):
return self._value <= value
def __gt__(self, value):
return self._value > value
def __ge__(self, value):
return self._value >= value
def __str__(self):
"""Delegate to master value
"""
return str(self._value)
def __add__(self, val):
return self._value.__add__(val)
def __radd__(self, val):
return val + self._value
def __contains__(self, item):
return item in self._value
def add_slave(self, config, name, path):
if isinstance(self._value, list):
values = []
for idx in range(len(self._value)):
try:
values.append(config.option(path, idx).value.get())
except PropertiesOptionError as err:
values.append(err)
else:
raise Exception('hu?')
self.slave[name] = values
class CreoleExtra:
def __init__(self,
suboption: Dict) -> None:
self.suboption = suboption
def __getattr__(self,
key: str) -> Any:
return self.suboption[key]
def __repr__(self):
return self.suboption.__str__()
class CreoleTemplateEngine:
"""Engine to process Creole cheetah template
"""
def __init__(self,
config: Config,
eosfunc_file: str,
distrib_dir: str,
tmp_dir: str,
dest_dir:str) -> None:
self.config = config
self.dest_dir = dest_dir
self.tmp_dir = tmp_dir
self.distrib_dir = distrib_dir
eos = {}
eosfunc = imp.load_source('eosfunc', eosfunc_file)
for func in dir(eosfunc):
if not func.startswith('_'):
eos[func] = getattr(eosfunc, func)
self.eosfunc = eos
self.creole_variables_dict = {}
for option in self.config.option.list(type='all'):
namespace = option.option.name()
if namespace in ['containers', 'actions']:
continue
elif namespace == 'creole':
self.load_eole_variables_creole(self.config,
option)
else:
self.load_eole_variables(self.config,
namespace,
option)
def load_eole_variables_creole(self,
config,
optiondescription):
for option in optiondescription.list('all'):
if option.option.isoptiondescription():
if option.option.isleadership():
for idx, suboption in enumerate(option.list('all')):
if idx == 0:
leader = CreoleLeader(suboption.value.get())
self.creole_variables_dict[suboption.option.name()] = leader
else:
leader.add_slave(config,
suboption.option.name(),
suboption.option.path())
else:
self.load_eole_variables_creole(config,
option)
else:
self.creole_variables_dict[option.option.name()] = option.value.get()
def load_eole_variables(self,
config,
namespace,
optiondescription):
families = {}
for family in optiondescription.list('all'):
variables = {}
for variable in family.list('all'):
if variable.option.isoptiondescription() and variable.option.isleadership():
for idx, suboption in enumerate(variable.list('all')):
if idx == 0:
leader = CreoleLeader(suboption.value.get())
leader_name = suboption.option.name()
else:
leader.add_slave(config,
suboption.option.name(),
suboption.option.path())
variables[leader_name] = leader
else:
variables[variable.option.name()] = variable.value.get()
families[family.option.name()] = CreoleExtra(variables)
self.creole_variables_dict[namespace] = CreoleExtra(families)
def patch_template(self,
filename: str):
"""Apply patch to a template
"""
patch_cmd = ['patch', '-d', self.tmp_dir, '-N', '-p1']
patch_no_debug = ['-s', '-r', '-', '--backup-if-mismatch']
# patches variante + locaux
for directory in [join(patch_dir, 'variante'), patch_dir]:
patch_file = join(directory, f'{filename}.patch')
if isfile(patch_file):
log.info(_("Patching template '{filename}' with '{patch_file}'"))
ret = call(patch_cmd + patch_no_debug + ['-i', patch_file])
if ret:
patch_cmd_err = ' '.join(patch_cmd + ['-i', patch_file])
log.error(_(f"Error applying patch: '{patch_file}'\nTo reproduce and fix this error {patch_cmd_err}"))
copy(filename, self.tmp_dir)
def strip_template_comment(self,
filename: str):
"""Strip comment from template
This apply if filevar has a del_comment attribut
"""
# suppression des commentaires si demandé (attribut del_comment)
if 'del_comment' in filevar and filevar['del_comment'] != '':
strip_cmd = ['sed', '-i']
log.info(_("Cleaning file '{0}'").format( filevar['source'] ))
raise Exception('hu')
#ret, out, err = pyeole.process.system_out(strip_cmd
# + ['/^\s*{0}/d ; /^$/d'.format(filevar['del_comment']),
# filevar['source'] ])
#if ret != 0:
# msg = _("Error removing comments '{0}': {1}")
# raise TemplateError(msg.format(filevar['del_comment'], err))
def prepare_template(self,
filename: str):
"""Prepare template source file
"""
log.info(_("Copy template: '{filename}' -> '{self.tmp_dir}'"))
copy(filename, self.tmp_dir)
self.patch_template(filename)
# self.strip_template_comment(filename)
def process(self,
destfilename: str,
filevar: Dict,
container: str,
variable: Any):
"""Process a cheetah template
"""
# full path of the destination file
log.info(_(f"Cheetah processing: '{destfilename}'"))
try:
cheetah_template = CheetahTemplate(join(self.tmp_dir,
filevar['source']),
self.creole_variables_dict,
self.eosfunc,
self.config.config.copy(),
container,
destfilename,
variable)
data = str(cheetah_template)
except CheetahNotFound as err:
varname = err.args[0][13:-1]
raise TemplateError(_(f"Error: unknown variable used in template {destfilename} : {varname}"))
except Exception as err:
raise TemplateError(_(f"Error while instantiating template {destfilename}: {err}"))
with open(destfilename, 'w') as file_h:
file_h.write(data)
def change_properties(self,
destfilename:str,
filevar: Dict):
#chowncmd = ['chown']
#chownarg = ''
chmodcmd = ['chmod']
chmodarg = ''
#if 'owner' in filevar and filevar['owner']:
# chownarg = filevar['owner']
#else:
# chownarg = 'root'
#if 'group' in filevar and filevar['group']:
# chownarg += ":" + filevar['group']
#else:
# chownarg += ':root'
if 'mode' in filevar and filevar['mode']:
chmodarg = filevar['mode']
else:
chmodarg = '0644'
#chowncmd.extend( [chownarg, destfilename] )
chmodcmd.extend([chmodarg, destfilename])
#log.info(_('Changing properties: {0}').format(' '.join(chowncmd)) )
#ret = call(chowncmd)
#if ret:
# log.error(_('Error changing properties {0}: {1}').format(ret, err) )
log.info(_('Changing properties: {0}').format(' '.join(chmodcmd)) )
ret = call(chmodcmd)
if ret:
chmod_cmd = ' '.join(chmodcmd)
log.error(_(f'Error changing properties: {chmodcmd}'))
def instance_file(self,
filevar: Dict,
container: str):
"""Run templatisation on one file of one container
"""
log.info(_("Instantiating file '{filename}'"))
container_dir = join(self.dest_dir,
container)
filenames = filevar['name']
if 'variable' in filevar:
variable = filevar['variable']
else:
variable = None
if not isinstance(filenames, list):
filenames = [filenames]
if variable:
variable = [variable]
for idx, filename in enumerate(filenames):
destfilename = join(container_dir,
filename[1:])
makedirs(dirname(destfilename), exist_ok=True)
if variable:
var = variable[idx]
else:
var = None
self.process(destfilename,
filevar,
container,
var)
self.change_properties(destfilename,
filevar)
def instance_files(self,
container=None):
"""Run templatisation on all files of all containers
@param container: name of a container
@type container: C{str}
"""
for template in listdir(self.distrib_dir):
self.prepare_template(join(self.distrib_dir, template))
for container_obj in self.config.option('containers').list('all'):
current_container = container_obj.option.doc()
if container is not None and container != current_container:
continue
for fills in container_obj.list('all'):
if fills.option.name() == 'files':
for fill_obj in fills.list('all'):
fill = fill_obj.value.dict()
filename = fill['source']
distib_file = join(self.distrib_dir, filename)
if not isfile(distib_file):
raise FileNotFound(_(f"File {distib_file} does not exist."))
if fill.get('activate', False):
self.instance_file(fill,
current_container)
else:
log.debug(_("Instantiation of file '{filename}' disabled"))
def generate(config: Config,
eosfunc_file: str,
distrib_dir: str,
tmp_dir: str,
dest_dir: str,
container: str=None):
engine = CreoleTemplateEngine(config,
eosfunc_file,
distrib_dir,
tmp_dir,
dest_dir)
engine.instance_files(container=container)