# virt_net ## _Ansible_ module using libvirt to manage QEMU/KVM virtual networks.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

263 lines
8.7 KiB

#!/usr/bin/python3
# This code is released under the WTFPL(http://www.wtfpl.net/) by MCO System (https://www.mcos.nc)
import os
import shutil
import libvirt
from lxml import etree
from xmldiff import main as xd_main
from ansible.module_utils.basic import AnsibleModule
class VirtNet:
"""The Virt_VM object manage all aspect of libvrt/KVM/QEMU virtual machine configuration from various parameters
"""
def __init__(self, params):
self.params = params
self.xml_base_filename = "kvm_net.xml"
self.xml_actual_root = None
self.xml_tmp_root = None
self.vir_connection = "qemu:///system"
self.vir_net = None
def _set_xml_uuid(self, element):
"""Set network UUID
"""
element.text = self.params['uuid']
def _set_xml_name(self, element):
"""Set network name
"""
element.text = self.params['name']
def _set_xml_forward(self, element):
"""Set network mode
"""
element.set('mode', self.params['mode'])
def _set_xml_bridge(self, element):
"""Set network bridge name
"""
element.set('name', self.params['bridge_name'])
def _set_xml_mac_addr(self, element):
"""Set network MAC address
"""
element.set('address', self.params['mac_addr'])
def _set_xml_ip(self, element):
"""Set network IP address and netmask
"""
element.set('address',self.params['dhcp_addr'])
element.set('netmask', self.params['dhcp_netmask'])
subelement = element.find('dhcp').find('dhcp')
self._set_xml_dhcp(subelement)
def _set_xml_dhcp(self, element):
"""Set net DHC params
"""
element.set('address', str(self.params['dhcp_address']))
element.set('netmask', str(self.params['dhcp_netmask']))
subelement = element.find('dhcp').find('range')
subelement.set('start', str(self.params['dhcp_first']))
subelement.set('start', str(self.params['dhcp_last']))
for addr in self.params['dhcp_reserved']:
host = subelement.append(etree.Element("host"))
host.set('mac', str(addr['mac_addr']))
host.set('mac', str(addr['name']))
host.set('ip', str(addr['ip_addr']))
def _set_autostart(self):
"""Set network autostart parameter.
:return: True if no error encountered. False otherwise
:rtype: bool
"""
result = 'unchanged'
try:
autostart = self.vir_net.autostart()
if self.params['auto_start'] != autostart:
self.vir_net.setAutostart(self.params['auto_start'])
result = 'changed'
except Exception as e:
result = 'error'
return result
def _set_state(self):
"""Set network state (Running or destroyed).
:return: True if no error encountered. False otherwise
:rtype: bool
"""
result = 'unchanged'
active = self.vir_net.isActive()
xml = self.vir_net.XMLDesc()
try:
if self.params['active'] != active and self.params['active']:
with libvirt.open(self.vir_connection) as connect:
connect.networkCreateXML(xml)
result = 'changed'
elif self.params['active'] != active and not self.params['active']:
self.vir_net.destroy()
result = 'changed'
except Exception as e:
result = 'error'
return result
def get_virt_net(self):
"""Get libvirt.virNetwork object from UUID and keep a reference to it in self.vir_net. Also get XML from domain XML file.
:return: True if operation succeeded, False otherwise, meaning that domain does not exist on system.
:rtype: bool
"""
try:
with libvirt.open(self.vir_connection) as connect:
self.vir_net = connect.networkLookupByUUIDString(self.params['uuid'])
with open(os.path.join(
self.params['libvirt_qemu_conf_path'],
'{}.xml'.format(self.params['name']))) as actual_xml_file:
self.xml_actual_root = etree.parse(actual_xml_file).getroot()
return True
except Exception as e:
return False
def create_xml_file(self):
"""Create temporary XML network file
:return: A tuple containing out (wich is a tuple containing path to tmp files) and a string representing potential error message
:rtype: tuple
"""
err = ''
self.xml_tmp_root = etree.parse(
os.path.join(
self.params['libvirt_tmp_path'],
self.xml_base_filename)
).getroot()
try:
elements = {'uuid': self._set_xml_uuid,
'name': self._set_xml_name,
'forward': self._set_xml_forward,
'bridge': self._set_xml_bridge,
'mac': self._set_xml_mac_addr,
'ip': self._set_xml_ip,}
for element in self.xml_tmp_root.getchildren():
if element.tag in elements:
elements[element.tag](element)
except Exception as e:
err = '[CREATE] {}'.format(str(e))
return err
def compare_xml_file(self):
"""Compare generated temporary XML network file with actual XML network file.
:return: A tuple with an integer representing the number of different lines and a string representing potential error message
:rtype: tuple
"""
out=1
err=''
# Make module idempotent
try:
diff = xd_main.diff_texts(etree.tostring(self.xml_tmp_root,
encoding="utf-8",
xml_declaration=False,
pretty_print=True).decode("utf-8"),
etree.tostring(self.xml_actual_root,
encoding="utf-8",
xml_declaration=False,
pretty_print=True).decode("utf-8"))
out = len(diff)
except Exception as e:
err = '[COMPARE] {}'.format(str(e))
return (out, err)
def define_network(self):
"""Define the generated network from its XML file
:return: A string representing potential error message (Empty string if no error)
:rtype: str
"""
err = ''
try:
with libvirt.open(self.vir_connection) as connect:
connect.networkDefineXML(etree.tostring(self.xml_tmp_root,
encoding="utf-8",
xml_declaration=False,
pretty_print=True).decode("utf-8"))
except Exception as e:
err = '[DEFINE] {}'.format(str(e))
return err
def apply_live_settings(self):
"""Apply settings to domain
:return: A dict representing status of each operations.
Status can be on of `changed`, `unchanged`, `error`
:rtype: dict
"""
results = {}
if self.get_virt_net():
settings = {'autostart': self._set_autostart,
'state': self._set_state }
for setting in settings:
results[setting] = settings[setting]()
return results
def main():
module = AnsibleModule(
supports_check_mode = True,
argument_spec = dict(
uuid = dict(required=True),
name = dict(required=True),
mode = dict(required=True, default='nat'),
bridge_name = dict(required=True, default='virbr0'),
mac_addr = dict(required=True),
dhcp_addr = dict(required=True),
dhcp_netmask = dict(required=True),
dhcp_first = dict(required=True),
dhcp_last = dict(required=True),
dhcp_reserved = dict(required=True, type='list'),
autostart = dict(required=True, type='bool'),
active = dict(required=True, type='bool'),
present = dict(required=True, type='bool'),
)
)
virt_net = VirtNet(module.params)
result = {'name': virt_net.params['name'],
'compare': 0,
'already_exists': False}
# XML domain file generation
err = virt_net.create_xml_file()
if len(err) != 0:
module.fail_json(msg=err)
if virt_net.get_virt_net():
result['already_exists'] = True
# Make module idempotent by comparison with existing file
out, err = virt_net.compare_xml_file()
if len(err) > 0:
module.fail_json(msg=err)
if out > 0:
# Differences spotted, this a new file
# Let's validate that new domain XML file
result['compare'] = out
err = virt_net.define_network()
if len(err) > 0:
# Some error has been encountered
module.fail_json(msg=err)
result['defined'] = True
result['changed'] = True
else:
# File does not exists yet
# Try to install XML file
err = virt_net.define_network()
if len(err) > 0:
# Some error has been encountered
module.fail_json(msg=err)
result['defined'] = True
result['changed'] = True
live_settings = virt_net.apply_live_settings()
result['live_settings'] = live_settings
# Eventually, we have succeed !
module.exit_json(**result)
if __name__ == '__main__':
main()