livemedia-creator: Add appliance creation

This adds support for creating an appliance description file for the
disk image. Mako templates are used to make it easy to support other
appliance targets. The included example works with virt-image.
This commit is contained in:
Brian C. Lane 2012-05-24 15:55:46 -07:00
parent 7a23c600c6
commit 6e2fdfe23f
4 changed files with 197 additions and 43 deletions

View File

@ -24,7 +24,7 @@ If you are using the lorax git repo you can run it like so:
sudo PATH=./src/sbin/:$PATH PYTHONPATH=./src/ ./src/sbin/livemedia-creator \
--make-iso --iso=/extra/iso/Fedora-16-x86_64-netinst.iso \
--ks=./docs/livemedia-example.ks --lorax-templates=./share/
--ks=./docs/fedora-livemedia.ks --lorax-templates=./share/
If you want to watch the install you can pass '--vnc vnc' and use a vnc
client to connect to localhost:0
@ -35,8 +35,7 @@ to monitor the logs for fatal errors, but may not catch everything.
HOW IT WORKS
------------
The --make-* switches define the final output. Currently only --make-iso
and --make-disk are working.
The --make-* switches define the final output.
You then need to either pass --iso and --ks in order to create a disk image
using virt-install, or --disk-image to use a disk image from a previous run
@ -48,19 +47,19 @@ customize the installed system in the same way that current spin-kickstarts
do.
livemedia-creator monitors the install process for problems by watching the
install logs. They are written to the current directory or to the base directory
specified by the --logfile command. You can also monitor the install by passing
--vnc vnc and using a vnc client. This is recommended when first modifying a
kickstart, since there are still places where Anaconda may get stuck without
the log monitor catching it.
install logs. They are written to the current directory or to the base
directory specified by the --logfile command. You can also monitor the install
by passing --vnc vnc and using a vnc client. This is recommended when first
modifying a kickstart, since there are still places where Anaconda may get
stuck without the log monitor catching it.
The output from this process is a partitioned disk image. kpartx can be used
to mount and examine it when there is a problem with the install. It can also
be booted using kvm.
Once the disk image is created it copies the / partition into a formatted
disk image which is then used as the input to lorax for creation of the
final media.
When creating an iso the disk image's / partition is copied into a formatted
disk image which is then used as the input to lorax for creation of the final
media.
The final image is created by lorax, using the templates in /usr/share/lorax/
or the directory specified by --lorax-templates
@ -169,6 +168,38 @@ This will produce an ami-root.img file in the working directory.
At this time I have not tested the image with EC2. Feedback would we welcome.
APPLIANCE CREATION ------------------ livemedia-creator can now replace
appliance-tools by using the --make-appliance switch. This will create the
partitioned disk image and an XML file that can be used with virt-image to
setup a virtual system.
The XML is generated using the Mako template from
/usr/share/lorax/appliance/virt-image.xml You can use a different template by
passing --app-template <template path>
Documentation on the Mako template system can be found here:
http://docs.makotemplates.org/en/latest/index.html
The name of the final output XML is appliance.xml, this can be changed with
--app-file <file path>
The following variables are passed to the template:
disks A list of disk_info about each disk.
Each entry has the following attributes:
name base name of the disk image file
format "raw"
checksum_type "sha256"
checksum sha256 checksum of the disk image
name Name of appliance, from --app-name argument
arch Architecture
memory Memory in KB (from --ram)
vcpus from --vcpus
networks list of networks from the kickstart or []
title from --title
project from --project
releasever from --releasever
DEBUGGING PROBLEMS
------------------
Cleaning up an aborted (ctrl-c) virt-install run (as root):
@ -190,21 +221,6 @@ Cleaning up aborted --no-virt installs can sometimes be accomplished by running
the anaconda-cleanup script.
THE FUTURE
----------
The current release supports creating live iso's and ami images. In the future
I also want it to be able to create appliance images.
It is also limited to x86 architectures because of it's use of virt-install.
I hope to be able to support other arches by using Anaconda's image install
feature instead of virt-install. This will require that livemedia-creator
be running on the same release as is being created in order to avoid odd
problems.
I would like to provide a set of alternate lorax template scripts to create
other media.
HACKING
-------
Development on this will take place as part of the lorax project, and on the

View File

