From ac427e077a2716e14dc2856c8e23bca4a2e85b79 Mon Sep 17 00:00:00 2001 From: Jeroen Hoekx Date: Fri, 12 Oct 2012 22:40:04 +0200 Subject: [PATCH] Add virt_boot module to define libvirt boot parameters. --- virt_boot | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100755 virt_boot diff --git a/virt_boot b/virt_boot new file mode 100755 index 00000000000..b7748100b6e --- /dev/null +++ b/virt_boot @@ -0,0 +1,309 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) 2012, Jeroen Hoekx +# +# This file is part of Ansible +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +author: Jeroen Hoekx +module: virt_boot +short_description: Define libvirt boot parameters +description: + - "This module configures the boot order or boot media of a libvirt virtual + machine. A guest can be configured to boot from network, hard disk, floppy, + cdrom or a direct kernel boot. Specific media can be attached for cdrom, + floppy and direct kernel boot." + - This module requires the libvirt module. +version_added: "0.8" +options: + domain: + description: + - The name of the libvirt domain. + required: true + boot: + description: + - "Specify the boot order of the virtual machine. This is a comma-separated + list of: I(fd), I(hd), I(cdrom) and I(network)." + required: false + bootmenu: + description: + - Enable or disable the boot menu. + required: false + choices: [ "yes", "no" ] + kernel: + description: + - The path of the kernel to boot. + required: false + initrd: + description: + - The path of the initrd to boot. + required: false + cmdline: + description: + - The command line to boot the kernel with. + required: false + device: + default: hdc + description: + - The libvirt device name of the cdrom/floppy. + required: false + image: + description: + - The image to connect to the cdrom/floppy device. + required: false +examples: + - description: Boot from a cdrom image. + code: virt_boot domain=archrear image=/srv/rear/archrear/rear-archrear.iso boot=cdrom + - description: Boot from the local disk. + code: virt_boot domain=archrear boot=hd + - description: Boot a specific kernel with a special command line. + code: virt_boot domain=archrear kernel=$storage/kernel-archrear initrd=$storage/initramfs-archrear.img cmdline="root=/dev/ram0 vga=normal rw" + - description: Boot from the harddisk and if that fails from the network. + code: virt_boot domain=archrear boot=hd,network + - description: Enable the boot menu. + code: virt_boot domain=archrear bootmenu=yes +requirements: [ "libvirt" ] +notes: + - Run this on the libvirt host. + - I(kernel) and I(boot) are mutually exclusive. + - This module can not change a running system. + - Using direct kernel boot will always result in a I(changed) state due to libvirt internals. +''' + +from xml.dom.minidom import parseString + +try: + import libvirt +except ImportError: + print "failed=True msg='libvirt python module unavailable'" + sys.exit(1) + +def get_domain(name): + conn = libvirt.open("qemu:///system") + domain = conn.lookupByName(name) + + return domain, conn + +def get_xml(domain): + domain_data = domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE) + tree = parseString(domain_data) + + return tree + +def write_xml(tree, conn): + conn.defineXML( tree.toxml() ) + +def element_text(element, data=None): + if data: + to_be_removed = [] + for node in element.childNodes: + to_be_removed.append(node) + for node in to_be_removed: + element.removeChild(node) + element.appendChild( element.ownerDocument.createTextNode(data) ) + if element.firstChild and element.firstChild.nodeType==element.TEXT_NODE: + return element.firstChild.data + +def get_disk(tree, device): + for target in tree.getElementsByTagName('target'): + if target.getAttribute("dev") == device: + return target + +def attach_disk(domain, tree, device, image): + disk = get_disk(tree, device) + if disk: + source = disk.parentNode.getElementsByTagName('source').item(0) + if source and source.getAttribute("file") == image: + return False + + CDROM_TEMPLATE=''' + + + + ''' + xml = CDROM_TEMPLATE.format(path=image, dev=device) + domain.updateDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CONFIG) + return True + +def detach_disk(domain, tree, device): + disk = get_disk(tree, device) + if disk: + source = disk.parentNode.getElementsByTagName('source').item(0) + if source and source.hasAttribute("file"): + source.removeAttribute("file") + xml = disk.parentNode.toxml() + domain.updateDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CONFIG) + return True + return False + +def main(): + + module = AnsibleModule( + argument_spec = dict( + domain=dict(required=True, aliases=['guest']), + boot=dict(), + bootmenu=dict(choices=BOOLEANS), + kernel=dict(), + initrd=dict(), + cmdline=dict(), + device=dict(default='hdc'), + image=dict(), + ), + required_one_of = [['boot','kernel','image','bootmenu']], + mutually_exclusive = [['boot','kernel']] + ) + + params = module.params + + domain_name = params['domain'] + + boot = params['boot'] + bootmenu = module.boolean(params['bootmenu']) + kernel = params['kernel'] + initrd = params['initrd'] + cmdline = params['cmdline'] + + device = params['device'] + image = params['image'] + + changed = False + + domain, conn = get_domain(domain_name) + if domain.isActive(): + module.fail_json(msg="Domain %s is still running."%(domain_name)) + tree = get_xml(domain) + + ### Connect image + if image: + changed = changed or attach_disk(domain, tree, device, image) + if not boot and not kernel: + module.exit_json(changed=changed, image=image, device=device) + else: + changed = changed or detach_disk(domain, tree, device) + + if changed: + tree = get_xml(domain) + + ### Boot ordering + os = tree.getElementsByTagName('os').item(0) + boot_list = os.getElementsByTagName('boot') + kernel_el = os.getElementsByTagName('kernel').item(0) + initrd_el = os.getElementsByTagName('initrd').item(0) + cmdline_el = os.getElementsByTagName('cmdline').item(0) + if boot: + if kernel_el: + changed = True + kernel_el.parentNode.removeChild(kernel_el) + if initrd_el: + changed = True + initrd_el.parentNode.removeChild(initrd_el) + if cmdline_el: + changed = True + cmdline_el.parentNode.removeChild(cmdline_el) + + items = boot.split(',') + if boot_list: + needs_change = False + if len(items) == len(boot_list): + for (boot_el, dev) in zip(boot_list, items): + if boot_el.getAttribute('dev') != dev: + needs_change = True + else: + needs_change = True + + if needs_change: + changed = True + to_be_removed = [] + for boot_el in boot_list: + to_be_removed.append(boot_el) + for boot_el in to_be_removed: + os.removeChild(boot_el) + for item in items: + boot_el = tree.createElement('boot') + boot_el.setAttribute('dev', item) + os.appendChild(boot_el) + else: + changed = True + for item in items: + boot_el = tree.createElement('boot') + boot_el.setAttribute('dev', item) + os.appendChild(boot_el) + + elif kernel: + if boot_list: + changed = True + to_be_removed = [] + for boot_el in boot_list: + to_be_removed.append(boot_el) + for boot_el in to_be_removed: + os.removeChild(boot_el) + if kernel_el: + if element_text(kernel_el) != kernel: + changed = True + element_text(kernel_el, kernel) + else: + changed = True + kernel_el = tree.createElement('kernel') + kernel_el.appendChild( tree.createTextNode(kernel) ) + os.appendChild(kernel_el) + + if initrd_el: + if element_text(initrd_el) != initrd: + changed = True + element_text(initrd_el, initrd) + else: + changed = True + initrd_el = tree.createElement('initrd') + initrd_el.appendChild( tree.createTextNode(initrd) ) + os.appendChild(initrd_el) + + if cmdline_el: + if element_text(cmdline_el) != cmdline: + changed = True + element_text(cmdline_el, cmdline) + else: + changed = True + cmdline_el = tree.createElement('cmdline') + cmdline_el.appendChild( tree.createTextNode(cmdline) ) + os.appendChild(cmdline_el) + + ### Enable/disable bootmenu + bootmenu_state = tree.getElementsByTagName('bootmenu').item(0) + if bootmenu and bootmenu_state: + bootmenu_enabled = bootmenu_state.getAttribute('enable') + if bootmenu_enabled != 'yes': + changed = True + bootmenu_state.setAttribute('enable', 'yes') + elif bootmenu: + os = tree.getElementsByTagName('os').item(0) + bootmenu_state = tree.createElement('bootmenu') + bootmenu_state.setAttribute('enable', 'yes') + changed = True + os.appendChild(bootmenu_state) + elif bootmenu_state: + bootmenu_state.parentNode.removeChild(bootmenu_state) + changed = True + + ### save back + write_xml(tree, conn) + + module.exit_json(changed=changed) + +# this is magic, see lib/ansible/module_common.py +#<> +main()