2009-12-15 14:26:01 +00:00
|
|
|
#
|
2010-02-23 13:20:05 +00:00
|
|
|
# ltmpl.py
|
2010-01-12 11:45:54 +00:00
|
|
|
#
|
2014-05-09 22:13:39 +00:00
|
|
|
# Copyright (C) 2009-2014 Red Hat, Inc.
|
2010-01-12 11:45:54 +00:00
|
|
|
#
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# Red Hat Author(s): Martin Gracik <mgracik@redhat.com>
|
2011-06-30 17:22:39 +00:00
|
|
|
# Will Woods <wwoods@redhat.com>
|
2009-12-15 14:26:01 +00:00
|
|
|
#
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
import logging
|
|
|
|
logger = logging.getLogger("pylorax.ltmpl")
|
|
|
|
|
|
|
|
import os, re, glob, shlex, fnmatch
|
|
|
|
from os.path import basename, isdir
|
2012-07-27 14:29:34 +00:00
|
|
|
from subprocess import CalledProcessError
|
2011-06-30 17:22:39 +00:00
|
|
|
|
2014-05-09 00:21:34 +00:00
|
|
|
from pylorax.sysutils import joinpaths, cpfile, mvfile, replace, remove
|
2014-06-06 23:22:28 +00:00
|
|
|
from pylorax.dnfhelper import LoraxDownloadCallback, LoraxRpmCallback
|
2014-05-09 00:21:34 +00:00
|
|
|
from pylorax.base import DataHolder
|
2012-08-22 22:24:49 +00:00
|
|
|
from pylorax.executils import runcmd, runcmd_output
|
2014-11-05 02:57:21 +00:00
|
|
|
from pylorax.imgutils import mkcpio
|
2014-06-06 23:22:28 +00:00
|
|
|
import pylorax.output as output
|
2009-12-15 14:26:01 +00:00
|
|
|
|
2010-10-12 16:23:29 +00:00
|
|
|
from mako.lookup import TemplateLookup
|
2011-05-26 18:08:01 +00:00
|
|
|
from mako.exceptions import text_error_template
|
2011-09-15 23:24:35 +00:00
|
|
|
import sys, traceback
|
2012-02-07 17:46:30 +00:00
|
|
|
import struct
|
2014-06-06 23:22:28 +00:00
|
|
|
import dnf
|
|
|
|
import multiprocessing
|
2009-12-15 14:26:01 +00:00
|
|
|
|
2010-10-12 16:23:29 +00:00
|
|
|
class LoraxTemplate(object):
|
2014-05-09 00:21:34 +00:00
|
|
|
def __init__(self, directories=None):
|
|
|
|
directories = directories or ["/usr/share/lorax"]
|
2011-05-10 03:42:10 +00:00
|
|
|
# we have to add ["/"] to the template lookup directories or the
|
|
|
|
# file includes won't work properly for absolute paths
|
|
|
|
self.directories = ["/"] + directories
|
2009-12-15 14:26:01 +00:00
|
|
|
|
2010-02-23 13:20:05 +00:00
|
|
|
def parse(self, template_file, variables):
|
2011-05-10 03:42:10 +00:00
|
|
|
lookup = TemplateLookup(directories=self.directories)
|
|
|
|
template = lookup.get_template(template_file)
|
2010-10-29 12:41:23 +00:00
|
|
|
|
|
|
|
try:
|
2010-12-02 12:20:41 +00:00
|
|
|
textbuf = template.render(**variables)
|
2010-10-29 12:41:23 +00:00
|
|
|
except:
|
2011-09-15 23:24:35 +00:00
|
|
|
logger.error(text_error_template().render())
|
|
|
|
raise
|
2010-02-23 13:20:05 +00:00
|
|
|
|
2010-10-12 16:23:29 +00:00
|
|
|
# split, strip and remove empty lines
|
2010-12-02 12:20:41 +00:00
|
|
|
lines = textbuf.splitlines()
|
2010-10-12 16:23:29 +00:00
|
|
|
lines = map(lambda line: line.strip(), lines)
|
|
|
|
lines = filter(lambda line: line, lines)
|
|
|
|
|
2011-05-31 15:28:18 +00:00
|
|
|
# remove comments
|
|
|
|
lines = filter(lambda line: not line.startswith("#"), lines)
|
|
|
|
|
2011-03-10 09:53:55 +00:00
|
|
|
# mako template now returns unicode strings
|
2011-07-06 22:02:20 +00:00
|
|
|
lines = map(lambda line: line.encode("utf8"), lines)
|
2011-03-10 09:53:55 +00:00
|
|
|
|
2011-07-06 22:02:20 +00:00
|
|
|
# split with shlex and perform brace expansion
|
|
|
|
lines = map(split_and_expand, lines)
|
2010-10-12 16:23:29 +00:00
|
|
|
|
2010-02-23 13:20:05 +00:00
|
|
|
return lines
|
2011-06-30 17:22:39 +00:00
|
|
|
|
2011-07-06 22:02:20 +00:00
|
|
|
def split_and_expand(line):
|
|
|
|
return [exp for word in shlex.split(line) for exp in brace_expand(word)]
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
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
|
2011-07-06 22:02:20 +00:00
|
|
|
for f in glob.iglob(joinpaths(root, pathname)):
|
|
|
|
if f not in seen:
|
|
|
|
seen.add(f)
|
|
|
|
yield f[rootlen:] # remove the root to produce relative path
|
2011-06-30 17:22:39 +00:00
|
|
|
if fatal and not seen:
|
2014-05-09 00:21:34 +00:00
|
|
|
raise IOError("nothing matching %s in %s" % (pathname, root))
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def rexists(pathname, root=""):
|
2012-12-19 11:39:39 +00:00
|
|
|
# Generator is always True, even with no values;
|
|
|
|
# bool(rglob(...)) won't work here.
|
|
|
|
for _path in rglob(pathname, root):
|
|
|
|
return True
|
|
|
|
return False
|
2011-06-30 17:22:39 +00:00
|
|
|
|
2011-10-25 20:19:23 +00:00
|
|
|
# TODO: operate inside an actual chroot for safety? Not that RPM bothers..
|
2011-06-30 17:22:39 +00:00
|
|
|
class LoraxTemplateRunner(object):
|
2012-06-19 19:03:17 +00:00
|
|
|
'''
|
|
|
|
This class parses and executes Lorax templates. Sample usage:
|
|
|
|
|
|
|
|
# install a bunch of packages
|
2014-06-06 23:22:28 +00:00
|
|
|
runner = LoraxTemplateRunner(inroot=rundir, outroot=rundir, dbo=dnf_obj)
|
2012-06-19 19:03:17 +00:00
|
|
|
runner.run("install-packages.ltmpl")
|
|
|
|
|
|
|
|
# modify a runtime dir
|
|
|
|
runner = LoraxTemplateRunner(inroot=rundir, outroot=newrun)
|
|
|
|
runner.run("runtime-transmogrify.ltmpl")
|
|
|
|
|
|
|
|
NOTES:
|
|
|
|
|
|
|
|
* Parsing procedure is roughly:
|
|
|
|
1. Mako template expansion (on the whole file)
|
|
|
|
2. For each line of the result,
|
|
|
|
a. Whitespace splitting (using shlex.split())
|
|
|
|
b. Brace expansion (using brace_expand())
|
|
|
|
c. If the first token is the name of a function, call that function
|
|
|
|
with the rest of the line as arguments
|
|
|
|
|
|
|
|
* Parsing and execution are *separate* passes - so you can't use the result
|
|
|
|
of a command in an %if statement (or any other control statements)!
|
|
|
|
|
|
|
|
* Commands that run external programs (systemctl, gconfset) currently use
|
|
|
|
the *host*'s copy of that program, which may cause problems if there's a
|
|
|
|
big enough difference between the host and the image you're modifying.
|
|
|
|
|
|
|
|
* The commands are not executed under a real chroot, so absolute symlinks
|
|
|
|
will point *outside* the inroot/outroot. Be careful with symlinks!
|
|
|
|
|
|
|
|
ADDING NEW COMMANDS:
|
|
|
|
|
|
|
|
* Each template command is just a method of the LoraxTemplateRunner
|
|
|
|
object - so adding a new command is as easy as adding a new function.
|
|
|
|
|
|
|
|
* Each function gets arguments that correspond to the rest of the tokens
|
|
|
|
on that line (after word splitting and brace expansion)
|
|
|
|
|
|
|
|
* Commands should raise exceptions for errors - don't use sys.exit()
|
|
|
|
'''
|
2014-06-06 23:22:28 +00:00
|
|
|
def __init__(self, inroot, outroot, dbo=None, fatalerrors=True,
|
2014-05-09 00:21:34 +00:00
|
|
|
templatedir=None, defaults=None):
|
2011-06-30 17:22:39 +00:00
|
|
|
self.inroot = inroot
|
|
|
|
self.outroot = outroot
|
2014-06-06 23:22:28 +00:00
|
|
|
self.dbo = dbo
|
2011-06-30 17:22:39 +00:00
|
|
|
self.fatalerrors = fatalerrors
|
2011-08-08 23:01:38 +00:00
|
|
|
self.templatedir = templatedir or "/usr/share/lorax"
|
2014-05-09 00:21:34 +00:00
|
|
|
self.templatefile = None
|
2011-06-30 20:59:55 +00:00
|
|
|
# some builtin methods
|
|
|
|
self.builtins = DataHolder(exists=lambda p: rexists(p, root=inroot),
|
2011-06-30 21:54:02 +00:00
|
|
|
glob=lambda g: list(rglob(g, root=inroot)))
|
2014-05-09 00:21:34 +00:00
|
|
|
self.defaults = defaults or {}
|
2011-06-30 17:22:39 +00:00
|
|
|
self.results = DataHolder(treeinfo=dict()) # just treeinfo for now
|
2011-08-31 23:32:37 +00:00
|
|
|
# TODO: set up custom logger with a filter to add line info
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def _out(self, path):
|
|
|
|
return joinpaths(self.outroot, path)
|
|
|
|
def _in(self, path):
|
|
|
|
return joinpaths(self.inroot, path)
|
|
|
|
|
|
|
|
def _filelist(self, *pkgs):
|
2014-06-19 21:43:56 +00:00
|
|
|
""" Return the list of files in the packages """
|
|
|
|
pkglist = []
|
|
|
|
for pkg_glob in pkgs:
|
|
|
|
pkglist += list(self.dbo.sack.query().installed().filter(name__glob=pkg_glob))
|
|
|
|
|
|
|
|
# dnf/hawkey doesn't make any distinction between file, dir or ghost like yum did
|
|
|
|
# so only return the files.
|
|
|
|
return set(f for pkg in pkglist for f in pkg.files if not os.path.isdir(self._out(f)))
|
2011-06-30 17:22:39 +00:00
|
|
|
|
2011-08-01 21:24:20 +00:00
|
|
|
def _getsize(self, *files):
|
|
|
|
return sum(os.path.getsize(self._out(f)) for f in files if os.path.isfile(self._out(f)))
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
def run(self, templatefile, **variables):
|
2011-06-30 20:59:55 +00:00
|
|
|
for k,v in self.defaults.items() + self.builtins.items():
|
2011-06-30 17:22:39 +00:00
|
|
|
variables.setdefault(k,v)
|
2011-09-15 23:24:35 +00:00
|
|
|
logger.debug("parsing %s", templatefile)
|
|
|
|
self.templatefile = templatefile
|
2011-06-30 17:22:39 +00:00
|
|
|
t = LoraxTemplate(directories=[self.templatedir])
|
|
|
|
commands = t.parse(templatefile, variables)
|
|
|
|
self._run(commands)
|
|
|
|
|
2011-09-15 23:24:35 +00:00
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
def _run(self, parsed_template):
|
2011-09-15 23:24:35 +00:00
|
|
|
logger.info("running %s", self.templatefile)
|
2011-06-30 17:22:39 +00:00
|
|
|
for (num, line) in enumerate(parsed_template,1):
|
|
|
|
logger.debug("template line %i: %s", num, " ".join(line))
|
2011-10-26 17:06:05 +00:00
|
|
|
skiperror = False
|
2011-06-30 17:22:39 +00:00
|
|
|
(cmd, args) = (line[0], line[1:])
|
2011-10-26 17:06:05 +00:00
|
|
|
# Following Makefile convention, if the command is prefixed with
|
|
|
|
# a dash ('-'), we'll ignore any errors on that line.
|
|
|
|
if cmd.startswith('-'):
|
|
|
|
cmd = cmd[1:]
|
|
|
|
skiperror = True
|
2011-06-30 17:22:39 +00:00
|
|
|
try:
|
|
|
|
# grab the method named in cmd and pass it the given arguments
|
|
|
|
f = getattr(self, cmd, None)
|
2011-11-04 17:41:10 +00:00
|
|
|
if cmd[0] == '_' or cmd == 'run' or not callable(f):
|
2014-05-09 00:21:34 +00:00
|
|
|
raise ValueError("unknown command %s" % cmd)
|
2011-06-30 17:22:39 +00:00
|
|
|
f(*args)
|
2014-05-09 00:21:34 +00:00
|
|
|
except Exception: # pylint: disable=broad-except
|
2011-10-26 17:06:05 +00:00
|
|
|
if skiperror:
|
2012-06-18 22:18:37 +00:00
|
|
|
logger.debug("ignoring error")
|
2011-10-26 17:06:05 +00:00
|
|
|
continue
|
2011-09-15 23:24:35 +00:00
|
|
|
logger.error("template command error in %s:", self.templatefile)
|
|
|
|
logger.error(" %s", " ".join(line))
|
|
|
|
# format the exception traceback
|
|
|
|
exclines = traceback.format_exception(*sys.exc_info())
|
|
|
|
# skip the bit about "ltmpl.py, in _run()" - we know that
|
|
|
|
exclines.pop(1)
|
|
|
|
# log the "ErrorType: this is what happened" line
|
|
|
|
logger.error(" " + exclines[-1].strip())
|
|
|
|
# and log the entire traceback to the debug log
|
|
|
|
for line in ''.join(exclines).splitlines():
|
|
|
|
logger.debug(" " + line)
|
2011-06-30 17:22:39 +00:00
|
|
|
if self.fatalerrors:
|
|
|
|
raise
|
|
|
|
|
|
|
|
def install(self, srcglob, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
install SRC DEST
|
|
|
|
Copy the given file (or files, if a glob is used) from the input
|
|
|
|
tree to the given destination in the output tree.
|
|
|
|
The path to DEST must exist in the output tree.
|
|
|
|
If DEST is a directory, SRC will be copied into that directory.
|
|
|
|
If DEST doesn't exist, SRC will be copied to a file with that name,
|
|
|
|
assuming the rest of the path exists.
|
|
|
|
This is pretty much like how the 'cp' command works.
|
|
|
|
Examples:
|
|
|
|
install usr/share/myconfig/grub.conf /boot
|
|
|
|
install /usr/share/myconfig/grub.conf.in /boot/grub.conf
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
for src in rglob(self._in(srcglob), fatal=True):
|
|
|
|
cpfile(src, self._out(dest))
|
|
|
|
|
2014-11-05 02:57:21 +00:00
|
|
|
def installimg(self, srcdir, destfile):
|
|
|
|
'''
|
|
|
|
installimg SRCDIR DESTFILE
|
|
|
|
Create a compressed cpio archive of the contents of SRCDIR and place
|
|
|
|
it in DESTFILE.
|
|
|
|
|
|
|
|
If SRCDIR doesn't exist or is empty nothing is created.
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
installimg ${LORAXDIR}/product/ images/product.img
|
|
|
|
installimg ${LORAXDIR}/updates/ images/updates.img
|
|
|
|
'''
|
|
|
|
if not os.path.isdir(self._in(srcdir)) or not os.listdir(self._in(srcdir)):
|
|
|
|
return
|
|
|
|
logger.info("Creating image file %s from contents of %s", self._out(destfile), self._in(srcdir))
|
|
|
|
mkcpio(self._in(srcdir), self._out(destfile))
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
def mkdir(self, *dirs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
mkdir DIR [DIR ...]
|
|
|
|
Create the named DIR(s). Will create leading directories as needed.
|
|
|
|
Example:
|
|
|
|
mkdir /images
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
for d in dirs:
|
|
|
|
d = self._out(d)
|
|
|
|
if not isdir(d):
|
|
|
|
os.makedirs(d)
|
|
|
|
|
|
|
|
def replace(self, pat, repl, *fileglobs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
replace PATTERN REPLACEMENT FILEGLOB [FILEGLOB ...]
|
|
|
|
Find-and-replace the given PATTERN (Python-style regex) with the given
|
|
|
|
REPLACEMENT string for each of the files listed.
|
|
|
|
Example:
|
|
|
|
replace @VERSION@ ${product.version} /boot/grub.conf /boot/isolinux.cfg
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
match = False
|
|
|
|
for g in fileglobs:
|
|
|
|
for f in rglob(self._out(g)):
|
|
|
|
match = True
|
|
|
|
replace(f, pat, repl)
|
|
|
|
if not match:
|
2014-05-09 00:21:34 +00:00
|
|
|
raise IOError("no files matched %s" % " ".join(fileglobs))
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def append(self, filename, data):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
append FILE STRING
|
|
|
|
Append STRING (followed by a newline character) to FILE.
|
2011-11-04 17:41:10 +00:00
|
|
|
Python character escape sequences ('\\n', '\\t', etc.) will be
|
2011-09-14 22:33:30 +00:00
|
|
|
converted to the appropriate characters.
|
|
|
|
Examples:
|
|
|
|
append /etc/depmod.d/dd.conf "search updates built-in"
|
|
|
|
append /etc/resolv.conf ""
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
with open(self._out(filename), "a") as fobj:
|
|
|
|
fobj.write(data.decode('string_escape')+"\n")
|
|
|
|
|
|
|
|
def treeinfo(self, section, key, *valuetoks):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
treeinfo SECTION KEY ARG [ARG ...]
|
|
|
|
Add an item to the treeinfo data store.
|
|
|
|
The given SECTION will have a new item added where
|
|
|
|
KEY = ARG ARG ...
|
|
|
|
Example:
|
|
|
|
treeinfo images-${kernel.arch} boot.iso images/boot.iso
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
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):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
installkernel SECTION SRC DEST
|
|
|
|
Install the kernel from SRC in the input tree to DEST in the output
|
|
|
|
tree, and then add an item to the treeinfo data store, in the named
|
|
|
|
SECTION, where "kernel" = DEST.
|
|
|
|
|
|
|
|
Equivalent to:
|
|
|
|
install SRC DEST
|
|
|
|
treeinfo SECTION kernel DEST
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
self.install(src, dest)
|
|
|
|
self.treeinfo(section, "kernel", dest)
|
|
|
|
|
|
|
|
def installinitrd(self, section, src, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
installinitrd SECTION SRC DEST
|
|
|
|
Same as installkernel, but for "initrd".
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
self.install(src, dest)
|
2012-10-08 10:35:00 +00:00
|
|
|
self.chmod(dest, '644')
|
2011-06-30 17:22:39 +00:00
|
|
|
self.treeinfo(section, "initrd", dest)
|
|
|
|
|
2012-11-26 23:26:45 +00:00
|
|
|
def installupgradeinitrd(self, section, src, dest):
|
|
|
|
'''
|
|
|
|
installupgradeinitrd SECTION SRC DEST
|
|
|
|
Same as installkernel, but for "upgrade".
|
|
|
|
'''
|
|
|
|
self.install(src, dest)
|
|
|
|
self.chmod(dest, '644')
|
|
|
|
self.treeinfo(section, "upgrade", dest)
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
def hardlink(self, src, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
hardlink SRC DEST
|
|
|
|
Create a hardlink at DEST which is linked to SRC.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
if isdir(self._out(dest)):
|
|
|
|
dest = joinpaths(dest, basename(src))
|
|
|
|
os.link(self._out(src), self._out(dest))
|
|
|
|
|
|
|
|
def symlink(self, target, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
symlink SRC DEST
|
|
|
|
Create a symlink at DEST which points to SRC.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
if rexists(self._out(dest)):
|
|
|
|
self.remove(dest)
|
|
|
|
os.symlink(target, self._out(dest))
|
|
|
|
|
|
|
|
def copy(self, src, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
copy SRC DEST
|
|
|
|
Copy SRC to DEST.
|
|
|
|
If DEST is a directory, SRC will be copied inside it.
|
|
|
|
If DEST doesn't exist, SRC will be copied to a file with
|
|
|
|
that name, if the path leading to it exists.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
cpfile(self._out(src), self._out(dest))
|
|
|
|
|
|
|
|
def move(self, src, dest):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
move SRC DEST
|
|
|
|
Move SRC to DEST.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
mvfile(self._out(src), self._out(dest))
|
|
|
|
|
|
|
|
def remove(self, *fileglobs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
remove FILEGLOB [FILEGLOB ...]
|
|
|
|
Remove all the named files or directories.
|
2011-10-26 17:06:05 +00:00
|
|
|
Will *not* raise exceptions if the file(s) are not found.
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
for g in fileglobs:
|
|
|
|
for f in rglob(self._out(g)):
|
|
|
|
remove(f)
|
2012-03-21 08:49:42 +00:00
|
|
|
logger.debug("removed %s", f)
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def chmod(self, fileglob, mode):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
chmod FILEGLOB OCTALMODE
|
|
|
|
Change the mode of all the files matching FILEGLOB to OCTALMODE.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
for f in rglob(self._out(fileglob), fatal=True):
|
|
|
|
os.chmod(f, int(mode,8))
|
|
|
|
|
2011-09-14 22:33:30 +00:00
|
|
|
# TODO: do we need a new command for gsettings?
|
2011-06-30 17:22:39 +00:00
|
|
|
def gconfset(self, path, keytype, value, outfile=None):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
gconfset PATH KEYTYPE VALUE [OUTFILE]
|
|
|
|
Set the given gconf PATH, with type KEYTYPE, to the given value.
|
|
|
|
OUTFILE defaults to /etc/gconf/gconf.xml.defaults if not given.
|
|
|
|
Example:
|
|
|
|
gconfset /apps/metacity/general/num_workspaces int 1
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
if outfile is None:
|
|
|
|
outfile = self._out("etc/gconf/gconf.xml.defaults")
|
2012-07-27 14:29:34 +00:00
|
|
|
cmd = ["gconftool-2", "--direct",
|
2011-06-30 17:22:39 +00:00
|
|
|
"--config-source=xml:readwrite:%s" % outfile,
|
2012-07-27 14:29:34 +00:00
|
|
|
"--set", "--type", keytype, path, value]
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(cmd)
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def log(self, msg):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
log MESSAGE
|
|
|
|
Emit the given log message. Be sure to put it in quotes!
|
|
|
|
Example:
|
|
|
|
log "Reticulating splines, please wait..."
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
logger.info(msg)
|
|
|
|
|
2011-09-15 23:24:35 +00:00
|
|
|
# TODO: add ssh-keygen, mkisofs(?), find, and other useful commands
|
2011-06-30 17:22:39 +00:00
|
|
|
def runcmd(self, *cmdlist):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
runcmd CMD [--chdir=DIR] [ARG ...]
|
|
|
|
Run the given command with the given arguments.
|
|
|
|
If "--chdir=DIR" is given, change to the named directory
|
|
|
|
before executing the command.
|
|
|
|
|
|
|
|
NOTE: All paths given MUST be COMPLETE, ABSOLUTE PATHS to the file
|
|
|
|
or files mentioned. ${root}/${inroot}/${outroot} are good for
|
|
|
|
constructing these paths.
|
|
|
|
|
|
|
|
FURTHER NOTE: Please use this command only as a last resort!
|
|
|
|
Whenever possible, you should use the existing template commands.
|
|
|
|
If the existing commands don't do what you need, fix them!
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
(this should be replaced with a "find" function)
|
|
|
|
runcmd find ${root} -name "*.pyo" -type f -delete
|
|
|
|
%for f in find(root, name="*.pyo"):
|
|
|
|
remove ${f}
|
|
|
|
%endfor
|
|
|
|
'''
|
2012-07-27 14:29:34 +00:00
|
|
|
cwd = None
|
2011-06-30 17:22:39 +00:00
|
|
|
cmd = cmdlist
|
2012-01-19 12:50:02 +00:00
|
|
|
logger.debug('running command: %s', cmd)
|
2011-08-01 21:24:20 +00:00
|
|
|
if cmd[0].startswith("--chdir="):
|
2012-07-27 14:29:34 +00:00
|
|
|
cwd = cmd[0].split('=',1)[1]
|
2011-06-30 17:22:39 +00:00
|
|
|
cmd = cmd[1:]
|
2012-01-19 12:50:02 +00:00
|
|
|
|
|
|
|
try:
|
2014-06-06 23:22:28 +00:00
|
|
|
stdout = runcmd_output(cmd, cwd=cwd)
|
|
|
|
if stdout:
|
|
|
|
logger.debug('command output:\n%s', stdout)
|
2012-06-18 22:18:37 +00:00
|
|
|
logger.debug("command finished successfully")
|
2012-01-19 12:50:02 +00:00
|
|
|
except CalledProcessError as e:
|
2012-06-18 22:18:37 +00:00
|
|
|
if e.output:
|
|
|
|
logger.debug('command output:\n%s', e.output)
|
|
|
|
logger.debug('command returned failure (%d)', e.returncode)
|
|
|
|
raise
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def installpkg(self, *pkgs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
2012-06-01 12:42:55 +00:00
|
|
|
installpkg [--required] PKGGLOB [PKGGLOB ...]
|
2011-09-14 22:33:30 +00:00
|
|
|
Request installation of all packages matching the given globs.
|
|
|
|
Note that this is just a *request* - nothing is *actually* installed
|
|
|
|
until the 'run_pkg_transaction' command is given.
|
|
|
|
'''
|
2012-06-01 12:42:55 +00:00
|
|
|
required = False
|
|
|
|
if pkgs[0] == '--required':
|
|
|
|
pkgs = pkgs[1:]
|
|
|
|
required = True
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
for p in pkgs:
|
2011-09-15 23:27:31 +00:00
|
|
|
try:
|
2014-06-06 23:22:28 +00:00
|
|
|
self.dbo.install(p)
|
2014-05-09 00:21:34 +00:00
|
|
|
except Exception as e: # pylint: disable=broad-except
|
2011-10-24 20:27:36 +00:00
|
|
|
# FIXME: save exception and re-raise after the loop finishes
|
2014-06-06 23:22:28 +00:00
|
|
|
logger.error("installpkg %s failed: %s", p, str(e))
|
2012-06-01 12:42:55 +00:00
|
|
|
if required:
|
2012-06-18 22:18:37 +00:00
|
|
|
raise
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def removepkg(self, *pkgs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
removepkg PKGGLOB [PKGGLOB...]
|
|
|
|
Delete the named package(s).
|
|
|
|
IMPLEMENTATION NOTES:
|
|
|
|
RPM scriptlets (%preun/%postun) are *not* run.
|
|
|
|
Files are deleted, but directories are left behind.
|
|
|
|
'''
|
2011-08-01 21:24:20 +00:00
|
|
|
for p in pkgs:
|
|
|
|
filepaths = [f.lstrip('/') for f in self._filelist(p)]
|
2011-08-31 23:32:37 +00:00
|
|
|
# TODO: also remove directories that aren't owned by anything else
|
2011-08-01 21:24:20 +00:00
|
|
|
if filepaths:
|
|
|
|
logger.debug("removepkg %s: %ikb", p, self._getsize(*filepaths)/1024)
|
|
|
|
self.remove(*filepaths)
|
|
|
|
else:
|
|
|
|
logger.debug("removepkg %s: no files to remove!", p)
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def run_pkg_transaction(self):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
run_pkg_transaction
|
|
|
|
Actually install all the packages requested by previous 'installpkg'
|
|
|
|
commands.
|
|
|
|
'''
|
2014-06-06 23:22:28 +00:00
|
|
|
|
|
|
|
def do_transaction(base, queue):
|
|
|
|
try:
|
|
|
|
display = LoraxRpmCallback(queue)
|
|
|
|
base.do_transaction(display=display)
|
|
|
|
except BaseException as e:
|
|
|
|
logger.error("The transaction process has ended abruptly: %s", e)
|
|
|
|
queue.put(('quit', str(e)))
|
|
|
|
|
|
|
|
try:
|
|
|
|
logger.info("Checking dependencies")
|
|
|
|
self.dbo.resolve()
|
|
|
|
except dnf.exceptions.DepsolveError as e:
|
|
|
|
logger.error("Dependency check failed: %s", e)
|
|
|
|
raise
|
|
|
|
logger.info("%d packages selected", len(self.dbo.transaction))
|
|
|
|
if len(self.dbo.transaction) == 0:
|
|
|
|
raise Exception("No packages in transaction")
|
|
|
|
|
|
|
|
pkgs_to_download = self.dbo.transaction.install_set
|
|
|
|
logger.info("Downloading packages")
|
|
|
|
progress = LoraxDownloadCallback()
|
|
|
|
try:
|
|
|
|
self.dbo.download_packages(pkgs_to_download, progress)
|
|
|
|
except dnf.exceptions.DownloadError as e:
|
|
|
|
logger.error("Failed to download the following packages: %s", e)
|
|
|
|
raise
|
|
|
|
|
|
|
|
logger.info("Preparing transaction from installation source")
|
|
|
|
queue = multiprocessing.Queue()
|
|
|
|
msgout = output.LoraxOutput()
|
|
|
|
process = multiprocessing.Process(target=do_transaction, args=(self.dbo, queue))
|
|
|
|
process.start()
|
|
|
|
(token, msg) = queue.get()
|
|
|
|
while token not in ('post', 'quit'):
|
|
|
|
if token == 'install':
|
|
|
|
logging.info("%s", msg)
|
|
|
|
msgout.writeline(msg)
|
|
|
|
(token, msg) = queue.get()
|
|
|
|
if token == 'quit':
|
|
|
|
logger.error("Transaction failed.")
|
|
|
|
raise Exception("Transaction failed")
|
|
|
|
|
|
|
|
logger.info("Performing post-installation setup tasks")
|
|
|
|
process.join()
|
2012-06-04 08:16:06 +00:00
|
|
|
|
2015-02-10 20:05:36 +00:00
|
|
|
# Reset the package sack to pick up the installed packages
|
|
|
|
self.dbo.reset(repos=False)
|
|
|
|
self.dbo.fill_sack(load_system_repo=True, load_available_repos=False)
|
|
|
|
|
|
|
|
# At this point dnf should know about the installed files. Double check that it really does.
|
|
|
|
if len(self._filelist("anaconda-core")) == 0:
|
|
|
|
raise Exception("Failed to reset dbo to installed package set")
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
def removefrom(self, pkg, *globs):
|
2011-09-14 22:33:30 +00:00
|
|
|
'''
|
|
|
|
removefrom PKGGLOB [--allbut] FILEGLOB [FILEGLOB...]
|
|
|
|
Remove all files matching the given file globs from the package
|
|
|
|
(or packages) named.
|
|
|
|
If '--allbut' is used, all the files from the given package(s) will
|
|
|
|
be removed *except* the ones which match the file globs.
|
|
|
|
Examples:
|
|
|
|
removefrom usbutils /usr/bin/*
|
|
|
|
removefrom xfsprogs --allbut /sbin/*
|
|
|
|
'''
|
2011-08-31 23:32:37 +00:00
|
|
|
cmd = "%s %s" % (pkg, " ".join(globs)) # save for later logging
|
|
|
|
keepmatches = False
|
|
|
|
if globs[0] == '--allbut':
|
|
|
|
keepmatches = True
|
|
|
|
globs = globs[1:]
|
|
|
|
# get pkg filelist and find files that match the globs
|
2011-08-01 21:24:20 +00:00
|
|
|
filelist = self._filelist(pkg)
|
2011-08-31 23:32:37 +00:00
|
|
|
matches = set()
|
2011-08-01 21:24:20 +00:00
|
|
|
for g in globs:
|
|
|
|
globs_re = re.compile(fnmatch.translate(g))
|
2011-08-31 23:32:37 +00:00
|
|
|
m = filter(globs_re.match, filelist)
|
|
|
|
if m:
|
|
|
|
matches.update(m)
|
2011-08-01 21:24:20 +00:00
|
|
|
else:
|
2011-08-31 23:32:37 +00:00
|
|
|
logger.debug("removefrom %s %s: no files matched!", pkg, g)
|
|
|
|
# are we removing the matches, or keeping only the matches?
|
|
|
|
if keepmatches:
|
2014-05-09 00:21:34 +00:00
|
|
|
remove_files = filelist.difference(matches)
|
2011-08-31 23:32:37 +00:00
|
|
|
else:
|
2014-05-09 00:21:34 +00:00
|
|
|
remove_files = matches
|
2011-08-31 23:32:37 +00:00
|
|
|
# remove the files
|
2014-05-09 00:21:34 +00:00
|
|
|
if remove_files:
|
2011-08-31 23:32:37 +00:00
|
|
|
logger.debug("%s: removed %i/%i files, %ikb/%ikb", cmd,
|
2014-05-09 00:21:34 +00:00
|
|
|
len(remove_files), len(filelist),
|
|
|
|
self._getsize(*remove_files)/1024, self._getsize(*filelist)/1024)
|
|
|
|
self.remove(*remove_files)
|
2011-08-31 23:32:37 +00:00
|
|
|
else:
|
|
|
|
logger.debug("%s: no files to remove!", cmd)
|
2012-02-07 17:46:30 +00:00
|
|
|
|
|
|
|
def createaddrsize(self, addr, src, dest):
|
|
|
|
'''
|
|
|
|
createaddrsize INITRD_ADDRESS INITRD ADDRSIZE
|
|
|
|
Create the initrd.addrsize file required in LPAR boot process.
|
|
|
|
Examples:
|
|
|
|
createaddrsize ${INITRD_ADDRESS} ${outroot}/${BOOTDIR}/initrd.img ${outroot}/${BOOTDIR}/initrd.addrsize
|
|
|
|
'''
|
|
|
|
addrsize = open(dest, "wb")
|
|
|
|
addrsize_data = struct.pack(">iiii", 0, int(addr, 16), 0, os.stat(src).st_size)
|
|
|
|
addrsize.write(addrsize_data)
|
|
|
|
addrsize.close()
|
2012-06-19 19:14:27 +00:00
|
|
|
|
|
|
|
def systemctl(self, cmd, *units):
|
|
|
|
'''
|
|
|
|
systemctl [enable|disable|mask] UNIT [UNIT...]
|
|
|
|
Enable, disable, or mask the given systemd units.
|
|
|
|
Examples:
|
|
|
|
systemctl disable lvm2-monitor.service
|
|
|
|
systemctl mask fedora-storage-init.service fedora-configure.service
|
|
|
|
'''
|
|
|
|
if cmd not in ('enable', 'disable', 'mask'):
|
|
|
|
raise ValueError('unsupported systemctl cmd: %s' % cmd)
|
|
|
|
if not units:
|
|
|
|
logger.debug("systemctl: no units given for %s, ignoring", cmd)
|
|
|
|
return
|
|
|
|
self.mkdir("/run/systemd/system") # XXX workaround for systemctl bug
|
|
|
|
systemctl = ('systemctl', '--root', self.outroot, '--no-reload',
|
|
|
|
'--quiet', cmd)
|
|
|
|
# XXX for some reason 'systemctl enable/disable' always returns 1
|
|
|
|
try:
|
2012-07-27 14:29:34 +00:00
|
|
|
cmd = systemctl + units
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(cmd)
|
2012-06-19 19:14:27 +00:00
|
|
|
except CalledProcessError:
|
|
|
|
pass
|