#
# ltmpl.py
#
# Copyright (C) 2009 Red Hat, Inc.
#
# 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, see .
#
# Red Hat Author(s): Martin Gracik
# Will Woods
#
import logging
logger = logging.getLogger("pylorax.ltmpl")
import os, re, glob, shlex, fnmatch
from os.path import basename, isdir
from subprocess import check_call
from sysutils import joinpaths, cpfile, mvfile, replace, remove
from yumhelper import * # Lorax*Callback classes
from base import DataHolder
from mako.lookup import TemplateLookup
from mako.exceptions import text_error_template
class LoraxTemplate(object):
def __init__(self, directories=["/usr/share/lorax"]):
# we have to add ["/"] to the template lookup directories or the
# file includes won't work properly for absolute paths
self.directories = ["/"] + directories
def parse(self, template_file, variables):
lookup = TemplateLookup(directories=self.directories)
template = lookup.get_template(template_file)
try:
textbuf = template.render(**variables)
except:
print text_error_template().render()
raise SystemExit(2)
# split, strip and remove empty lines
lines = textbuf.splitlines()
lines = map(lambda line: line.strip(), lines)
lines = filter(lambda line: line, lines)
# remove comments
lines = filter(lambda line: not line.startswith("#"), lines)
# mako template now returns unicode strings
lines = map(lambda line: line.encode("ascii"), lines)
# split with shlex
lines = map(shlex.split, lines)
self.lines = lines
return lines
def brace_expand(s):
if not ('{' in s and ',' in s and '}' in s):
yield s
else:
right = s.find('}')
left = s[:right].rfind('{')
(prefix, choices, suffix) = (s[:left], s[left+1:right], s[right+1:])
for choice in choices.split(','):
for alt in brace_expand(prefix+choice+suffix):
yield alt
def rglob(pathname, root="/", fatal=False):
seen = set()
rootlen = len(root)+1
for g in brace_expand(pathname):
for f in glob.iglob(joinpaths(root, g)):
if f not in seen:
seen.add(f)
yield f[rootlen:] # remove the root to produce relative path
if fatal and not seen:
raise IOError, "nothing matching %s in %s" % (pathname, root)
def rexists(pathname, root=""):
return True if rglob(pathname, root) else False
# command notes:
# "install" and "exist" assume their first argument is in inroot
# everything else operates on outroot
# multiple args allowed: mkdir, treeinfo, runcmd, remove, replace
# globs accepted: chmod, install*, remove*, replace
class LoraxTemplateRunner(object):
def __init__(self, inroot, outroot, yum=None, fatalerrors=False,
templatedir=None, defaults={}):
self.inroot = inroot
self.outroot = outroot
self.yum = yum
self.fatalerrors = fatalerrors
self.templatedir = templatedir
# some builtin methods
self.builtins = DataHolder(exists=lambda p: rexists(p, root=inroot),
glob=lambda g: rglob(g, root=inroot))
self.defaults = defaults
self.results = DataHolder(treeinfo=dict()) # just treeinfo for now
def _out(self, path):
return joinpaths(self.outroot, path)
def _in(self, path):
return joinpaths(self.inroot, path)
def _filelist(self, *pkgs):
pkglist = self.yum.doPackageLists(pkgnarrow="installed", patterns=pkgs)
return set([f for pkg in pkglist.installed for f in pkg.filelist])
def run(self, templatefile, **variables):
for k,v in self.defaults.items() + self.builtins.items():
variables.setdefault(k,v)
logger.info("parsing %s", templatefile)
t = LoraxTemplate(directories=[self.templatedir])
commands = t.parse(templatefile, variables)
self._run(commands)
def _run(self, parsed_template):
logger.info("running template commands")
for (num, line) in enumerate(parsed_template,1):
logger.debug("template line %i: %s", num, " ".join(line))
(cmd, args) = (line[0], line[1:])
try:
# grab the method named in cmd and pass it the given arguments
f = getattr(self, cmd, None)
if f is None or cmd is 'run':
raise ValueError, "unknown command %s" % cmd
f(*args)
except Exception as e:
logger.error("template command error: %s", str(line))
if self.fatalerrors:
raise
logger.error(str(e))
def install(self, srcglob, dest):
for src in rglob(self._in(srcglob), fatal=True):
cpfile(src, self._out(dest))
def mkdir(self, *dirs):
for d in dirs:
d = self._out(d)
if not isdir(d):
os.makedirs(d)
def replace(self, pat, repl, *fileglobs):
match = False
for g in fileglobs:
for f in rglob(self._out(g)):
match = True
replace(f, pat, repl)
if not match:
raise IOError, "no files matched %s" % " ".join(fileglobs)
def append(self, filename, data):
with open(self._out(filename), "a") as fobj:
fobj.write(data.decode('string_escape')+"\n")
def treeinfo(self, section, key, *valuetoks):
if section not in self.results.treeinfo:
self.results.treeinfo[section] = dict()
self.results.treeinfo[section][key] = " ".join(valuetoks)
def installkernel(self, section, src, dest):
self.install(src, dest)
self.treeinfo(section, "kernel", dest)
def installinitrd(self, section, src, dest):
self.install(src, dest)
self.treeinfo(section, "initrd", dest)
def hardlink(self, src, dest):
if isdir(self._out(dest)):
dest = joinpaths(dest, basename(src))
os.link(self._out(src), self._out(dest))
def symlink(self, target, dest):
if rexists(self._out(dest)):
self.remove(dest)
os.symlink(target, self._out(dest))
def copy(self, src, dest):
cpfile(self._out(src), self._out(dest))
def copyif(self, src, dest):
if rexists(self._out(src)):
self.copy(src, dest)
def move(self, src, dest):
mvfile(self._out(src), self._out(dest))
def moveif(self, src, dest):
if rexists(self._out(src)):
self.move(src, dest)
def remove(self, *fileglobs):
for g in fileglobs:
for f in rglob(self._out(g)):
remove(f)
def chmod(self, fileglob, mode):
for f in rglob(self._out(fileglob), fatal=True):
os.chmod(f, int(mode,8))
def gconfset(self, path, keytype, value, outfile=None):
if outfile is None:
outfile = self._out("etc/gconf/gconf.xml.defaults")
check_call(["gconftool-2", "--direct",
"--config-source=xml:readwrite:%s" % outfile,
"--set", "--type", keytype, path, value])
def log(self, msg):
logger.info(msg)
def runcmd(self, *cmdlist):
'''Note that we need full paths for everything here'''
chdir = lambda: None
cmd = cmdlist
if cmd[0].startswith("chdir="):
dirname = cmd[0].split('=',1)[1]
chdir = lambda: os.chdir(dirname)
cmd = cmd[1:]
check_call(cmd, preexec_fn=chdir)
def installpkg(self, *pkgs):
for p in pkgs:
self.yum.install(pattern=p)
def removepkg(self, *pkgs):
# NOTE: "for p in pkgs: self.yum.remove(pattern=p)" traces back, so..
filepaths = [f.lstrip('/') for f in self._filelist(*pkgs)]
self.remove(*filepaths)
def run_pkg_transaction(self):
self.yum.buildTransaction()
self.yum.repos.setProgressBar(LoraxDownloadCallback())
self.yum.processTransaction(callback=LoraxTransactionCallback(),
rpmDisplay=LoraxRpmCallback())
self.yum.closeRpmDB()
def removefrom(self, pkg, *globs):
globset = set()
for g in globs:
globset.update(brace_expand(g))
globs_re = re.compile("|".join([fnmatch.translate(g) for g in globset]))
remove = filter(globs_re.match, self._filelist(pkg))
logger.debug("removing %i files from %s", len(remove), pkg)
self.remove(*remove)