@ -32,6 +32,7 @@ Requires: squashfs-tools >= 4.2
Requires: util-linux
Requires: xz
Requires: yum
Requires: pykickstart
%ifarch %{ix86} x86_64
Requires: syslinux >= 4.02-5

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<image>
<name>${name}</name>
<domain>
<boot type='hvm'>
<guest>
<arch>${arch}</arch>
</guest>
<os>
<loader dev='hd'/>
</os>
%for disk, letter in zip(disks, xrange(97, 123)):
<drive disk='${disk.name}' target='hd${chr(letter)}'/>
%endfor
</boot>
<devices>
<vcpu>${vcpus}</vcpu>
<memory>${memory}</memory>
%for net in networks:
<interface/>
%endfor
<graphics/>
</devices>
</domain>
<storage>
%for disk in disks:
<disk file='${disk.name}' use='system' format='${disk.format}'>
%if disk.checksum:
<checksum type='${disk.checksum_type}'>${disk.checksum}</checksum>
%endif
</disk>
%endfor
</storage>
</image>

View File

@ -36,11 +36,16 @@ from time import sleep
import shutil
import traceback
import argparse
import hashlib
# Use pykickstart to calculate disk image size
from pykickstart.parser import KickstartParser
from pykickstart.version import makeVersion
# Use Mako templates for appliance builder descriptions
from mako.template import Template
from mako.exceptions import text_error_template
# Use the Lorax treebuilder branch for iso creation
from pylorax.base import DataHolder
from pylorax.treebuilder import TreeBuilder, RuntimeBuilder, udev_escape
@ -332,7 +337,11 @@ def is_image_mounted(disk_img):
def anaconda_install( disk_img, disk_size, kickstart, repo, args ):
"""
disk_img Full path of the disk image
disk_size Disk size in GB
kickstart Full path to kickstart file
repo URL of repository
args Extra args to pass to anaconda --image install
"""
# Create the sparse image
mksparse( disk_img, disk_size * 1024**3 )
@ -354,6 +363,53 @@ def get_kernels( boot_dir ):
return [f[8:] for f in files if f.startswith("vmlinuz-")]
def make_appliance(disk_img, name, template, outfile, networks=None, ram=1024,
vcpus=1, title="Linux", project="Linux", releasever=17):
"""
Generate an appliance description file
disk_img Full path of the disk image
name Name of the appliance, passed to the template
template Full path of Mako template
outfile Full path of file to write, using template
networks List of networks from the kickstart
ram Ram, in MB, passed to template. Default is 1024
vcpus CPUs, passed to template. Default is 1
title Title, passed to template. Default is 'Linux'
project Project, passed to template. Default is 'Linux'
releasever Release version, passed to template. Default is 17
"""
if not (disk_img and template and outfile):
return None
log.info("Creating appliance definition using ${0}".format(template))
# Mount the disk and figure out the arch
arch = "x86_64"
log.info("Calculating SHA256 checksum of {0}".format(disk_img))
sha256 = hashlib.sha256()
with open(disk_img) as f:
while True:
data = f.read(1024*1024)
if not data:
break
sha256.update(data)
log.info("SHA256 of {0} is {1}".format(disk_img, sha256.hexdigest()))
disk_info = DataHolder(name=os.path.basename(disk_img), format="raw",
checksum_type="sha256", checksum=sha256.hexdigest())
try:
result = Template(filename=template).render(disks=[disk_info], name=name,
arch=arch, memory=ram*1024, vcpus=vcpus, networks=networks,
title=title, project=project, releasever=releasever)
except Exception:
log.error(text_error_template().render())
raise
with open(outfile, "w") as f:
f.write(result)
def make_ami( disk_img, ami_img="ami-root.img", ami_label="AMI" ):
"""
Copy the / partition to an un-partitioned disk image
@ -483,6 +539,8 @@ if __name__ == '__main__':
parser.add_argument( "--ks", action="append", type=os.path.abspath,
help="Kickstart file defining the install." )
parser.add_argument( "--image-name", default=None,
help="Name of disk image to create. Default is a random name." )
parser.add_argument( "--image-only", action="store_true",
help="Exit after creating disk image." )
parser.add_argument( "--keep-image", action="store_true",
@ -508,6 +566,15 @@ if __name__ == '__main__':
help="Directory to copy the resulting images and iso into. "
"Defaults to the temporary working directory")
# Group of arguments for appliance creation
app_group = parser.add_argument_group("appliance arguments")
app_group.add_argument( "--app-name", default=None,
help="Name of appliance to pass to template")
app_group.add_argument( "--app-template", default=None,
help="Path to template to use for appliance data.")
app_group.add_argument( "--app-file", default="appliance.xml",
help="Appliance template results file.")
# Group of arguments to pass to virt-install
if not libvirt:
virt_group = parser.add_argument_group("virt-install arguments (DISABLED -- no libvirt)")
@ -590,10 +657,6 @@ if __name__ == '__main__':
log.error( "The disk image {0} is missing.".format( opts.disk_image ) )
sys.exit( 1 )
if opts.make_appliance:
log.error( "--make-appliance is not yet implemented." )
sys.exit( 1 )
if not opts.no_virt and not opts.iso and not opts.disk_image:
log.error( "virt-install needs an install iso." )
sys.exit( 1 )
@ -606,14 +669,41 @@ if __name__ == '__main__':
log.error("virt-install requires libvirt to be installed.")
sys.exit(1)
tempfile.tempdir = opts.tmp
if opts.no_virt and not os.path.exists("/usr/sbin/anaconda"):
log.error("no-virt requires anaconda to be installed.")
sys.exit(1)
# Make the disk image
if not opts.disk_image:
# Parse the kickstart to get the partition sizes
if opts.make_appliance and not opts.app_template:
opts.app_template = joinpaths(opts.lorax_templates,
"appliance/libvirt.tmpl")
if opts.make_appliance and not os.path.exists(opts.app_template):
log.error("The appliance template ({0}) doesn't "
"exist".format(opts.app_template))
sys.exit(1)
if opts.image_name and os.path.exists(joinpaths(opts.tmp,opts.image_name)):
log.error("The disk image to be created should not exist.")
sys.exit(1)
if opts.app_file:
opts.app_file = joinpaths(opts.tmp, opts.app_file)
tempfile.tempdir = opts.tmp
disk_img = None
# Parse the kickstart
if opts.ks:
ks_version = makeVersion()
ks = KickstartParser( ks_version, errorsAreFatal=False, missingIncludeIsFatal=False )
ks.readKickstart( opts.ks[0] )
# Make the disk image
if not opts.disk_image:
if not opts.ks:
log.error("Image creation requires a kickstart file")
sys.exit(1)
disk_size = 1 + (sum( [p.size for p in ks.handler.partition.partitions] ) / 1024)
log.info( "disk_size = {0}GB".format(disk_size) )
@ -627,7 +717,10 @@ if __name__ == '__main__':
"graphical), this will interfere with livemedia-creator.")
sys.exit(1)
disk_img = tempfile.mktemp( prefix="disk", suffix=".img", dir=opts.tmp )
if opts.image_name:
disk_img = joinpaths(opts.tmp, opts.image_name)
else:
disk_img = tempfile.mktemp( prefix="disk", suffix=".img", dir=opts.tmp )
install_log = os.path.abspath(os.path.dirname(opts.logfile))+"/virt-install.log"
log.info( "disk_img = {0}".format(disk_img) )
@ -700,12 +793,21 @@ if __name__ == '__main__':
opts.lorax_templates,
opts.title, opts.project, opts.releasever,
opts.volid )
# cleanup the mess
if disk_img and not opts.keep_image and not opts.disk_image:
os.unlink( disk_img )
log.info("Disk image erased")
disk_img = None
elif opts.make_ami and not opts.image_only:
result_dir = make_ami(opts.disk_image or disk_img)
# cleanup the mess
if disk_img and not opts.keep_image and not opts.disk_image:
os.unlink( disk_img )
elif opts.make_appliance and not opts.image_only:
if not opts.ks:
networks = []
else:
networks = ks.handler.network.network
make_appliance(opts.disk_image or disk_img, opts.app_name,
opts.app_template, opts.app_file, networks, opts.ram,
opts.vcpus, opts.arch, opts.title, opts.project, opts.releasever)
if opts.result_dir and result_dir:
shutil.copytree( result_dir, opts.result_dir )
@ -714,10 +816,10 @@ if __name__ == '__main__':
log.info("SUMMARY")
log.info("-------")
log.info("Logs are in {0}".format(os.path.abspath(os.path.dirname(opts.logfile))))
if opts.keep_image or opts.make_disk:
if disk_img:
log.info("Disk image is at {0}".format(disk_img))
else:
log.info("Disk image erased")
if opts.make_appliance:
log.info("Appliance description is in {0}".format(opts.app_file))
if result_dir:
log.info("Results are in {0}".format(opts.result_dir or result_dir))