588 lines
20 KiB
Python
588 lines
20 KiB
Python
#
|
|
# __init__.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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# Red Hat Author(s): Martin Gracik <mgracik@redhat.com>
|
|
# David Cantrell <dcantrell@redhat.com>
|
|
#
|
|
|
|
import sys
|
|
import os
|
|
import ConfigParser
|
|
import glob
|
|
import time
|
|
import datetime
|
|
import shutil
|
|
|
|
import yum
|
|
import yum.callbacks
|
|
import yum.rpmtrans
|
|
|
|
from base import BaseLoraxClass
|
|
import output
|
|
|
|
import insttree
|
|
import images
|
|
from sysutils import *
|
|
|
|
|
|
# basearch efiarch 64bit
|
|
ARCHMAP = {"i386": ["i386", "IA32", False],
|
|
"i586": ["i386", "IA32", False],
|
|
"i686": ["i386", "IA32", False],
|
|
"x86_64": ["x86_64", "X64", True],
|
|
"ppc": ["ppc", "", False],
|
|
"ppc64": ["ppc", "", True],
|
|
"s390": ["s390", "", False],
|
|
"s390x": ["s390x", "", True],
|
|
"sparc": ["sparc", "", False],
|
|
"sparc64": ["sparc", "", True],
|
|
"ia64": ["ia64", "IA64", True]}
|
|
|
|
LIB32 = "lib"
|
|
LIB64 = "lib64"
|
|
|
|
|
|
class Lorax(BaseLoraxClass):
|
|
|
|
def __init__(self, yb, installtree, outputdir,
|
|
product, version, release,
|
|
workdir="/tmp", variant="", bugurl="", updatesdir=None):
|
|
|
|
BaseLoraxClass.__init__(self)
|
|
|
|
# XXX check if we have root privileges
|
|
assert os.geteuid() == self.const.ROOT_UID, "no root privileges"
|
|
|
|
# XXX check if we have a yumbase object
|
|
assert isinstance(yb, yum.YumBase), "not an yum base object"
|
|
|
|
# setup yum and the install tree
|
|
self.yum = YumHelper(yb)
|
|
self.installtree = insttree.InstallTree(yum=self.yum,
|
|
rootdir=installtree,
|
|
updatesdir=updatesdir)
|
|
|
|
# create the output directory
|
|
assert not os.path.isdir(outputdir), "output directory exists"
|
|
self.outputdir = outputdir
|
|
makedirs_(self.outputdir)
|
|
|
|
# required parameters
|
|
self.product = product
|
|
self.version = version
|
|
self.release = release
|
|
|
|
# create the working director
|
|
self.workdir = workdir
|
|
makedirs_(self.workdir)
|
|
|
|
# optional parameters
|
|
self.variant = variant
|
|
self.bugurl = bugurl
|
|
|
|
# setup the output
|
|
output_level = output.INFO
|
|
if self.conf.debug:
|
|
output_level = output.DEBUG
|
|
|
|
self.output.basic_config(colors=self.conf.colors,
|
|
encoding=self.conf.encoding,
|
|
output_level=output_level)
|
|
|
|
ignore_errors = set()
|
|
if os.path.isfile(self.conf.ignore_errors):
|
|
with open(self.conf.ignore_errors, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith("#"):
|
|
ignore_errors.add(line)
|
|
|
|
self.output.ignore = ignore_errors
|
|
|
|
def run(self):
|
|
# check if we have all required commands
|
|
for cmd in self.cmd.values():
|
|
if not os.path.isfile(cmd):
|
|
err = "required command <b>{0}</b> does not exist"
|
|
self.pwarning(err.format(cmd))
|
|
|
|
# set the build architecture
|
|
self.pinfo(":: setting the build architecture")
|
|
self.conf.buildarch = self.get_buildarch()
|
|
basearch, efiarch, is64 = ARCHMAP[self.conf.buildarch]
|
|
|
|
self.conf.basearch = basearch
|
|
self.conf.efiarch = efiarch
|
|
|
|
# set the libdir
|
|
self.conf.libdir = LIB32
|
|
if is64:
|
|
self.conf.libdir = LIB64
|
|
|
|
# read the configuration files
|
|
self.pinfo(":: reading the configuration files")
|
|
c = self.get_config()
|
|
packages, modules, initrd_template, install_template = c
|
|
|
|
# add the branding
|
|
packages.add("{0}-logos".format(self.product.lower()))
|
|
packages.add("{0}-release".format(self.product.lower()))
|
|
|
|
# prepare the install tree
|
|
self.pinfo(":: preparing the install tree")
|
|
self.installtree.install_packages(packages=packages)
|
|
self.installtree.run_ldconfig()
|
|
self.installtree.copy_updates()
|
|
self.installtree.fix_problems()
|
|
|
|
# check the anaconda runtime directory
|
|
anarun = os.path.join(self.installtree.rootdir,
|
|
self.const.ANACONDA_RUNTIME)
|
|
|
|
if not os.path.isdir(anarun):
|
|
self.pcritical("no anaconda runtime directory found")
|
|
sys.exit(1)
|
|
|
|
# prepare the output directory
|
|
self.pinfo(":: preparing the output directory")
|
|
ok = self.prepare_outputdir()
|
|
if not ok:
|
|
self.pcritical("unable to prepare the output directory")
|
|
sys.exit(1)
|
|
|
|
# write the treeinfo, discinfo and buildstamp
|
|
self.pinfo(":: creating the treeinfo, discinfo and buildstamp")
|
|
self.conf.treeinfo = self.write_treeinfo()
|
|
self.conf.discinfo = self.write_discinfo()
|
|
self.conf.buildstamp = self.write_buildstamp()
|
|
|
|
# create the initrd images for all kernels in install tree
|
|
initrd = images.InitRD(self.installtree, modules, initrd_template,
|
|
self.workdir)
|
|
|
|
for kernel, initrd in initrd.create():
|
|
if kernel.is_pae:
|
|
kernelfile = "vmlinuz-PAE"
|
|
initrdfile = "initrd-PAE.img"
|
|
|
|
# add images section to treeinfo
|
|
section = "images-xen"
|
|
data = {"kernel": "images/pxeboot/vmlinuz-PAE",
|
|
"initrd": "images/pxeboot/initrd-PAE.img"}
|
|
self.treeinfo_add_section(self.conf.treeinfo, section, data)
|
|
elif kernel.is_xen:
|
|
kernelfile = "vmlinuz-xen"
|
|
initrdfile = "initrd-xen.img"
|
|
else:
|
|
kernelfile = "vmlinuz"
|
|
initrdfile = "initrd.img"
|
|
|
|
# add images section to treeinfo
|
|
section = "images-{0}".format(self.conf.basearch)
|
|
data = {"kernel": "images/pxeboot/vmlinuz",
|
|
"initrd": "images/pxeboot/initrd.img"}
|
|
self.treeinfo_add_section(self.conf.treeinfo, section, data)
|
|
|
|
# copy the kernel and initrd image to the isolinux directory
|
|
kdst = os.path.join(self.conf.isodir, kernelfile)
|
|
idst = os.path.join(self.conf.isodir, initrdfile)
|
|
shutil.copy2(kernel.path, kdst)
|
|
shutil.copy2(initrd, idst)
|
|
|
|
# create the efi images
|
|
if self.installtree.do_efi:
|
|
efi = images.EFI(self.installtree, kernel, initrd,
|
|
self.product, self.version, self.workdir)
|
|
|
|
efiboot, efidisk = efi.create()
|
|
|
|
# copy the efi images to the images directory
|
|
shutil.copy2(efiboot, self.conf.imgdir)
|
|
shutil.copy2(efidisk, self.conf.imgdir)
|
|
|
|
# copy the kernel and initrd image to the pxe directory
|
|
kdst = os.path.join(self.conf.pxedir, kernelfile)
|
|
idst = os.path.join(self.conf.pxedir, initrdfile)
|
|
shutil.copy2(kernel.path, kdst)
|
|
shutil.copy2(initrd, idst)
|
|
|
|
# create the install image
|
|
install = images.Install(self.installtree, install_template,
|
|
self.workdir)
|
|
|
|
installimg = install.create()
|
|
if not installimg:
|
|
self.perror("unable to create the install image")
|
|
sys.exit(1)
|
|
|
|
# add stage2 section to the treeinfo
|
|
section = "stage2"
|
|
data = {"mainimage": "images/install.img"}
|
|
self.treeinfo_add_section(self.conf.treeinfo, section, data)
|
|
|
|
# move the install image to the images directory
|
|
shutil.move(installimg, self.conf.imgdir)
|
|
|
|
# create the boot iso image
|
|
boot = images.Boot(self.product, self.workdir)
|
|
bootiso = boot.create()
|
|
|
|
# add boot iso to the images section in treeinfo
|
|
section = "images-{0}".format(self.conf.basearch)
|
|
data = {"boot.iso": "images/boot.iso"}
|
|
self.treeinfo_add_section(self.conf.treeinfo, section, data)
|
|
|
|
# move the boot iso to the images directory
|
|
shutil.move(bootiso, self.conf.imgdir)
|
|
|
|
# copy the treeinfo and discinfo to the output directory
|
|
shutil.copy2(self.conf.treeinfo, self.outputdir)
|
|
shutil.copy2(self.conf.discinfo, self.outputdir)
|
|
|
|
# cleanup
|
|
self.cleanup()
|
|
|
|
def get_buildarch(self):
|
|
# get the architecture of the anaconda package
|
|
installed, available = self.yum.search(self.const.ANACONDA_PACKAGE)
|
|
|
|
if installed:
|
|
anaconda = installed[0]
|
|
if available:
|
|
anaconda = available[0]
|
|
|
|
try:
|
|
buildarch = anaconda.arch
|
|
except:
|
|
# fallback to the system architecture
|
|
self.pwarning("using system architecture")
|
|
buildarch = os.uname()[4]
|
|
|
|
return buildarch
|
|
|
|
def get_config(self):
|
|
generic = os.path.join(self.conf.confdir, "config.noarch")
|
|
specific = os.path.join(self.conf.confdir,
|
|
"config.{0}".format(self.conf.basearch))
|
|
|
|
packages, modules = set(), set()
|
|
initrd_template, install_template = None, None
|
|
|
|
for f in (generic, specific):
|
|
if not os.path.isfile(f):
|
|
continue
|
|
|
|
c = ConfigParser.ConfigParser()
|
|
c.read(f)
|
|
|
|
if c.has_option("lorax", "packages"):
|
|
list = c.get("lorax", "packages").split()
|
|
for name in list:
|
|
if name.startswith("-"):
|
|
packages.discard(name)
|
|
else:
|
|
packages.add(name)
|
|
|
|
if c.has_option("lorax", "modules"):
|
|
list = c.get("lorax", "modules").split()
|
|
for name in list:
|
|
if name.startswith("-"):
|
|
modules.discard(name)
|
|
else:
|
|
modules.add(name)
|
|
|
|
if c.has_option("lorax", "initrd_template"):
|
|
initrd_template = c.get("lorax", "initrd_template")
|
|
initrd_template = os.path.join(self.conf.confdir,
|
|
initrd_template)
|
|
|
|
if c.has_option("lorax", "install_template"):
|
|
install_template = c.get("lorax", "install_template")
|
|
install_template = os.path.join(self.conf.confdir,
|
|
install_template)
|
|
|
|
return packages, modules, initrd_template, install_template
|
|
|
|
def prepare_outputdir(self):
|
|
imgdir = os.path.join(self.outputdir, "images")
|
|
makedirs_(imgdir)
|
|
|
|
# write the images/README file
|
|
text = """
|
|
This directory contains image files that can be used to create media
|
|
capable of starting the {0} installation process.
|
|
|
|
The boot.iso file is an ISO 9660 image of a bootable CD-ROM. It is useful
|
|
in cases where the CD-ROM installation method is not desired, but the
|
|
CD-ROM's boot speed would be an advantage.
|
|
|
|
To use this image file, burn the file onto CD-R (or CD-RW) media as you
|
|
normally would.
|
|
"""
|
|
|
|
text = text.format(self.product)
|
|
with open(os.path.join(imgdir, "README"), "w") as f:
|
|
f.write(text)
|
|
|
|
pxedir = os.path.join(imgdir, "pxeboot")
|
|
makedirs_(pxedir)
|
|
|
|
# write the images/pxeboot/README file
|
|
text = """
|
|
The files in this directory are useful for booting a machine via PXE.
|
|
|
|
The following files are available:
|
|
vmlinuz - the kernel used for the installer
|
|
initrd.img - an initrd with support for all install methods and
|
|
drivers supported for installation of {0}
|
|
"""
|
|
|
|
text = text.format(self.product)
|
|
with open(os.path.join(pxedir, "README"), "w") as f:
|
|
f.write(text)
|
|
|
|
efidir = os.path.join(self.outputdir, "EFI/BOOT")
|
|
makedirs_(efidir)
|
|
|
|
isodir = os.path.join(self.outputdir, "isolinux")
|
|
makedirs_(isodir)
|
|
|
|
self.conf.imgdir = imgdir
|
|
self.conf.pxedir = pxedir
|
|
self.conf.efidir = efidir
|
|
self.conf.isodir = isodir
|
|
|
|
isolinuxbin = os.path.join(self.installtree.rootdir,
|
|
self.const.ISOLINUXBIN)
|
|
syslinuxcfg = os.path.join(self.installtree.rootdir,
|
|
self.const.SYSLINUXCFG)
|
|
|
|
if not os.path.isfile(isolinuxbin):
|
|
self.perror("no isolinux binary found")
|
|
return False
|
|
|
|
# copy the isolinux.bin file
|
|
shutil.copy2(isolinuxbin, self.conf.isodir)
|
|
|
|
# copy the syslinux.cfg
|
|
isolinuxcfg = os.path.join(self.conf.isodir, "isolinux.cfg")
|
|
shutil.copy2(syslinuxcfg, isolinuxcfg)
|
|
|
|
# set the product and version in isolinux.cfg
|
|
replace_(isolinuxcfg, r"@PRODUCT@", self.product)
|
|
replace_(isolinuxcfg, r"@VERSION@", self.version)
|
|
|
|
# set up the label for finding stage2 with a hybrid iso
|
|
replace_(isolinuxcfg, r"initrd=initrd.img",
|
|
'initrd=initrd.img stage2=hd:LABEL="{0}"'.format(self.product))
|
|
|
|
# copy the .msg files
|
|
msgfiles = os.path.join(self.const.ANACONDA_BOOTDIR, "*.msg")
|
|
msgfiles = os.path.join(self.installtree.rootdir, msgfiles)
|
|
for fname in glob.iglob(msgfiles):
|
|
shutil.copy2(fname, self.conf.isodir)
|
|
path = os.path.join(self.conf.isodir, os.path.basename(fname))
|
|
replace_(path, r"@VERSION@", self.version)
|
|
|
|
# copy the memtest
|
|
memtest = os.path.join(self.const.BOOTDIR, "memtest*")
|
|
memtest = os.path.join(self.installtree.rootdir, memtest)
|
|
for fname in glob.iglob(memtest):
|
|
shutil.copy2(fname, os.path.join(self.conf.isodir, "memtest"))
|
|
|
|
text = """label memtest86
|
|
menu label ^Memory test
|
|
kernel memtest
|
|
append -
|
|
|
|
"""
|
|
|
|
with open(isolinuxcfg, "a") as f:
|
|
f.write(text)
|
|
|
|
break
|
|
|
|
# copy the grub.conf
|
|
grubconf = os.path.join(self.installtree.rootdir,
|
|
self.const.ANACONDA_BOOTDIR, "grub.conf")
|
|
shutil.copy2(grubconf, isodir)
|
|
|
|
# copy the splash files
|
|
vesasplash = os.path.join(self.installtree.rootdir,
|
|
self.const.VESASPLASH)
|
|
vesamenu = os.path.join(self.installtree.rootdir,
|
|
self.const.VESAMENU)
|
|
|
|
splashtools = os.path.join(self.installtree.rootdir,
|
|
self.const.SPLASHTOOLS)
|
|
syslinuxsplash = os.path.join(self.installtree.rootdir,
|
|
self.const.SYSLINUXSPLASH)
|
|
splashlss = os.path.join(self.installtree.rootdir,
|
|
self.const.SPLASHLSS)
|
|
|
|
if os.path.isfile(vesasplash):
|
|
shutil.copy2(vesasplash, os.path.join(isodir, "splash.jpg"))
|
|
shutil.copy2(vesamenu, isodir)
|
|
replace_(isolinuxcfg, r"default linux", "default vesamenu.c32")
|
|
replace_(isolinuxcfg, r"prompt 1", "#prompt 1")
|
|
else:
|
|
if os.path.isfile(splashtools):
|
|
cmd = "{0} {1} {2}".format(splashtools,
|
|
syslinuxsplash,
|
|
splashlss)
|
|
|
|
err, stdout = commands.getstatusoutput(cmd)
|
|
if err:
|
|
self.pwarning(stdout)
|
|
|
|
if os.path.isfile(splashlss):
|
|
shutil.copy2(splashlss, isodir)
|
|
|
|
return True
|
|
|
|
def write_treeinfo(self, discnum=1, totaldiscs=1, packagedir=""):
|
|
outfile = os.path.join(self.workdir, ".treeinfo")
|
|
|
|
c = ConfigParser.ConfigParser()
|
|
|
|
variant = self.variant
|
|
if variant is None:
|
|
variant = ""
|
|
|
|
section = "general"
|
|
data = {"timestamp": time.time(),
|
|
"family": self.product,
|
|
"version": self.version,
|
|
"variant": variant,
|
|
"arch": self.conf.basearch,
|
|
"discnum": discnum,
|
|
"totaldiscs": totaldiscs,
|
|
"packagedir": packagedir}
|
|
|
|
c.add_section(section)
|
|
map(lambda (key, value): c.set(section, key, value), data.items())
|
|
|
|
with open(outfile, "w") as f:
|
|
c.write(f)
|
|
|
|
return outfile
|
|
|
|
def treeinfo_add_section(self, treeinfo, section, data):
|
|
c = ConfigParser.ConfigParser()
|
|
c.read(treeinfo)
|
|
|
|
if not c.has_section(section):
|
|
c.add_section(section)
|
|
|
|
map(lambda (key, value): c.set(section, key, value), data.items())
|
|
|
|
with open(treeinfo, "w") as f:
|
|
c.write(f)
|
|
|
|
def write_discinfo(self, discnum="ALL"):
|
|
outfile = os.path.join(self.workdir, ".discinfo")
|
|
|
|
with open(outfile, "w") as f:
|
|
f.write("{0:f}\n".format(time.time()))
|
|
f.write("{0}\n".format(self.release))
|
|
f.write("{0}\n".format(self.conf.basearch))
|
|
f.write("{0}\n".format(discnum))
|
|
|
|
return outfile
|
|
|
|
def write_buildstamp(self):
|
|
outfile = os.path.join(self.workdir, ".buildstamp")
|
|
|
|
now = datetime.datetime.now()
|
|
uuid = "{0}.{1}"
|
|
uuid = uuid.format(now.strftime("%Y%m%d%H%M"), self.conf.buildarch)
|
|
|
|
with open(outfile, "w") as f:
|
|
f.write("{0}\n".format(uuid))
|
|
f.write("{0}\n".format(self.product))
|
|
f.write("{0}\n".format(self.version))
|
|
f.write("{0}\n".format(self.bugurl))
|
|
|
|
return outfile
|
|
|
|
def cleanup(self):
|
|
# TODO
|
|
pass
|
|
|
|
|
|
class YumHelper(object):
|
|
|
|
def __init__(self, yb):
|
|
self.yb = yb
|
|
|
|
def install(self, pattern):
|
|
try:
|
|
self.yb.install(name=pattern)
|
|
except yum.Errors.InstallError:
|
|
try:
|
|
self.yb.install(pattern=pattern)
|
|
except yum.Errors.InstallError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def process_transaction(self):
|
|
self.yb.resolveDeps()
|
|
self.yb.buildTransaction()
|
|
|
|
cb = yum.callbacks.ProcessTransBaseCallback()
|
|
rpmcb = RpmCallback()
|
|
|
|
self.yb.processTransaction(callback=cb, rpmDisplay=rpmcb)
|
|
|
|
self.yb.closeRpmDB()
|
|
self.yb.close()
|
|
|
|
def search(self, pattern):
|
|
pl = self.yb.doPackageLists(patterns=[pattern])
|
|
return pl.installed, pl.available
|
|
|
|
|
|
class RpmCallback(yum.rpmtrans.SimpleCliCallBack):
|
|
|
|
def __init__(self):
|
|
yum.rpmtrans.SimpleCliCallBack.__init__(self)
|
|
self.output = output.LoraxOutput()
|
|
|
|
self.termwidth = 79
|
|
|
|
def event(self, package, action, te_current, te_total,
|
|
ts_current, ts_total):
|
|
|
|
info = "({0:3d}/{1:3d}) [{2:3.0f}%] {3} "
|
|
info = info.format(ts_current, ts_total,
|
|
float(te_current) / float(te_total) * 100,
|
|
self.action[action])
|
|
|
|
pkg = "{0}".format(package)
|
|
|
|
infolen = len(info)
|
|
pkglen = len(pkg)
|
|
if (infolen + pkglen) > self.termwidth:
|
|
pkg = "{0}...".format(pkg[:self.termwidth-infolen-3])
|
|
|
|
msg = "{0}<b>{1}</b>\r".format(info, pkg)
|
|
self.output.write(msg)
|
|
if te_current == te_total:
|
|
self.output.write("\n")
|