rougail/creole/template.py

608 lines
22 KiB
Python

# -*- coding: utf-8 -*-
"""
Gestion du mini-langage de template
On travaille sur les fichiers cibles
"""
import sys
import shutil
import logging
import traceback
import os
from os import listdir, unlink
from os.path import basename, join
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
import config as cfg
from .client import CreoleClient, CreoleClientError
from .error import FileNotFound, TemplateError, TemplateDisabled
import eosfunc
from .i18n import _
import pyeole
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
class IsDefined(object):
"""
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 = _(u"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(object):
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' : u'%',
'cheetahVarStartToken' : u'%%',
'EOLSlurpToken' : u'%',
'PSPStartToken' : u'µ' * 10,
'PSPEndToken' : u'µ' * 10,
'commentStartToken' : u'µ' * 10,
'commentEndToken' : u'µ' * 10,
'multiLineCommentStartToken' : u'µ' * 10,
'multiLineCommentEndToken' : u'µ' * 10}
return kls.old_compile(*args, **kwargs)
ChtTemplate.old_compile = ChtTemplate.compile
ChtTemplate.compile = cl_compile
class CheetahTemplate(ChtTemplate):
"""classe pour personnaliser et faciliter la construction
du template Cheetah
"""
def __init__(self, filename, context, current_container):
"""Initialize Creole CheetahTemplate
@param filename: name of the file to process
@type filename: C{str}
@param context: flat dictionary of creole variables as 'name':'value',
@type context: C{dict}
@param current_container: flat dictionary describing the current container
@type current_container: C{dict}
"""
eos = {}
for func in dir(eosfunc):
if not func.startswith('_'):
eos[func] = getattr(eosfunc, func)
# ajout des variables decrivant les conteneurs
#FIXME chercher les infos dans le client !
ChtTemplate.__init__(self, file=filename,
searchList=[context, eos, {u'is_defined' : IsDefined(context),
u'creole_client' : CreoleClient(),
u'current_container':CreoleGet(current_container),
}])
class CreoleMaster(object):
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, Exception):
raise value
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 CreoleMaster(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 CreoleMaster(self._value[i], ret, i)
def __len__(self):
"""Delegate to master value
"""
return len(self._value)
def __repr__(self):
"""Show CreoleMaster as dictionary.
The master value is stored under 'value' key.
The slaves are stored under 'slave' key.
"""
return repr({u'value': self._value, u'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, name, value):
"""Add a slave variable
Minimal check on type and value of the slave in regards to the
master one.
@param name: name of the slave variable
@type name: C{str}
@param value: value of the slave variable
"""
if isinstance(self._value, list):
if not isinstance(value, list):
raise TypeError
elif len(value) != len(self._value):
raise ValueError(_(u'length mismatch'))
new_value = []
for val in value:
if isinstance(val, dict):
new_value.append(ValueError(val['err']))
else:
new_value.append(val)
value = new_value
elif isinstance(value, list):
raise TypeError
self.slave[name] = value
class CreoleTemplateEngine(object):
"""Engine to process Creole cheetah template
"""
def __init__(self, force_values=None):
#force_values permit inject value and not used CreoleClient (used by CreoleLint)
self.client = CreoleClient()
self.creole_variables_dict = {}
self.force_values = force_values
self.load_eole_variables()
def load_eole_variables(self):
# remplacement des variables EOLE
self.creole_variables_dict = {}
if self.force_values is not None:
values = self.force_values
else:
values = self.client.get_creole()
for varname, value in values.items():
if varname in self.creole_variables_dict:
# Creation of a slave create the master
continue
if varname.find('.') != -1:
#support des groupes
mastername, slavename = varname.split('.')
if not mastername in self.creole_variables_dict or not \
isinstance(self.creole_variables_dict [mastername],
CreoleMaster):
# Create the master variable
if mastername in values:
self.creole_variables_dict[mastername] = CreoleMaster(values[mastername])
else:
#only for CreoleLint
self.creole_variables_dict[mastername] = CreoleMaster(value)
#test only for CreoleLint
if mastername != slavename:
self.creole_variables_dict[mastername].add_slave(slavename, value)
else:
self.creole_variables_dict[varname] = value
def patch_template(self, filevar, force_no_active=False):
"""Apply patch to a template
"""
var_dir = os.path.join(cfg.patch_dir,'variante')
patch_cmd = ['patch', '-d', cfg.templatedir, '-N', '-p1']
patch_no_debug = ['-s', '-r', '-', '--backup-if-mismatch']
tmpl_filename = os.path.split(filevar[u'source'])[1]
# patches variante + locaux
for directory in [var_dir, cfg.patch_dir]:
patch_file = os.path.join(directory, '{0}.patch'.format(tmpl_filename))
if os.access(patch_file, os.F_OK):
msg = _(u"Patching template '{0}' with '{1}'")
log.info(msg.format(filevar[u'source'], patch_file))
ret, out, err = pyeole.process.system_out(patch_cmd + patch_no_debug + ['-i', patch_file])
if ret != 0:
msg = _(u"Error applying patch: '{0}'\nTo reproduce and fix this error {1}")
log.error(msg.format(patch_file, ' '.join(patch_cmd + ['-i', patch_file])))
#8307 : recopie le template original et n'arrête pas le processus
self._copy_to_template_dir(filevar, force_no_active)
#raise TemplateError(msg.format(patch_file, err))
def strip_template_comment(self, filevar):
"""Strip comment from template
This apply if filevar has a del_comment attribut
"""
# suppression des commentaires si demandé (attribut del_comment)
strip_cmd = ['sed', '-i']
if u'del_comment' in filevar and filevar[u'del_comment'] != '':
log.info(_(u"Cleaning file '{0}'").format( filevar[u'source'] ))
ret, out, err = pyeole.process.system_out(strip_cmd
+ ['/^\s*{0}/d ; /^$/d'.format(filevar[u'del_comment']),
filevar[u'source'] ])
if ret != 0:
msg = _(u"Error removing comments '{0}': {1}")
raise TemplateError(msg.format(filevar[u'del_comment'], err))
def _check_filevar(self, filevar, force_no_active=False):
"""Verify that filevar is processable
:param filevar: template file informations
:type filevar: `dict`
:raise CreoleClientError: if :data:`filevar` is disabled
inexistant or unknown.
"""
if not force_no_active and (u'activate' not in filevar or not filevar[u'activate']):
raise CreoleClientError(_(u"Template file not enabled:"
u" {0}").format(basename(filevar[u'source'])))
if u'source' not in filevar or filevar[u'source'] is None:
raise CreoleClientError(_(u"Template file not set:"
u" {0}").format(basename(filevar['source'])))
if u'name' not in filevar or filevar[u'name'] is None:
raise CreoleClientError(_(u"Template target not set:"
u" {0}").format(basename(filevar[u'source'])))
def _copy_to_template_dir(self, filevar, force_no_active=False):
"""Copy template to processing temporary directory.
:param filevar: template file informations
:type filevar: `dict`
:param force_no_active: copy disabled template if `True`
:type filevar: `bool`
:raise FileNotFound: if source template does not exist
"""
self._check_filevar(filevar, force_no_active)
tmpl_source_name = os.path.split(filevar[u'source'])[1]
tmpl_source_file = os.path.join(cfg.distrib_dir, tmpl_source_name)
if not os.path.isfile(tmpl_source_file):
msg = _(u"Template {0} unexistent").format(tmpl_source_file)
raise FileNotFound(msg)
else:
log.info(_(u"Copy template: '{0}' -> '{1}'").format(tmpl_source_file, cfg.templatedir))
shutil.copy(tmpl_source_file, cfg.templatedir)
def prepare_template(self, filevar, force_no_active=False):
"""Prepare template source file
"""
self._copy_to_template_dir(filevar, force_no_active)
self.patch_template(filevar, force_no_active)
self.strip_template_comment(filevar)
def verify(self, filevar):
"""
verifie que les fichiers existent
@param mkdir : création du répertoire si nécessaire
"""
if not os.path.isfile(filevar[u'source']):
raise FileNotFound(_(u"File {0} does not exist.").format(filevar[u'source']))
destfilename = filevar[u'full_name']
dir_target = os.path.dirname(destfilename)
if dir_target != '' and not os.path.isdir(dir_target):
if not filevar[u'mkdir']:
raise FileNotFound(_(u"Folder {0} does not exist but is required by {1}").format(dir_target, destfilename))
os.makedirs(dir_target)
# FIXME: pose plus de problème qu'autre chose (cf. #3048)
#if not isfile(target):
# system('cp %s %s' % (source, target))
def process(self, filevar, container):
"""Process a cheetah template
Process a cheetah template and copy the file to destination.
@param filevar: dictionary describing the file to process
@type filevar: C{dict}
@param container: dictionary describing the container
@type container: C{dict}
"""
UTF = "#encoding: utf-8"
self._check_filevar(filevar)
# full path of the destination file
destfilename = filevar[u'full_name']
log.info(_(u"Cheetah processing: '{0}' -> '{1}'").format(filevar[u'source'],
destfilename))
# utilisation d'un fichier temporaire
# afin de ne pas modifier l'original
tmpfile = mktemp()
shutil.copy(filevar[u'source'], tmpfile)
# ajout de l'en-tête pour le support de l'UTF-8
# FIXME: autres encodages ?
#os.system("sed -i '1i{0}' {1}".format(UTF, tmpfile)) (supprimé depuis #6224)
try:
cheetah_template = CheetahTemplate(tmpfile, self.creole_variables_dict, container)
os.unlink(tmpfile)
# suppression de l'en-tête UTF-8 ajouté !!! (supprimé depuis #6224)
data = str(cheetah_template) # .replace("{0}\n".format(UTF), '', 1)
except CheetahNotFound, err:
varname = err.args[0][13:-1]
msg = _(u"Error: unknown variable used in template {0} : {1}").format(filevar[u'name'], varname)
raise TemplateError, msg
except UnicodeDecodeError, err:
msg = _(u"Encoding issue detected in template {0}").format(filevar[u'name'])
raise TemplateError, msg
except Exception, err:
msg = _(u"Error while instantiating template {0}: {1}").format(filevar[u'name'], err)
raise TemplateError, msg
# écriture du fichier cible
if destfilename == '':
# CreoleCat may need to write on stdout (#10065)
sys.stdout.write(data)
else:
try:
file_h = file(destfilename, 'w')
file_h.write(data)
file_h.close()
except IOError, e:
msg = _(u"Unable to write in file '{0}': '{1}'").format(destfilename, e)
raise FileNotFound, msg
def change_properties(self, filevar, container=None, force_full_name=False):
chowncmd = [u'chown']
chownarg = ''
chmodcmd = [u'chmod']
chmodarg = ''
if not force_full_name:
destfilename = filevar[u'name']
else:
destfilename = filevar[u'full_name']
if u'owner' in filevar and filevar[u'owner']:
chownarg = filevar[u'owner']
else:
chownarg = u'root'
if u'group' in filevar and filevar[u'group']:
chownarg += ":" + filevar[u'group']
else:
chownarg += u':root'
if u'mode' in filevar and filevar[u'mode']:
chmodarg = filevar[u'mode']
else:
chmodarg = u'0644'
chowncmd.extend( [chownarg, destfilename] )
chmodcmd.extend( [chmodarg, destfilename] )
log.info(_(u'Changing properties: {0}').format(' '.join(chowncmd)) )
ret, out, err = pyeole.process.creole_system_out( chowncmd, container=container, context=False )
if ret != 0:
log.error(_(u'Error changing properties {0}: {1}').format(ret, err) )
log.info(_(u'Changing properties: {0}').format(' '.join(chmodcmd)) )
ret, out, err = pyeole.process.creole_system_out( chmodcmd, container=container, context=False )
if ret != 0:
log.error(_(u'Error changing properties {0}: {1}').format(ret, err) )
def remove_destfile(self, filevar):
"""
suppression du fichier de destination
"""
destfilename = filevar[u'full_name']
if os.path.isfile(destfilename):
os.unlink(destfilename)
else:
log.debug(_(u"File '{0}' unexistent.").format(destfilename))
def _instance_file(self, filevar, container=None):
"""Run templatisation on one file of one container
@param filevar: Dictionary describing the file
@type filevar: C{dict}
@param container: Dictionary describing a container
@type container: C{dict}
"""
if not filevar.get(u'activate', False):
try:
# copy and patch disabled templates too (#11029)
self.prepare_template(filevar, force_no_active=True)
except FileNotFound:
pass
if u'rm' in filevar and filevar[u'rm']:
log.info(_(u"Removing file '{0}'"
u" from container '{1}'").format(filevar[u'name'],
container[u'name']))
self.remove_destfile(filevar)
# The caller handles if it's an error
raise TemplateDisabled(_(u"Instantiation of file '{0}' disabled").format(filevar[u'name']))
log.info(_(u"Instantiating file '{0}'"
u" from '{1}'").format(filevar[u'name'], filevar[u'source']))
self.prepare_template(filevar)
self.verify(filevar)
self.process(filevar, container)
if filevar['name'].startswith('..') and container not in [None, 'root']:
self.change_properties(filevar, None, True)
else:
self.change_properties(filevar, container)
def instance_file(self, filename=None, container='root', ctx=None):
"""Run templatisation on one file
@param filename: name of a file
@type filename: C{str}
@param container: name of a container
@type container: C{str}
"""
if container == 'all':
if ctx is None:
groups = self.client.get_groups()
else:
groups = ctx.keys()
for group in groups:
if group in ['all', 'root']:
continue
if ctx is None:
lctx = None
else:
lctx = ctx[group]
self.instance_file(filename=filename, container=group, ctx=lctx)
else:
if ctx is None:
ctx = self.client.get_container_infos(container)
filevars = [f for f in ctx[u'files'] if f[u'name'] == filename]
for f in filevars:
self._instance_file(f, ctx)
def instance_files(self, filenames=None, container=None, containers_ctx=None):
"""Run templatisation on all files of all containers
@param filenames: names of files
@type filename: C{list}
@param container: name of a container
@type container: C{str}
"""
if containers_ctx is None:
containers_ctx = []
if container is not None:
containers_ctx = [self.client.get_container_infos(container)]
else:
for group_name in self.client.get_groups():
containers_ctx.append(self.client.get_group_infos(group_name))
if filenames is None:
all_files = set(listdir(cfg.distrib_dir))
prev_files = set(listdir(cfg.templatedir))
all_declared_files = set()
for ctx in containers_ctx:
for fdict in ctx[u'files']:
all_declared_files.add(basename(fdict['source']))
undeclared_files = all_files - all_declared_files
toremove_files = prev_files - all_files
# delete old templates (#6600)
for fname in toremove_files:
rm_file = join(cfg.templatedir, fname)
log.debug(_(u"Removing file '{0}'").format(rm_file))
unlink(rm_file)
# copy template not referenced in a dictionary (#6303)
for fname in undeclared_files:
fobj = {'source': join(cfg.templatedir, fname), 'name': ''}
self.prepare_template(fobj, True)
for ctx in containers_ctx:
for fdict in ctx[u'files']:
if not filenames or fdict[u'name'] in filenames:
try:
self._instance_file(fdict, container=ctx)
except TemplateDisabled, err:
# Information on disabled template only useful
# in debug
log.debug(err, exc_info=True)