608 lines
22 KiB
Python
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)
|