2011-05-09 14:47:25 +00:00
|
|
|
# treebuilder.py - handle arch-specific tree building stuff using templates
|
|
|
|
#
|
2014-05-09 22:13:39 +00:00
|
|
|
# Copyright (C) 2011-2014 Red Hat, Inc.
|
2011-05-09 14:47:25 +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/>.
|
|
|
|
#
|
|
|
|
# Author(s): Will Woods <wwoods@redhat.com>
|
|
|
|
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger("pylorax.treebuilder")
|
|
|
|
|
2011-06-30 17:22:39 +00:00
|
|
|
import os, re
|
2014-05-09 00:21:34 +00:00
|
|
|
from os.path import basename
|
2012-01-26 05:59:48 +00:00
|
|
|
from shutil import copytree, copy2
|
2014-05-09 00:21:34 +00:00
|
|
|
|
|
|
|
from pylorax.sysutils import joinpaths, remove
|
|
|
|
from pylorax.base import DataHolder
|
|
|
|
from pylorax.ltmpl import LoraxTemplateRunner
|
|
|
|
import pylorax.imgutils as imgutils
|
2012-08-22 22:24:49 +00:00
|
|
|
from pylorax.executils import runcmd, runcmd_output
|
2011-05-09 14:47:25 +00:00
|
|
|
|
2011-06-30 17:39:06 +00:00
|
|
|
templatemap = {
|
|
|
|
'i386': 'x86.tmpl',
|
|
|
|
'x86_64': 'x86.tmpl',
|
|
|
|
'ppc': 'ppc.tmpl',
|
|
|
|
'ppc64': 'ppc.tmpl',
|
2014-03-25 20:35:31 +00:00
|
|
|
'ppc64le': 'ppc64le.tmpl',
|
2011-06-30 17:39:06 +00:00
|
|
|
's390': 's390.tmpl',
|
|
|
|
's390x': 's390.tmpl',
|
2013-12-12 22:15:13 +00:00
|
|
|
'aarch64': 'aarch64.tmpl',
|
2012-06-21 07:31:58 +00:00
|
|
|
'arm': 'arm.tmpl',
|
|
|
|
'armhfp': 'arm.tmpl',
|
2011-06-30 17:39:06 +00:00
|
|
|
}
|
2011-05-09 14:47:25 +00:00
|
|
|
|
2011-07-01 19:42:47 +00:00
|
|
|
def generate_module_info(moddir, outfile=None):
|
|
|
|
def module_desc(mod):
|
2012-08-22 22:24:49 +00:00
|
|
|
output = runcmd_output(["modinfo", "-F", "description", mod])
|
2012-07-27 14:29:34 +00:00
|
|
|
return output.strip()
|
2011-07-01 19:42:47 +00:00
|
|
|
def read_module_set(name):
|
|
|
|
return set(l.strip() for l in open(joinpaths(moddir,name)) if ".ko" in l)
|
|
|
|
modsets = {'scsi':read_module_set("modules.block"),
|
|
|
|
'eth':read_module_set("modules.networking")}
|
|
|
|
|
|
|
|
modinfo = list()
|
2014-05-09 00:21:34 +00:00
|
|
|
for root, _dirs, files in os.walk(moddir):
|
2011-07-01 19:42:47 +00:00
|
|
|
for modtype, modset in modsets.items():
|
|
|
|
for mod in modset.intersection(files): # modules in this dir
|
2014-05-09 00:21:34 +00:00
|
|
|
(name, _ext) = os.path.splitext(mod) # foo.ko -> (foo, .ko)
|
2011-07-01 19:42:47 +00:00
|
|
|
desc = module_desc(joinpaths(root,mod)) or "%s driver" % name
|
|
|
|
modinfo.append(dict(name=name, type=modtype, desc=desc))
|
|
|
|
|
|
|
|
out = open(outfile or joinpaths(moddir,"module-info"), "w")
|
|
|
|
out.write("Version 0\n")
|
|
|
|
for mod in sorted(modinfo, key=lambda m: m.get('name')):
|
|
|
|
out.write('{name}\n\t{type}\n\t"{desc:.65}"\n'.format(**mod))
|
|
|
|
|
2011-05-14 07:27:25 +00:00
|
|
|
class RuntimeBuilder(object):
|
2011-05-26 17:35:28 +00:00
|
|
|
'''Builds the anaconda runtime image.'''
|
2014-05-02 01:34:54 +00:00
|
|
|
def __init__(self, product, arch, yum, templatedir=None,
|
|
|
|
add_templates=None,
|
|
|
|
add_template_vars=None):
|
2011-05-26 17:35:28 +00:00
|
|
|
root = yum.conf.installroot
|
2011-06-29 16:43:12 +00:00
|
|
|
# use a copy of product so we can modify it locally
|
2011-06-24 17:11:15 +00:00
|
|
|
product = product.copy()
|
2011-05-26 22:15:07 +00:00
|
|
|
product.name = product.name.lower()
|
2011-06-29 16:43:12 +00:00
|
|
|
self.vars = DataHolder(arch=arch, product=product, yum=yum, root=root,
|
|
|
|
basearch=arch.basearch, libdir=arch.libdir)
|
2011-05-26 17:07:30 +00:00
|
|
|
self.yum = yum
|
2011-08-08 23:01:38 +00:00
|
|
|
self._runner = LoraxTemplateRunner(inroot=root, outroot=root,
|
|
|
|
yum=yum, templatedir=templatedir)
|
2014-05-02 01:34:54 +00:00
|
|
|
self.add_templates = add_templates or []
|
|
|
|
self.add_template_vars = add_template_vars or {}
|
2011-06-29 16:43:12 +00:00
|
|
|
self._runner.defaults = self.vars
|
2011-05-12 21:17:50 +00:00
|
|
|
|
2012-05-21 08:42:06 +00:00
|
|
|
def _install_branding(self):
|
|
|
|
release = None
|
|
|
|
for pkg in self.yum.whatProvides('/etc/system-release', None, None):
|
|
|
|
if pkg.name.startswith('generic'):
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
release = pkg.name
|
|
|
|
break
|
|
|
|
|
|
|
|
if not release:
|
|
|
|
logger.error('could not get the release')
|
|
|
|
return
|
|
|
|
|
|
|
|
# release
|
|
|
|
logger.info('got release: %s', release)
|
|
|
|
self._runner.installpkg(release)
|
|
|
|
|
|
|
|
# logos
|
|
|
|
release, _suffix = release.split('-', 1)
|
|
|
|
self._runner.installpkg('%s-logos' % release)
|
|
|
|
|
2011-05-12 21:17:50 +00:00
|
|
|
def install(self):
|
|
|
|
'''Install packages and do initial setup with runtime-install.tmpl'''
|
2012-05-21 08:42:06 +00:00
|
|
|
self._install_branding()
|
2011-06-29 16:43:12 +00:00
|
|
|
self._runner.run("runtime-install.tmpl")
|
2014-05-02 01:34:54 +00:00
|
|
|
for tmpl in self.add_templates:
|
|
|
|
self._runner.run(tmpl, **self.add_template_vars)
|
2011-05-12 21:17:50 +00:00
|
|
|
|
2011-08-09 21:59:57 +00:00
|
|
|
def writepkglists(self, pkglistdir):
|
|
|
|
'''debugging data: write out lists of package contents'''
|
|
|
|
if not os.path.isdir(pkglistdir):
|
|
|
|
os.makedirs(pkglistdir)
|
|
|
|
for pkgobj in self.yum.doPackageLists(pkgnarrow='installed').installed:
|
|
|
|
with open(joinpaths(pkglistdir, pkgobj.name), "w") as fobj:
|
|
|
|
for fname in pkgobj.filelist + pkgobj.dirlist:
|
|
|
|
fobj.write("{0}\n".format(fname))
|
|
|
|
|
2011-08-08 23:03:53 +00:00
|
|
|
def postinstall(self):
|
2011-05-12 21:17:50 +00:00
|
|
|
'''Do some post-install setup work with runtime-postinstall.tmpl'''
|
2011-08-05 21:25:08 +00:00
|
|
|
# copy configdir into runtime root beforehand
|
2011-08-08 23:03:53 +00:00
|
|
|
configdir = joinpaths(self._runner.templatedir,"config_files")
|
2011-05-26 17:35:28 +00:00
|
|
|
configdir_path = "tmp/config_files"
|
2011-05-27 22:38:25 +00:00
|
|
|
fullpath = joinpaths(self.vars.root, configdir_path)
|
2011-05-26 22:15:07 +00:00
|
|
|
if os.path.exists(fullpath):
|
|
|
|
remove(fullpath)
|
2011-08-05 21:25:08 +00:00
|
|
|
copytree(configdir, fullpath)
|
2011-06-29 16:43:12 +00:00
|
|
|
self._runner.run("runtime-postinstall.tmpl", configdir=configdir_path)
|
2011-05-12 21:17:50 +00:00
|
|
|
|
|
|
|
def cleanup(self):
|
|
|
|
'''Remove unneeded packages and files with runtime-cleanup.tmpl'''
|
2012-09-17 14:27:13 +00:00
|
|
|
self._runner.run("runtime-cleanup.tmpl")
|
2011-05-12 21:17:50 +00:00
|
|
|
|
2011-08-09 22:18:12 +00:00
|
|
|
def writepkgsizes(self, pkgsizefile):
|
|
|
|
'''debugging data: write a big list of pkg sizes'''
|
|
|
|
fobj = open(pkgsizefile, "w")
|
|
|
|
getsize = lambda f: os.lstat(f).st_size if os.path.exists(f) else 0
|
|
|
|
for p in sorted(self.yum.doPackageLists(pkgnarrow='installed').installed):
|
|
|
|
pkgsize = sum(getsize(joinpaths(self.vars.root,f)) for f in p.filelist)
|
|
|
|
fobj.write("{0.name}.{0.arch}: {1}\n".format(p, pkgsize))
|
|
|
|
|
2011-07-01 19:42:47 +00:00
|
|
|
def generate_module_data(self):
|
|
|
|
root = self.vars.root
|
|
|
|
moddir = joinpaths(root, "lib/modules/")
|
|
|
|
for kver in os.listdir(moddir):
|
|
|
|
ksyms = joinpaths(root, "boot/System.map-%s" % kver)
|
|
|
|
logger.info("doing depmod and module-info for %s", kver)
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(["depmod", "-a", "-F", ksyms, "-b", root, kver])
|
2011-07-01 19:42:47 +00:00
|
|
|
generate_module_info(moddir+kver, outfile=moddir+"module-info")
|
|
|
|
|
2014-05-09 00:21:34 +00:00
|
|
|
def create_runtime(self, outfile="/var/tmp/squashfs.img", compression="xz", compressargs=None, size=2):
|
2011-05-18 17:53:54 +00:00
|
|
|
# make live rootfs image - must be named "LiveOS/rootfs.img" for dracut
|
2014-05-09 00:21:34 +00:00
|
|
|
compressargs = compressargs or []
|
2011-06-23 16:12:37 +00:00
|
|
|
workdir = joinpaths(os.path.dirname(outfile), "runtime-workdir")
|
2011-09-24 00:34:55 +00:00
|
|
|
if size:
|
|
|
|
fssize = size * (1024*1024*1024) # 2GB sparse file compresses down to nothin'
|
|
|
|
else:
|
|
|
|
fssize = None # Let mkext4img figure out the needed size
|
2011-05-27 22:38:25 +00:00
|
|
|
os.makedirs(joinpaths(workdir, "LiveOS"))
|
2011-05-31 15:15:16 +00:00
|
|
|
imgutils.mkext4img(self.vars.root, joinpaths(workdir, "LiveOS/rootfs.img"),
|
2011-05-18 17:53:54 +00:00
|
|
|
label="Anaconda", size=fssize)
|
2011-08-24 22:07:19 +00:00
|
|
|
|
|
|
|
# Reset selinux context on new rootfs
|
|
|
|
with imgutils.LoopDev( joinpaths(workdir, "LiveOS/rootfs.img") ) as loopdev:
|
|
|
|
with imgutils.Mount(loopdev) as mnt:
|
2014-05-02 01:34:54 +00:00
|
|
|
cmd = [ "setfiles", "-e", "/proc", "-e", "/sys", "-e", "/dev", "-e", "/install",
|
|
|
|
"/etc/selinux/targeted/contexts/files/file_contexts", "/"]
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(cmd, root=mnt)
|
2011-08-24 22:07:19 +00:00
|
|
|
|
2011-05-18 17:53:54 +00:00
|
|
|
# squash the live rootfs and clean up workdir
|
2011-07-20 20:45:00 +00:00
|
|
|
imgutils.mksquashfs(workdir, outfile, compression, compressargs)
|
2011-05-18 17:53:54 +00:00
|
|
|
remove(workdir)
|
|
|
|
|
2011-05-17 19:29:23 +00:00
|
|
|
class TreeBuilder(object):
|
2011-05-09 14:47:25 +00:00
|
|
|
'''Builds the arch-specific boot images.
|
|
|
|
inroot should be the installtree root (the newly-built runtime dir)'''
|
2012-12-18 13:31:45 +00:00
|
|
|
def __init__(self, product, arch, inroot, outroot, runtime, isolabel, domacboot=True, doupgrade=True, templatedir=None):
|
2011-06-23 14:46:28 +00:00
|
|
|
# NOTE: if you pass an arg named "runtime" to a mako template it'll
|
|
|
|
# clobber some mako internal variables - hence "runtime_img".
|
2011-06-29 16:43:12 +00:00
|
|
|
self.vars = DataHolder(arch=arch, product=product, runtime_img=runtime,
|
2012-02-07 10:03:09 +00:00
|
|
|
runtime_base=basename(runtime),
|
2011-06-29 16:43:12 +00:00
|
|
|
inroot=inroot, outroot=outroot,
|
2011-06-30 15:11:07 +00:00
|
|
|
basearch=arch.basearch, libdir=arch.libdir,
|
2012-12-18 13:31:45 +00:00
|
|
|
isolabel=isolabel, udev=udev_escape, domacboot=domacboot, doupgrade=doupgrade)
|
2011-08-08 23:01:38 +00:00
|
|
|
self._runner = LoraxTemplateRunner(inroot, outroot, templatedir=templatedir)
|
2011-06-29 16:43:12 +00:00
|
|
|
self._runner.defaults = self.vars
|
2012-01-26 05:59:48 +00:00
|
|
|
self.templatedir = templatedir
|
2014-05-09 00:21:34 +00:00
|
|
|
self.treeinfo_data = None
|
2011-05-14 07:27:25 +00:00
|
|
|
|
2011-05-31 18:36:59 +00:00
|
|
|
@property
|
|
|
|
def kernels(self):
|
|
|
|
return findkernels(root=self.vars.inroot)
|
|
|
|
|
2014-05-09 00:21:34 +00:00
|
|
|
def rebuild_initrds(self, add_args=None, backup="", prefix=""):
|
2011-05-09 14:47:25 +00:00
|
|
|
'''Rebuild all the initrds in the tree. If backup is specified, each
|
|
|
|
initrd will be renamed with backup as a suffix before rebuilding.
|
2012-11-13 06:33:14 +00:00
|
|
|
If backup is empty, the existing initrd files will be overwritten.
|
|
|
|
If suffix is specified, the existing initrd is untouched and a new
|
|
|
|
image is built with the filename "${prefix}-${kernel.version}.img"
|
|
|
|
'''
|
2014-05-09 00:21:34 +00:00
|
|
|
add_args = add_args or []
|
2012-07-06 04:17:32 +00:00
|
|
|
dracut = ["dracut", "--nomdadmconf", "--nolvmconf"] + add_args
|
2011-05-09 14:47:25 +00:00
|
|
|
if not backup:
|
|
|
|
dracut.append("--force")
|
2012-01-26 05:59:48 +00:00
|
|
|
|
2013-03-12 22:39:25 +00:00
|
|
|
kernels = [kernel for kernel in self.kernels if hasattr(kernel, "initrd")]
|
|
|
|
if not kernels:
|
|
|
|
raise Exception("No initrds found, cannot rebuild_initrds")
|
|
|
|
|
2011-07-06 16:22:49 +00:00
|
|
|
# Hush some dracut warnings. TODO: bind-mount proc in place?
|
|
|
|
open(joinpaths(self.vars.inroot,"/proc/modules"),"w")
|
2013-03-12 22:39:25 +00:00
|
|
|
for kernel in kernels:
|
2012-11-13 06:33:14 +00:00
|
|
|
if prefix:
|
|
|
|
idir = os.path.dirname(kernel.initrd.path)
|
|
|
|
outfile = joinpaths(idir, prefix+'-'+kernel.version+'.img')
|
|
|
|
else:
|
|
|
|
outfile = kernel.initrd.path
|
|
|
|
logger.info("rebuilding %s", outfile)
|
2011-05-09 14:47:25 +00:00
|
|
|
if backup:
|
2012-11-13 06:33:14 +00:00
|
|
|
initrd = joinpaths(self.vars.inroot, outfile)
|
2011-05-09 14:47:25 +00:00
|
|
|
os.rename(initrd, initrd + backup)
|
2012-11-13 06:33:14 +00:00
|
|
|
cmd = dracut + [outfile, kernel.version]
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(cmd, root=self.vars.inroot)
|
2014-02-13 17:29:26 +00:00
|
|
|
|
|
|
|
# ppc64 cannot boot images > 32MiB, check size and warn
|
2014-09-02 17:58:48 +00:00
|
|
|
if self.vars.arch.basearch in ("ppc64", "ppc64le") and os.path.exists(outfile):
|
2014-02-13 17:29:26 +00:00
|
|
|
st = os.stat(outfile)
|
|
|
|
if st.st_size > 32 * 1024 * 1024:
|
|
|
|
logging.warning("ppc64 initrd %s is > 32MiB", outfile)
|
|
|
|
|
2011-07-06 16:22:49 +00:00
|
|
|
os.unlink(joinpaths(self.vars.inroot,"/proc/modules"))
|
2011-05-09 14:47:25 +00:00
|
|
|
|
2011-06-30 17:39:06 +00:00
|
|
|
def build(self):
|
|
|
|
templatefile = templatemap[self.vars.arch.basearch]
|
|
|
|
self._runner.run(templatefile, kernels=self.kernels)
|
2011-06-30 22:13:24 +00:00
|
|
|
self.treeinfo_data = self._runner.results.treeinfo
|
2011-06-30 17:39:06 +00:00
|
|
|
self.implantisomd5()
|
|
|
|
|
2011-05-09 14:47:25 +00:00
|
|
|
def implantisomd5(self):
|
2014-05-09 00:21:34 +00:00
|
|
|
for _section, data in self.treeinfo_data.items():
|
2011-05-09 14:47:25 +00:00
|
|
|
if 'boot.iso' in data:
|
2011-05-27 22:38:25 +00:00
|
|
|
iso = joinpaths(self.vars.outroot, data['boot.iso'])
|
2012-08-22 22:24:49 +00:00
|
|
|
runcmd(["implantisomd5", iso])
|
2011-06-30 17:39:06 +00:00
|
|
|
|
2012-01-26 05:59:48 +00:00
|
|
|
@property
|
|
|
|
def dracut_hooks_path(self):
|
|
|
|
""" Return the path to the lorax dracut hooks scripts
|
|
|
|
|
|
|
|
Use the configured share dir if it is setup,
|
|
|
|
otherwise default to /usr/share/lorax/dracut_hooks
|
|
|
|
"""
|
|
|
|
if self.templatedir:
|
|
|
|
return joinpaths(self.templatedir, "dracut_hooks")
|
|
|
|
else:
|
|
|
|
return "/usr/share/lorax/dracut_hooks"
|
|
|
|
|
|
|
|
def copy_dracut_hooks(self, hooks):
|
|
|
|
""" Copy the hook scripts in hooks into the installroot's /tmp/
|
|
|
|
and return a list of commands to pass to dracut when creating the
|
|
|
|
initramfs
|
|
|
|
|
|
|
|
hooks is a list of tuples with the name of the hook script and the
|
|
|
|
target dracut hook directory
|
|
|
|
(eg. [("99anaconda-copy-ks.sh", "/lib/dracut/hooks/pre-pivot")])
|
|
|
|
"""
|
|
|
|
dracut_commands = []
|
|
|
|
for hook_script, dracut_path in hooks:
|
|
|
|
src = joinpaths(self.dracut_hooks_path, hook_script)
|
|
|
|
if not os.path.exists(src):
|
2014-05-09 00:21:34 +00:00
|
|
|
logger.error("Missing lorax dracut hook script %s", (src))
|
2012-01-26 05:59:48 +00:00
|
|
|
continue
|
|
|
|
dst = joinpaths(self.vars.inroot, "/tmp/", hook_script)
|
|
|
|
copy2(src, dst)
|
|
|
|
dracut_commands += ["--include", joinpaths("/tmp/", hook_script),
|
|
|
|
dracut_path]
|
|
|
|
return dracut_commands
|
|
|
|
|
2011-06-30 17:39:06 +00:00
|
|
|
#### TreeBuilder helper functions
|
|
|
|
|
|
|
|
def findkernels(root="/", kdir="boot"):
|
|
|
|
# To find possible flavors, awk '/BuildKernel/ { print $4 }' kernel.spec
|
2013-10-07 20:23:35 +00:00
|
|
|
flavors = ('debug', 'PAE', 'PAEdebug', 'smp', 'xen', 'lpae')
|
2011-06-30 17:39:06 +00:00
|
|
|
kre = re.compile(r"vmlinuz-(?P<version>.+?\.(?P<arch>[a-z0-9_]+)"
|
2013-09-05 03:16:08 +00:00
|
|
|
r"(.(?P<flavor>{0}))?)$".format("|".join(flavors)))
|
2011-06-30 17:39:06 +00:00
|
|
|
kernels = []
|
2012-11-13 06:33:15 +00:00
|
|
|
bootfiles = os.listdir(joinpaths(root, kdir))
|
|
|
|
for f in bootfiles:
|
2011-06-30 17:39:06 +00:00
|
|
|
match = kre.match(f)
|
|
|
|
if match:
|
|
|
|
kernel = DataHolder(path=joinpaths(kdir, f))
|
|
|
|
kernel.update(match.groupdict()) # sets version, arch, flavor
|
|
|
|
kernels.append(kernel)
|
|
|
|
|
2012-11-13 06:33:15 +00:00
|
|
|
# look for associated initrd/initramfs/etc.
|
2011-06-30 17:39:06 +00:00
|
|
|
for kernel in kernels:
|
2012-11-13 06:33:15 +00:00
|
|
|
for f in bootfiles:
|
|
|
|
if f.endswith('-'+kernel.version+'.img'):
|
2014-05-09 00:21:34 +00:00
|
|
|
imgtype, _rest = f.split('-',1)
|
2012-11-13 06:33:15 +00:00
|
|
|
# special backwards-compat case
|
|
|
|
if imgtype == 'initramfs':
|
|
|
|
imgtype = 'initrd'
|
|
|
|
kernel[imgtype] = DataHolder(path=joinpaths(kdir, f))
|
2011-06-30 17:39:06 +00:00
|
|
|
|
2014-05-09 00:21:34 +00:00
|
|
|
logger.debug("kernels=%s", kernels)
|
2011-06-30 17:39:06 +00:00
|
|
|
return kernels
|
|
|
|
|
|
|
|
# udev whitelist: 'a-zA-Z0-9#+.:=@_-' (see is_whitelisted in libudev-util.c)
|
|
|
|
udev_blacklist=' !"$%&\'()*,/;<>?[\\]^`{|}~' # ASCII printable, minus whitelist
|
|
|
|
udev_blacklist += ''.join(chr(i) for i in range(32)) # ASCII non-printable
|
|
|
|
def udev_escape(label):
|
|
|
|
out = u''
|
|
|
|
for ch in label.decode('utf8'):
|
|
|
|
out += ch if ch not in udev_blacklist else u'\\x%02x' % ord(ch)
|
|
|
|
return out.encode('utf8')
|