diff options
author | Josh Kearney <josh@jk0.org> | 2013-07-18 13:57:54 -0500 |
---|---|---|
committer | Rick Harris <rconradharris@gmail.com> | 2013-08-21 22:17:33 +0000 |
commit | 614c4a2359123ad0ecac7ac0d168be8e246a5f9d (patch) | |
tree | 4e35e66ffc1591bc4283b9b9d4e1d5ddfbb83405 /plugins | |
parent | 0ee03b07305d7d0e6c910579e8c66d53741cf9c0 (diff) | |
download | nova-614c4a2359123ad0ecac7ac0d168be8e246a5f9d.tar.gz |
xenapi: Added iPXE ISO boot support
This patch adds support for a new kind of Glance image, a specially crafted ISO
which supports iPXE booting, giving customers a means to roll their own image.
Two virt-layer modifications were needed. The first was adding configurations
for the iPXE ISO feature (network to use, boot menu, mkisofs_cmd). The second
was the ability to inject networking info into the ISO after it was
downloaded.
To use this feature, operators should enable the `ipxe_boot` image-property.
DocImpact
Implements blueprint xenapi-ipxe-iso-boot-support
Change-Id: I33acf9dfdff0a5ed9797723a142bc451348e8549
Diffstat (limited to 'plugins')
3 files changed, 146 insertions, 1 deletions
diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec index 85c2d1c05d..84578cf540 100644 --- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec @@ -34,6 +34,7 @@ rm -rf $RPM_BUILD_ROOT /etc/xapi.d/plugins/config_file /etc/xapi.d/plugins/console /etc/xapi.d/plugins/glance +/etc/xapi.d/plugins/ipxe /etc/xapi.d/plugins/kernel /etc/xapi.d/plugins/migration /etc/xapi.d/plugins/pluginlib_nova.py diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/ipxe b/plugins/xenserver/xenapi/etc/xapi.d/plugins/ipxe new file mode 100755 index 0000000000..a41acb448e --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/ipxe @@ -0,0 +1,134 @@ +#!/usr/bin/env python + +# Copyright (c) 2013 Openstack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Inject network configuration into iPXE ISO for boot.""" + +import os +import shutil + +import utils + +#FIXME(sirp): should this use pluginlib from 5.6? +from pluginlib_nova import * +configure_logging('ipxe') + + +ISOLINUX_CFG = """SAY iPXE ISO boot image +TIMEOUT 30 +DEFAULT ipxe.krn +LABEL ipxe.krn + KERNEL ipxe.krn + INITRD netcfg.ipxe +""" + +NETCFG_IPXE = """#!ipxe +:start +imgfree +ifclose net0 +set net0/ip %(ip_address)s +set net0/netmask %(netmask)s +set net0/gateway %(gateway)s +set dns %(dns)s +ifopen net0 +goto menu + +:menu +chain %(boot_menu_url)s +goto boot + +:boot +sanboot --no-describe --drive 0x80 +""" + + +def _write_file(filename, data): + # If the ISO was tampered with such that the destination is a symlink, + # that could allow a malicious user to write to protected areas of the + # dom0 filesystem. /HT to comstud for pointing this out. + # + # Short-term, checking that the destination is not a symlink should be + # sufficient. + # + # Long-term, we probably want to perform all file manipulations within a + # chroot jail to be extra safe. + if os.path.islink(filename): + raise RuntimeError('SECURITY: Cannot write to symlinked destination') + + logging.debug("Writing to file '%s'" % filename) + f = open(filename, 'w') + try: + f.write(data) + finally: + f.close() + + +def _unbundle_iso(sr_path, filename, path): + logging.debug("Unbundling ISO '%s'" % filename) + read_only_path = utils.make_staging_area(sr_path) + try: + utils.run_command(['mount', '-o', 'loop', filename, read_only_path]) + try: + shutil.copytree(read_only_path, path) + finally: + utils.run_command(['umount', read_only_path]) + finally: + utils.cleanup_staging_area(read_only_path) + + +def _create_iso(mkisofs_cmd, filename, path): + logging.debug("Creating ISO '%s'..." % filename) + orig_dir = os.getcwd() + os.chdir(path) + try: + utils.run_command([mkisofs_cmd, '-quiet', '-l', '-o', filename, + '-c', 'boot.cat', '-b', 'isolinux.bin', + '-no-emul-boot', '-boot-load-size', '4', + '-boot-info-table', '.']) + finally: + os.chdir(orig_dir) + + +def inject(session, sr_path, vdi_uuid, boot_menu_url, ip_address, netmask, + gateway, dns, mkisofs_cmd): + + iso_filename = '%s.img' % os.path.join(sr_path, 'iso', vdi_uuid) + + # Create staging area so we have a unique path but remove it since + # shutil.copytree will recreate it + staging_path = utils.make_staging_area(sr_path) + utils.cleanup_staging_area(staging_path) + + try: + _unbundle_iso(sr_path, iso_filename, staging_path) + + # Write Configs + _write_file(os.path.join(staging_path, 'netcfg.ipxe'), + NETCFG_IPXE % {"ip_address": ip_address, + "netmask": netmask, + "gateway": gateway, + "dns": dns, + "boot_menu_url": boot_menu_url}) + + _write_file(os.path.join(staging_path, 'isolinux.cfg'), + ISOLINUX_CFG) + + _create_iso(mkisofs_cmd, iso_filename, staging_path) + finally: + utils.cleanup_staging_area(staging_path) + + +if __name__ == "__main__": + utils.register_plugin_calls(inject) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py index 8404fd961b..a9002ad245 100644 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/utils.py @@ -28,6 +28,10 @@ LOG = logging.getLogger(__name__) CHUNK_SIZE = 8192 +class CommandNotFound(Exception): + pass + + def delete_if_exists(path): try: os.unlink(path) @@ -65,7 +69,13 @@ def make_subprocess(cmdline, stdout=False, stderr=False, stdin=False, kwargs['stdin'] = stdin and subprocess.PIPE or None kwargs['universal_newlines'] = universal_newlines kwargs['close_fds'] = close_fds - proc = subprocess.Popen(cmdline, **kwargs) + try: + proc = subprocess.Popen(cmdline, **kwargs) + except OSError, e: + if e.errno == errno.ENOENT: + raise CommandNotFound + else: + raise return proc |