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
|
|
|
#
|
|
|
|
# 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 <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
|
2016-03-30 20:57:10 +00:00
|
|
|
import shutil
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
from sysutils import joinpaths, cpfile, mvfile, replace, remove
|
|
|
|
from yumhelper import * # Lorax*Callback classes
|
|
|
|
from 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
|
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
|
2009-12-15 14:26:01 +00:00
|
|
|
|
2010-10-12 16:23:29 +00:00
|
|
|
class LoraxTemplate(object):
|
2011-05-10 03:42:10 +00:00
|
|
|
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
|
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
|
|
|
|
2011-04-27 20:37:19 +00:00
|
|
|
self.lines = lines
|
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:
|
|
|
|
raise IOError, "nothing matching %s in %s" % (pathname, root)
|
|
|
|
|
|
|
|
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
|
|
|
|
runner = LoraxTemplateRunner(inroot=rundir, outroot=rundir, yum=yum_obj)
|
|
|
|
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()
|
|
|
|
'''
|
2011-10-26 17:09:50 +00:00
|
|
|
def __init__(self, inroot, outroot, yum=None, fatalerrors=True,
|
2011-06-30 17:22:39 +00:00
|
|
|
templatedir=None, defaults={}):
|
|
|
|
self.inroot = inroot
|
|
|
|
self.outroot = outroot
|
|
|
|
self.yum = yum
|
|
|
|
self.fatalerrors = fatalerrors
|
2011-08-08 23:01:38 +00:00
|
|
|
self.templatedir = templatedir or "/usr/share/lorax"
|
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)))
|
2011-06-30 20:59:55 +00:00
|
|
|
self.defaults = defaults
|
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):
|
|
|
|
pkglist = self.yum.doPackageLists(pkgnarrow="installed", patterns=pkgs)
|
2012-01-05 21:15:54 +00:00
|
|
|
return set([f for pkg in pkglist.installed for f in pkg.filelist+pkg.ghostlist])
|
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)
|
Add ability for external templates to graft content into boot.iso (#1202278)
I originally added --add-template to support doing something similar
to pungi, which injects content into the system to be used by default.
However, this causes the content to be part of the squashfs, which
means PXE installations have to download significantly more data that
they may not need (if they actually want to pull the tree data from
the network, which is not an unusual case).
What I actually need is to be able to modify *both* the runtime image
and the arch-specific content. For the runtime, I need to change
/usr/share/anaconda/interactive-defaults.ks to point to the new
content. (Although, potentially we could patch Anaconda itself to
auto-detect an ostree repository configured in disk image, similar to
what it does for yum repositories)
For the arch-specfic image, I want to drop my content into the ISO
root.
So this patch adds --add-arch-template and --add-arch-template-var
in order to do the latter, while preserving the --add-template
to affect the runtime image.
Further, the templates will automatically graft in a directory named
"iso-graft/" from the working directory (if it exists).
(I suggest that external templates create a subdirectory named
"content" to avoid clashes with any future lorax work)
Thus, this will be used by the Atomic Host lorax templates to inject
content/repo, but could be used by e.g. pungi to add content/rpms as
well.
I tried to avoid code deduplication by creating a new template for the
product.img bits and this, but that broke because the parent boot.iso
code needs access to the `${imggraft}` variable. I think a real fix
here would involve turning the product.img, content/, *and* boot.iso
into a new template.
Resolves: rhbz#1202278
2015-03-17 21:26:21 +00:00
|
|
|
logger.debug("executing {0} with variables={1}".format(templatefile, variables))
|
2011-09-15 23:24:35 +00:00
|
|
|
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):
|
2011-06-30 17:22:39 +00:00
|
|
|
raise ValueError, "unknown command %s" % cmd
|
|
|
|
f(*args)
|
2011-09-15 23:24:35 +00:00
|
|
|
except Exception:
|
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):
|
2016-03-30 20:57:10 +00:00
|
|
|
try:
|
|
|
|
cpfile(src, self._out(dest))
|
|
|
|
except shutil.Error as e:
|
|
|
|
logger.error(e)
|
2011-06-30 17:22:39 +00:00
|
|
|
|
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:
|
|
|
|
raise IOError, "no files matched %s" % " ".join(fileglobs)
|
|
|
|
|
|
|
|
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.
|
|
|
|
'''
|
2016-03-30 20:57:10 +00:00
|
|
|
try:
|
|
|
|
cpfile(self._out(src), self._out(dest))
|
|
|
|
except shutil.Error as e:
|
|
|
|
logger.error(e)
|
2011-06-30 17:22:39 +00:00
|
|
|
|
|
|
|
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:
|
2012-08-22 22:24:49 +00:00
|
|
|
output = runcmd_output(cmd, cwd=cwd)
|
2012-06-18 22:18:37 +00:00
|
|
|
if output:
|
|
|
|
logger.debug('command output:\n%s', output)
|
|
|
|
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:
|
|
|
|
self.yum.install(pattern=p)
|
|
|
|
except Exception as e:
|
2011-10-24 20:27:36 +00:00
|
|
|
# FIXME: save exception and re-raise after the loop finishes
|
2012-06-01 12:42:55 +00:00
|
|
|
logger.error("installpkg %s failed: %s",p,str(e))
|
|
|
|
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.
|
|
|
|
'''
|
2011-06-30 17:22:39 +00:00
|
|
|
self.yum.buildTransaction()
|
|
|
|
self.yum.repos.setProgressBar(LoraxDownloadCallback())
|
|
|
|
self.yum.processTransaction(callback=LoraxTransactionCallback(),
|
|
|
|
rpmDisplay=LoraxRpmCallback())
|
2012-06-04 08:16:06 +00:00
|
|
|
|
|
|
|
# verify if all packages that were supposed to be installed,
|
|
|
|
# are really installed
|
|
|
|
errs = [t.po for t in self.yum.tsInfo if not self.yum.rpmdb.contains(po=t.po)]
|
|
|
|
for po in errs:
|
|
|
|
logger.error("package '%s' was not installed", po)
|
|
|
|
|
2017-08-08 18:24:30 +00:00
|
|
|
# Write the manifest of installed files to /root/lorax-packages.log
|
|
|
|
with open(self._out("root/lorax-packages.log"), "w") as f:
|
|
|
|
for t in sorted(self.yum.tsInfo):
|
|
|
|
f.write("%s\n" % t.po)
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
self.yum.closeRpmDB()
|
|
|
|
|
|
|
|
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:
|
|
|
|
remove = filelist.difference(matches)
|
|
|
|
else:
|
|
|
|
remove = matches
|
|
|
|
# remove the files
|
|
|
|
if remove:
|
|
|
|
logger.debug("%s: removed %i/%i files, %ikb/%ikb", cmd,
|
|
|
|
len(remove), len(filelist),
|
|
|
|
self._getsize(*remove)/1024, self._getsize(*filelist)/1024)
|
|
|
|
self.remove(*remove)
|
|
|
|
else:
|
2015-03-04 19:33:40 +00:00
|
|
|
logger.debug("removefrom %s: no files to remove!", cmd)
|
|
|
|
|
|
|
|
def removekmod(self, *globs):
|
|
|
|
'''
|
|
|
|
removekmod GLOB [GLOB...] [--allbut] KEEPGLOB [KEEPGLOB...]
|
|
|
|
Remove all files and directories matching the given file globs from the kernel
|
|
|
|
modules directory.
|
|
|
|
|
|
|
|
If '--allbut' is used, all the files from the modules will be removed *except*
|
|
|
|
the ones which match the file globs. There must be at least one initial GLOB
|
|
|
|
to search and one KEEPGLOB to keep. The KEEPGLOB is expanded to be *KEEPGLOB*
|
|
|
|
so that it will match anywhere in the path.
|
|
|
|
|
|
|
|
This only removes files from under /lib/modules/*/kernel/
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
removekmod sound drivers/media drivers/hwmon drivers/video
|
|
|
|
removekmod drivers/char --allbut virtio_console hw_random
|
|
|
|
'''
|
|
|
|
cmd = " ".join(globs)
|
|
|
|
if "--allbut" in globs:
|
|
|
|
idx = globs.index("--allbut")
|
|
|
|
if idx == 0:
|
|
|
|
raise ValueError("removekmod needs at least one GLOB before --allbut")
|
|
|
|
|
|
|
|
# Apply keepglobs anywhere they appear in the path
|
|
|
|
keepglobs = globs[idx+1:]
|
|
|
|
if len(keepglobs) == 0:
|
|
|
|
raise ValueError("removekmod needs at least one GLOB after --allbut")
|
|
|
|
|
|
|
|
globs = globs[:idx]
|
|
|
|
else:
|
|
|
|
# Nothing to keep
|
|
|
|
keepglobs = []
|
|
|
|
|
|
|
|
filelist = set()
|
|
|
|
for g in globs:
|
|
|
|
for top_dir in rglob(self._out("/lib/modules/*/kernel/"+g)):
|
|
|
|
for root, _dirs, files in os.walk(top_dir):
|
|
|
|
filelist.update(root+"/"+f for f in files)
|
|
|
|
|
|
|
|
# Remove anything matching keepglobs from the list
|
|
|
|
matches = set()
|
|
|
|
for g in keepglobs:
|
|
|
|
globs_re = re.compile(fnmatch.translate("*"+g+"*"))
|
|
|
|
m = filter(globs_re.match, filelist)
|
|
|
|
if m:
|
|
|
|
matches.update(m)
|
|
|
|
else:
|
|
|
|
logger.debug("removekmod %s: no files matched!", g)
|
|
|
|
remove_files = filelist.difference(matches)
|
|
|
|
|
|
|
|
if remove_files:
|
|
|
|
logger.debug("removekmod: removing %d files", len(remove_files))
|
|
|
|
map(remove, remove_files)
|
|
|
|
else:
|
|
|
|
logger.debug("removekmod %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
|
2017-08-10 00:56:16 +00:00
|
|
|
systemctl = ['systemctl', '--root', self.outroot, '--no-reload',
|
|
|
|
cmd]
|
|
|
|
# When a unit doesn't exist systemd aborts the command. Run them one at a time.
|
2012-06-19 19:14:27 +00:00
|
|
|
# XXX for some reason 'systemctl enable/disable' always returns 1
|
2017-08-10 00:56:16 +00:00
|
|
|
for unit in units:
|
|
|
|
try:
|
|
|
|
cmd = systemctl + [unit]
|
|
|
|
runcmd(cmd)
|
|
|
|
except CalledProcessError:
|
|
|
|
pass
|