From 0da9254537f6acc93da1cf09d6b3eee06853dec8 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 24 Jul 2015 08:43:49 +0200 Subject: [PATCH 1/7] libvirt: virt_net module This module manages network configuration in libvirt. --- cloud/misc/virt_net.py | 557 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 cloud/misc/virt_net.py diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py new file mode 100644 index 00000000000..b84f976c6f3 --- /dev/null +++ b/cloud/misc/virt_net.py @@ -0,0 +1,557 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Maciej Delmanowski +# +# 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 = ''' +--- +module: virt_net +author: "Maciej Delmanowski" +version_added: "2.0" +short_description: Manage libvirt network configuration +description: + - Manage I(libvirt) networks. +options: + name: + required: true + aliases: ['network'] + description: + - name of the network being managed. Note that network must be previously + defined with xml. + state: + required: false + choices: [ "active", "inactive", "present", "absent" ] + description: + - specify which state you want a network to be in. + If 'active', network will be started. + If 'present', ensure that network is present but do not change its + state; if it's missing, you need to specify xml argument. + If 'inactive', network will be stopped. + If 'undefined' or 'absent', network will be removed from I(libvirt) configuration. + command: + required: false + choices: [ "define", "create", "start", "stop", "destroy", + "undefine", "get_xml", "list_nets", "facts", + "info", "status"] + description: + - in addition to state management, various non-idempotent commands are available. + See examples. + autostart: + required: false + choices: ["yes", "no"] + description: + - Specify if a given storage pool should be started automatically on system boot. + uri: + required: false + default: "qemu:///system" + description: + - libvirt connection uri. + xml: + required: false + description: + - XML document used with the define command. +requirements: + - "python >= 2.6" + - "python-libvirt" + - "python-lxml" +''' + +EXAMPLES = ''' +# Define a new network +- virt_net: command=define name=br_nat xml='{{ lookup("template", "network/bridge.xml.j2") }}' + +# Start a network +- virt_net: command=create name=br_nat + +# List available networks +- virt_net: command=list_nets + +# Get XML data of a specified network +- virt_net: command=get_xml name=br_nat + +# Stop a network +- virt_net: command=destroy name=br_nat + +# Undefine a network +- virt_net: command=undefine name=br_nat + +# Gather facts about networks +# Facts will be available as 'ansible_libvirt_networks' +- virt_net: command=facts + +# Gather information about network managed by 'libvirt' remotely using uri +- virt_net: command=info uri='{{ item }}' + with_items: libvirt_uris + register: networks + +# Ensure that a network is active (needs to be defined and built first) +- virt_net: state=active name=br_nat + +# Ensure that a network is inactive +- virt_net: state=inactive name=br_nat + +# Ensure that a given network will be started at boot +- virt_net: autostart=yes name=br_nat + +# Disable autostart for a given network +- virt_net: autostart=no name=br_nat +''' + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE=2 + +import sys + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +try: + from lxml import etree +except ImportError: + HAS_XML = False +else: + HAS_XML = True + +ALL_COMMANDS = [] +ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', + 'undefine', 'destroy', 'get_xml', 'define'] +HOST_COMMANDS = [ 'list_nets', 'facts', 'info' ] +ALL_COMMANDS.extend(ENTRY_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +ENTRY_STATE_ACTIVE_MAP = { + 0 : "inactive", + 1 : "active" +} + +ENTRY_STATE_AUTOSTART_MAP = { + 0 : "no", + 1 : "yes" +} + +ENTRY_STATE_PERSISTENT_MAP = { + 0 : "no", + 1 : "yes" +} + +class EntryNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + cmd = "uname -r" + rc, stdout, stderr = self.module.run_command(cmd) + + if "xen" in stdout: + conn = libvirt.open(None) + else: + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_entry(self, entryid): + # entryid = -1 returns a list of everything + + results = [] + + # Get active entries + entries = self.conn.listNetworks() + for name in entries: + entry = self.conn.networkLookupByName(name) + results.append(entry) + + # Get inactive entries + entries = self.conn.listDefinedNetworks() + for name in entries: + entry = self.conn.networkLookupByName(name) + results.append(entry) + + if entryid == -1: + return results + + for entry in results: + if entry.name() == entryid: + return entry + + raise EntryNotFound("network %s not found" % entryid) + + def create(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).create() + else: + try: + state = self.find_entry(entryid).isActive() + except: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def destroy(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).destroy() + else: + if self.find_entry(entryid).isActive(): + return self.module.exit_json(changed=True) + + def undefine(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).undefine() + else: + if not self.find_entry(entryid): + return self.module.exit_json(changed=True) + + def get_status2(self, entry): + state = entry.isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + + def get_status(self, entryid): + if not self.module.check_mode: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + else: + try: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + except: + return ENTRY_STATE_ACTIVE_MAP.get("inactive","unknown") + + def get_uuid(self, entryid): + return self.find_entry(entryid).UUIDString() + + def get_xml(self, entryid): + return self.find_entry(entryid).XMLDesc(0) + + def get_forward(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/forward')[0].get('mode') + except: + raise ValueError('Forward mode not specified') + return result + + def get_domain(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/domain')[0].get('name') + except: + raise ValueError('Domain not specified') + return result + + def get_macaddress(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/mac')[0].get('address') + except: + raise ValueError('MAC address not specified') + return result + + def get_autostart(self, entryid): + state = self.find_entry(entryid).autostart() + return ENTRY_STATE_AUTOSTART_MAP.get(state,"unknown") + + def get_autostart2(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).autostart() + else: + try: + return self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + + def set_autostart(self, entryid, val): + if not self.module.check_mode: + return self.find_entry(entryid).setAutostart(val) + else: + try: + state = self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + if bool(state) != val: + return self.module.exit_json(changed=True) + + def get_bridge(self, entryid): + return self.find_entry(entryid).bridgeName() + + def get_persistent(self, entryid): + state = self.find_entry(entryid).isPersistent() + return ENTRY_STATE_PERSISTENT_MAP.get(state,"unknown") + + def define_from_xml(self, entryid, xml): + if not self.module.check_mode: + return self.conn.networkDefineXML(xml) + else: + try: + state = self.find_entry(entryid) + except: + return self.module.exit_json(changed=True) + + +class VirtNetwork(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + self.conn = LibvirtConnection(self.uri, self.module) + + def get_net(self, entryid): + return self.conn.find_entry(entryid) + + def list_nets(self, state=None): + entries = self.conn.find_entry(-1) + results = [] + for x in entries: + try: + if state: + entrystate = self.conn.get_status2(x) + if entrystate == state: + results.append(x.name()) + else: + results.append(x.name()) + except: + pass + return results + + def state(self): + entries = self.list_nets() + results = [] + for entry in entries: + state_blurb = self.conn.get_status(entry) + results.append("%s %s" % (entry,state_blurb)) + return results + + def autostart(self, entryid): + return self.conn.set_autostart(entryid, True) + + def get_autostart(self, entryid): + return self.conn.get_autostart2(entryid) + + def set_autostart(self, entryid, state): + return self.conn.set_autostart(entryid, state) + + def create(self, entryid): + return self.conn.create(entryid) + + def start(self, entryid): + return self.conn.create(entryid) + + def stop(self, entryid): + return self.conn.destroy(entryid) + + def destroy(self, entryid): + return self.conn.destroy(entryid) + + def undefine(self, entryid): + return self.conn.undefine(entryid) + + def status(self, entryid): + return self.conn.get_status(entryid) + + def get_xml(self, entryid): + return self.conn.get_xml(entryid) + + def define(self, entryid, xml): + return self.conn.define_from_xml(entryid, xml) + + def info(self, facts_mode='info'): + return self.facts(facts_mode) + + def facts(self, facts_mode='facts'): + entries = self.list_nets() + results = dict() + for entry in entries: + results[entry] = dict() + results[entry]["autostart"] = self.conn.get_autostart(entry) + results[entry]["persistent"] = self.conn.get_persistent(entry) + results[entry]["state"] = self.conn.get_status(entry) + results[entry]["bridge"] = self.conn.get_bridge(entry) + results[entry]["uuid"] = self.conn.get_uuid(entry) + + try: + results[entry]["forward_mode"] = self.conn.get_forward(entry) + except ValueError as e: + pass + + try: + results[entry]["domain"] = self.conn.get_domain(entry) + except ValueError as e: + pass + + try: + results[entry]["macaddress"] = self.conn.get_macaddress(entry) + except ValueError as e: + pass + + facts = dict() + if facts_mode == 'facts': + facts["ansible_facts"] = dict() + facts["ansible_facts"]["ansible_libvirt_networks"] = results + elif facts_mode == 'info': + facts['networks'] = results + return facts + + +def core(module): + + state = module.params.get('state', None) + name = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + autostart = module.params.get('autostart', None) + + v = VirtNetwork(uri, module) + res = {} + + if state and command == 'list_nets': + res = v.list_nets(state=state) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + if state: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if state in [ 'active' ]: + if v.status(name) is not 'active': + res['changed'] = True + res['msg'] = v.start(name) + elif state in [ 'present' ]: + try: + v.get_net(name) + except EntryNotFound: + if not xml: + module.fail_json(msg = "network '" + name + "' not present, but xml not specified") + v.define(name, xml) + res = {'changed': True, 'created': name} + elif state in [ 'inactive' ]: + entries = v.list_nets() + if name in entries: + if v.status(name) is not 'inactive': + res['changed'] = True + res['msg'] = v.destroy(name) + elif state in [ 'undefined', 'absent' ]: + entries = v.list_nets() + if name in entries: + if v.status(name) is not 'inactive': + v.destroy(name) + res['changed'] = True + res['msg'] = v.undefine(name) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in ENTRY_COMMANDS: + if not name: + module.fail_json(msg = "%s requires 1 argument: name" % command) + if command == 'define': + if not xml: + module.fail_json(msg = "define requires xml argument") + try: + v.get_net(name) + except EntryNotFound: + v.define(name, xml) + res = {'changed': True, 'created': name} + return VIRT_SUCCESS, res + res = getattr(v, command)(name) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + elif hasattr(v, command): + res = getattr(v, command)() + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % basecmd) + + if autostart: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if autostart == 'yes': + if not v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, True) + elif autostart == 'no': + if v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, False) + + return VIRT_SUCCESS, res + + module.fail_json(msg="expected state or command parameter to be specified") + +def main(): + + module = AnsibleModule ( + argument_spec = dict( + name = dict(aliases=['network']), + state = dict(choices=['active', 'inactive', 'present', 'absent']), + command = dict(choices=ALL_COMMANDS), + uri = dict(default='qemu:///system'), + xml = dict(), + autostart = dict(choices=['yes', 'no']) + ), + supports_check_mode = True + ) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + if not HAS_XML: + module.fail_json( + msg='The `lxml` module is not importable. Check the requirements.' + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception, e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * +main() From 8b2cc4f7bbee6cf913d055acb1d515e0b73989f6 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:11:52 +0200 Subject: [PATCH 2/7] Remove separate check for Xen Module checked for Xen-based system, however since 'xen:///' URI support exists in 'libvirt', we should use it explicitly instead. --- cloud/misc/virt_net.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index b84f976c6f3..cd579a3f785 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -164,13 +164,7 @@ class LibvirtConnection(object): self.module = module - cmd = "uname -r" - rc, stdout, stderr = self.module.run_command(cmd) - - if "xen" in stdout: - conn = libvirt.open(None) - else: - conn = libvirt.open(uri) + conn = libvirt.open(uri) if not conn: raise Exception("hypervisor connection failure") From 00e7e225ce2dec35f1890a691185bc694cf20e2f Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:14:03 +0200 Subject: [PATCH 3/7] Rewrite for loops in a more Pythonic style --- cloud/misc/virt_net.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index cd579a3f785..3622d455e48 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -177,14 +177,12 @@ class LibvirtConnection(object): results = [] # Get active entries - entries = self.conn.listNetworks() - for name in entries: + for name in self.conn.listNetworks(): entry = self.conn.networkLookupByName(name) results.append(entry) # Get inactive entries - entries = self.conn.listDefinedNetworks() - for name in entries: + for name in self.conn.listDefinedNetworks(): entry = self.conn.networkLookupByName(name) results.append(entry) @@ -334,9 +332,8 @@ class VirtNetwork(object): return results def state(self): - entries = self.list_nets() results = [] - for entry in entries: + for entry in self.list_nets(): state_blurb = self.conn.get_status(entry) results.append("%s %s" % (entry,state_blurb)) return results @@ -378,9 +375,8 @@ class VirtNetwork(object): return self.facts(facts_mode) def facts(self, facts_mode='facts'): - entries = self.list_nets() results = dict() - for entry in entries: + for entry in self.list_nets(): results[entry] = dict() results[entry]["autostart"] = self.conn.get_autostart(entry) results[entry]["persistent"] = self.conn.get_persistent(entry) From dc92f0af4ca6c569a3048c25bdb9efef59c1a006 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:15:23 +0200 Subject: [PATCH 4/7] Rewrite method to not use try/except Additional checks are not needed, because 'self.conn.get_entry(-1)' returns all existing entries, each one should have state defined. --- cloud/misc/virt_net.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 3622d455e48..30d796b711f 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -317,18 +317,13 @@ class VirtNetwork(object): return self.conn.find_entry(entryid) def list_nets(self, state=None): - entries = self.conn.find_entry(-1) results = [] - for x in entries: - try: - if state: - entrystate = self.conn.get_status2(x) - if entrystate == state: - results.append(x.name()) - else: - results.append(x.name()) - except: - pass + for entry in self.conn.find_entry(-1): + if state: + if state == self.conn.get_status2(entry): + results.append(entry.name()) + else: + results.append(entry.name()) return results def state(self): From 2b15b0564cb68962100bfd10dab7c44779223096 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:18:00 +0200 Subject: [PATCH 5/7] Add whitespace so diff with 'virt_pool' is easier --- cloud/misc/virt_net.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 30d796b711f..2be753c6a89 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -501,6 +501,7 @@ def core(module): module.fail_json(msg="expected state or command parameter to be specified") + def main(): module = AnsibleModule ( From 13e51060ec2bd9940fae48f4112282733097c380 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:18:39 +0200 Subject: [PATCH 6/7] Remove unused parameter from method arguments --- cloud/misc/virt_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 2be753c6a89..078275cd84a 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -366,8 +366,8 @@ class VirtNetwork(object): def define(self, entryid, xml): return self.conn.define_from_xml(entryid, xml) - def info(self, facts_mode='info'): - return self.facts(facts_mode) + def info(self): + return self.facts(facts_mode='info') def facts(self, facts_mode='facts'): results = dict() From 2af729944af658d24168be6486d56235f3122030 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Tue, 28 Jul 2015 00:21:27 +0200 Subject: [PATCH 7/7] Update author information in virt_net docs --- cloud/misc/virt_net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 cloud/misc/virt_net.py diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py old mode 100644 new mode 100755 index 078275cd84a..21cdca5fbd7 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: virt_net -author: "Maciej Delmanowski" +author: "Maciej Delmanowski (@drybjed)" version_added: "2.0" short_description: Manage libvirt network configuration description: