#
# Copyright (C) 2011-2017 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/>.
#
import logging
log = logging.getLogger("pylorax")
import os
import glob
import shutil
import sys
import subprocess
import tempfile
from time import sleep
import uuid
from pylorax.executils import execWithRedirect, execWithCapture
from pylorax.imgutils import get_loop_name, dm_detach, mount, umount
from pylorax.imgutils import PartitionMount, mksparse, mkext4img, loop_detach
from pylorax.imgutils import mktar, mkdiskfsimage, mkqcow2
from pylorax.logmonitor import LogMonitor
from pylorax.sysutils import joinpaths
from pylorax.treebuilder import udev_escape
ROOT_PATH = "/mnt/sysimage/"
# no-virt mode doesn't need libvirt, so make it optional
try:
import libvirt
except ImportError:
libvirt = None
[docs]class InstallError(Exception):
pass
[docs]class IsoMountpoint(object):
"""
Mount the iso on a temporary directory and check to make sure the
vmlinuz and initrd.img files exist
Check the iso for a LiveOS directory and set a flag.
Extract the iso's label.
initrd_path can be used to point to a boot.iso tree with a newer
initrd.img than the iso has. The iso is still used for stage2.
"""
def __init__( self, iso_path, initrd_path=None ):
""" iso_path is the path to a boot.iso
initrd_path overrides mounting the iso for access to
initrd and vmlinuz.
"""
self.label = None
self.iso_path = iso_path
self.initrd_path = initrd_path
if not self.initrd_path:
self.mount_dir = mount(self.iso_path, opts="loop")
else:
self.mount_dir = self.initrd_path
kernel_list = [("/isolinux/vmlinuz", "/isolinux/initrd.img"),
("/ppc/ppc64/vmlinuz", "/ppc/ppc64/initrd.img"),
("/images/pxeboot/vmlinuz", "/images/pxeboot/initrd.img")]
if os.path.isdir( self.mount_dir+"/repodata" ):
self.repo = self.mount_dir
else:
self.repo = None
self.liveos = os.path.isdir( self.mount_dir+"/LiveOS" )
try:
for kernel, initrd in kernel_list:
if (os.path.isfile(self.mount_dir+kernel) and
os.path.isfile(self.mount_dir+initrd)):
self.kernel = self.mount_dir+kernel
self.initrd = self.mount_dir+initrd
break
else:
raise Exception("Missing kernel and initrd file in iso, failed"
" to search under: {0}".format(kernel_list))
except:
self.umount()
raise
self.get_iso_label()
[docs] def umount( self ):
if not self.initrd_path:
umount(self.mount_dir)
[docs] def get_iso_label( self ):
"""
Get the iso's label using isoinfo
"""
isoinfo_output = execWithCapture("isoinfo", ["-d", "-i", self.iso_path])
log.debug( isoinfo_output )
for line in isoinfo_output.splitlines():
if line.startswith("Volume id: "):
self.label = line[11:]
return
[docs]class VirtualInstall( object ):
"""
Run virt-install using an iso and kickstart(s)
"""
def __init__( self, iso, ks_paths, disk_img, img_size=2,
kernel_args=None, memory=1024, vnc=None, arch=None,
cancel_func=None, virtio_host="127.0.0.1", virtio_port=6080,
qcow2=False, boot_uefi=False, ovmf_path=None):
"""
Start the installation
:param iso: Information about the iso to use for the installation
:type iso: IsoMountpoint
:param list ks_paths: Paths to kickstart files. All are injected, the
first one is the one executed.
:param str disk_img: Path to a disk image, created it it doesn't exist
:param int img_size: The image size, in MiB, to create if it doesn't exist
:param str kernel_args: Extra kernel arguments to pass on the kernel cmdline
:param int memory: Amount of RAM to assign to the virt, in MiB
:param str vnc: Arguments to pass to virt-install --graphics
:param str arch: Optional architecture to use in the virt
:param log_check: Method that returns True if the installation fails
:type log_check: method
:param str virtio_host: Hostname to connect virtio log to
:param int virtio_port: Port to connect virtio log to
:param bool qcow2: Set to True if disk_img is a qcow2
:param bool boot_uefi: Use OVMF to boot the VM in UEFI mode
:param str ovmf_path: Path to the OVMF firmware
"""
self.virt_name = "LiveOS-"+str(uuid.uuid4())
# add --graphics none later
# add whatever serial cmds are needed later
args = ["-n", self.virt_name,
"-r", str(memory),
"--noreboot",
"--noautoconsole"]
args.append("--graphics")
if vnc:
args.append(vnc)
else:
args.append("none")
for ks in ks_paths:
args.append("--initrd-inject")
args.append(ks)
disk_opts = "path={0}".format(disk_img)
if qcow2:
disk_opts += ",format=qcow2"
else:
disk_opts += ",format=raw"
if not os.path.isfile(disk_img):
disk_opts += ",size={0}".format(img_size)
args.append("--disk")
args.append(disk_opts)
if iso.liveos:
disk_opts = "path={0},device=cdrom".format(iso.iso_path)
args.append("--disk")
args.append(disk_opts)
extra_args = "ks=file:/{0}".format(os.path.basename(ks_paths[0]))
if not vnc:
extra_args += " inst.cmdline console=ttyS0"
if kernel_args:
extra_args += " "+kernel_args
if iso.liveos:
extra_args += " stage2=hd:LABEL={0}".format(udev_escape(iso.label))
args.append("--extra-args")
args.append(extra_args)
args.append("--location")
args.append(iso.mount_dir)
channel_args = "tcp,host={0}:{1},mode=connect,target_type=virtio" \
",name=org.fedoraproject.anaconda.log.0".format(
virtio_host, virtio_port)
args.append("--channel")
args.append(channel_args)
if arch:
args.append("--arch")
args.append(arch)
if boot_uefi and ovmf_path:
args.append("--boot")
args.append("loader=%s/OVMF_CODE.fd,loader_ro=yes,loader_type=pflash,nvram_template=%s/OVMF_VARS.fd,loader_secure=no" % (ovmf_path, ovmf_path))
log.info("Running virt-install.")
try:
execWithRedirect("virt-install", args, raise_err=True)
except subprocess.CalledProcessError as e:
raise InstallError("Problem starting virtual install: %s" % e)
conn = libvirt.openReadOnly(None)
dom = conn.lookupByName(self.virt_name)
# TODO: If vnc has been passed, we should look up the port and print that
# for the user at this point
while dom.isActive() and not (cancel_func and cancel_func()):
sys.stdout.write(".")
sys.stdout.flush()
sleep(10)
print
if cancel_func and cancel_func():
log.info( "Installation error or cancel detected. See logfile." )
else:
log.info( "Install finished. Or at least virt shut down." )
[docs] def destroy( self ):
"""
Make sure the virt has been shut down and destroyed
Could use libvirt for this instead.
"""
log.info( "Shutting down %s", self.virt_name)
subprocess.call(["virsh", "destroy", self.virt_name])
# Undefine the virt, UEFI installs need to have --nvram passed
subprocess.call(["virsh", "undefine", self.virt_name, "--nvram"])
[docs]def novirt_install(opts, disk_img, disk_size, repo_url, cancel_func=None):
"""
Use Anaconda to install to a disk image
"""
# Clean up /tmp/ from previous runs to prevent stale info from being used
for path in ["/tmp/yum.repos.d/", "/tmp/yum.cache/", "/tmp/yum.root/", "/tmp/yum.pluginconf.d/"]:
if os.path.isdir(path):
shutil.rmtree(path)
args = ["--kickstart", opts.ks[0], "--cmdline", "--repo", repo_url]
if opts.anaconda_args:
for arg in opts.anaconda_args:
args += arg.split(" ", 1)
if opts.proxy:
args += ["--proxy", opts.proxy]
if opts.armplatform:
args += ["--armplatform", opts.armplatform]
if opts.make_iso or opts.make_fsimage:
# Make a blank fs image
args += ["--dirinstall"]
mkext4img(None, disk_img, label=opts.fs_label, size=disk_size * 1024**3)
if not os.path.isdir(ROOT_PATH):
os.mkdir(ROOT_PATH)
mount(disk_img, opts="loop", mnt=ROOT_PATH)
elif opts.make_tar:
args += ["--dirinstall"]
# Install directly into ROOT_PATH, make sure it starts clean
if os.path.exists(ROOT_PATH):
shutil.rmtree(ROOT_PATH)
if not os.path.isdir(ROOT_PATH):
os.mkdir(ROOT_PATH)
else:
args += ["--image", disk_img]
# Create the sparse image
mksparse(disk_img, disk_size * 1024**3)
cancel_funcs = []
if cancel_func is not None:
cancel_funcs.append(cancel_func)
# Make sure anaconda has the right product and release
os.environ["ANACONDA_PRODUCTNAME"] = opts.project
os.environ["ANACONDA_PRODUCTVERSION"] = opts.releasever
rc = execWithRedirect("anaconda", args, callback_func=lambda : any(f() for f in cancel_funcs))
# Move the anaconda logs over to a log directory
log_dir = os.path.abspath(os.path.dirname(opts.logfile))
log_anaconda = joinpaths(log_dir, "anaconda")
if not os.path.isdir(log_anaconda):
os.mkdir(log_anaconda)
for l in ["anaconda.log", "ifcfg.log", "program.log", "storage.log",
"packaging.log", "yum.log"]:
if os.path.exists("/tmp/"+l):
shutil.copy2("/tmp/"+l, log_anaconda)
os.unlink("/tmp/"+l)
if opts.make_iso or opts.make_fsimage:
umount(ROOT_PATH)
else:
# If anaconda failed the disk image may still be in use by dm
execWithRedirect("anaconda-cleanup", [])
if disk_img:
dm_name = os.path.splitext(os.path.basename(disk_img))[0]
log.debug("Removing device-mapper setup on %s", dm_name)
for d in sorted(glob.glob("/dev/mapper/"+dm_name+"*"), reverse=True):
dm_detach(d)
log.debug("Removing loop device for %s", disk_img)
loop_detach("/dev/"+get_loop_name(disk_img))
if rc:
raise InstallError("novirt_install failed")
if opts.make_tar:
compress_args = []
for arg in opts.compress_args:
compress_args += arg.split(" ", 1)
rc = mktar(ROOT_PATH, disk_img, opts.compression, compress_args)
shutil.rmtree(ROOT_PATH)
log.info("tar finished with rc=%d", rc)
if rc:
raise InstallError("novirt_install failed")
elif opts.qcow2:
log.info("Converting %s to qcow2", disk_img)
qcow2_args = []
for arg in opts.qcow2_args:
qcow2_args += arg.split(" ", 1)
# convert the image to qcow2 format
if "-O" not in qcow2_args:
qcow2_args.extend(["-O", "qcow2"])
qcow2_img = tempfile.mktemp(prefix="disk", suffix=".img")
execWithRedirect("qemu-img", ["convert"] + qcow2_args + [disk_img, qcow2_img], raise_err=True)
execWithRedirect("mv", ["-f", qcow2_img, disk_img], raise_err=True)
[docs]def virt_install(opts, install_log, disk_img, disk_size, cancel_func=None):
"""
Use virt-install to install to a disk image
install_log is the path to write the log from virt-install
disk_img is the full path to the final disk or filesystem image
disk_size is the size of the disk to create in GiB
"""
iso_mount = IsoMountpoint(opts.iso, opts.location)
log_monitor = LogMonitor(install_log)
cancel_funcs = [log_monitor.server.log_check]
if cancel_func is not None:
cancel_funcs.append(cancel_func)
kernel_args = ""
if opts.kernel_args:
kernel_args += opts.kernel_args
if opts.proxy:
kernel_args += " proxy="+opts.proxy
if opts.qcow2 and not opts.make_fsimage:
# virt-install can't take all the qcow2 options so create the image first
qcow2_args = []
for arg in opts.qcow2_args:
qcow2_args += arg.split(" ", 1)
mkqcow2(disk_img, disk_size*1024**3, qcow2_args)
if opts.make_fsimage or opts.make_tar:
diskimg_path = tempfile.mktemp(prefix="disk", suffix=".img")
else:
diskimg_path = disk_img
try:
virt = VirtualInstall(iso_mount, opts.ks, diskimg_path, disk_size,
kernel_args, opts.ram, opts.vnc, opts.arch,
cancel_func = lambda : any(f() for f in cancel_funcs),
virtio_host = log_monitor.host,
virtio_port = log_monitor.port,
qcow2=opts.qcow2, boot_uefi=opts.virt_uefi,
ovmf_path=opts.ovmf_path)
virt.destroy()
log_monitor.shutdown()
except InstallError as e:
log.error("VirtualInstall failed: %s", e)
raise
finally:
log.info("unmounting the iso")
iso_mount.umount()
if log_monitor.server.log_check():
raise InstallError("virt_install failed. See logfile.")
elif cancel_func and cancel_func():
raise InstallError("virt_install canceled by cancel_func")
if opts.make_fsimage:
mkdiskfsimage(diskimg_path, disk_img, label=opts.fs_label)
os.unlink(diskimg_path)
elif opts.make_tar:
compress_args = []
for arg in opts.compress_args:
compress_args += arg.split(" ", 1)
with PartitionMount(diskimg_path) as img_mount:
if img_mount and img_mount.mount_dir:
rc = mktar(img_mount.mount_dir, disk_img, opts.compression, compress_args)
os.unlink(diskimg_path)
if rc:
raise InstallError("virt_install failed")