Browse Source

v0.1

master v0.1
Doug Le Tough 1 year ago
parent
commit
babf45f707
4 changed files with 453 additions and 3 deletions
  1. +175
    -3
      README.md
  2. +3
    -0
      TODO.md
  3. +12
    -0
      kvm_net.xml
  4. +263
    -0
      virt_net.py

+ 175
- 3
README.md View File

@ -1,5 +1,177 @@
# virt_net
# virt_net
## _Ansible_ module using libvirt to manage QEMU/KVM virtual networks.
## _Ansible_ module using libvirt to manage QEMU/KVM networks
This module allow basic QEMU/KVM network management and comes in combination with the [`virt_vm` _Ansible_ module](https://git.redatomik.org/mcos/virt_vm).
This module **does not** intend to cover every _libvirt_ networks aspects and capabilities.
The objective of this module is to offer a suitable response to [MCO System](https://www.mcos.nc) internal virtual networks configuration problematics.
There is **no plan** to make this module part of the community _Ansible_ modules collection and, therefore, it will **not** be published to the _Ansible Galaxy_ module collection.
This _Ansible_ module is released under the [***WTFPL***](http://www.wtfpl.net/) and you are encouraged to use it if it suits you needs.
You also may consider forking this module to adapt it to your specific needs.
## Requirements
### Packages
The following packages are mandatory for this modules to be used (_Centos 8_ naming convention).
- libvirt
- libvirt-client
- libvirt-daemon-driver-qemu
- python3-libvirt
- qemu-img
- qemu-kvm
- qemu-kvm-core
- qemu-kvm-common
- qemu-kvm-block-iscsi
- qemu-kvm-block-ssh
- qemu-kvm-block-gluster
- qemu-kvm-block-rbd
- qemu-kvm-block-curl
- ipxe-roms-qemu
- python3-libvirt
- python3-lxml
### Python3 modules
The following module is not available in basic _Centos 8_ packages repositories and must be installed with `pip3`
- xmldiff
## Features
- Generates and define persistent [_libvirt_ network](https://libvirt.org/docs/libvirt-appdev-guide-python/en-US/html/) from XML definition
- Hot configuration modification and application whenever possible
- Idempotent module
### Usage
The module needs to have access to a template XML file (`kvm_net.xml`) stored in `libvirt_tmp_path`.
This template XML file **must be deployed by a previous task**.
The module needs to access _XML_ domain files. These files will be searched in `libvirt_qemu_conf_path`.
## Limitations
This module is **not** a complete implementation of _libvirt_ and some _libvirt_ parameters are not available.
This module does **not** make any consistency ckeck on parameters. It's up to users to provide a consistent configuration to prevent failed module run or inconsistent network state. However, _libvirt_ validates _XML_ network files before defining the conresponding network so an invalid configuration would not be possible.
[Registering a variable](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#registering-variables) may be useful for debugging purpose (especially the `live_settings` dict that will show what has been successfully, or not, applied to the running network).
## Known bugs
Most bugs are unknown :D
Please, send reports or patches to [contact@at mcos.nc](/dev/null)
## Parameters
| var | object | type |
| ----------------- | ------ | ---- |
| `uuid` | Network unique identifier | **str**. Must be UUID formated |
| `name` | Network name | **str**. Should **not** contain space characters |
| `mode` | Forward mode of the bridge interface (`nat` is actually the only supported mode and is default). See [documentation](https://libvirt.org/formatnetwork.html) | **str** |
| `bridge_name` | Name of the bridge interface that will be created | **str** |
| `mac_addr` | _MAC_ address for the bridge interface | **str** |
| `dhcp_addr` | _DHCP IP_ address used bay _DHCP_ server | **str** |
| `dhcp_netmask` | Netmask for _DHCP_ serveur network | **str** |
| `dhcp_first` | First available _IP_ address of the _DHCP_ range | **str** |
| `dhcp_last` | Last available _IP_ address of the _DHCP_ range | **str** |
| `dhcp_reserved` | List of 3 keys dicts such as {'mac_addr': ''<VALUE>', 'hostname': '<VALUE>', 'ip_addr': '<VALUE>'} | **list of dicts** |
| `active` | Define network state. Set to `False`, network will be immediately destroyed. Set to `True`, network will be started | **bool** |
| `autostart` | if set to `True`, network will auto start with _libvirt_ daemon | **bool** |
| `present` | If set to `False`, existing network will be undefined and deleted. This operation **is not** recoverable. | **bool** |
## Exemples
The following example will create the `default` network. This network will be configured in `nat` mode and will use a bridge interface named `virbr0` with _MAC_ address `00:71:37:70:50:01`.
The _DHCP_ server for this network will be `192.168.122.1/24` and its _IP_ range will go from `192.168.122.2` to `192.168.122.254`.
This network will reserve some _IP_ addresses for the hosts specified by the `dhcp_reserved` list.
```yaml
---
# vars/kvm_base.yml
# -- ---------------------------------------------------------------------------
libvirt_tmp_path: /var/lib/libvirt/tmp
libvirt_qemu_conf_path: /etc/libvirt/qemu
```
```yaml
---
# vars/kvm_net.yml
# -- ---------------------------------------------------------------------------
kvm_nets:
- uuid: 'a45c13c9-6e30-48fd-962b-374194280b6e'
name: default
mode: nat
bridge_name: virbr0
mac_addr: 00:71:37:70:50:01
ip_addr: 192.168.122.1
netmask: 255.255.255.0
dhcp_reserved:
- mac_addr: '00:71:37:05:00:01'
hostname: test_vm
ip_addr: 192.168.122.2
- mac_addr: '00:71:37:05:00:02'
hostname: test2_vm
ip_addr: 192.168.122.3
active: yes
auto_start: yes
present: yes
```
```yaml
---
# roles/vm_host/tasks/kvm_net.yml
# -- ---------------------------------------------------------------------------
- name: include kvm_base vars
include_vars: kvm_base.yml
- name: include kvm_net vars
include_vars: kvm_net.yml
- name: create libvirt tmp dir
file:
path: "{{ libvirt_tmp_path }}"
state: directory
owner: root
group: root
mode: 0755
- name: base network XML file installation
copy:
src: files/kvm_net.xml
dest: "{{ libvirt_tmp_path }}/"
owner: root
group: root
mode: '0644'
- name: KVM network configuration
virt_net:
uuid: "{{ item.uuid }}"
name: "{{ item.name }}"
mode: "{{ item.mode }}"
bridge_name: "{{ item.bridge_name }}"
mac_addr: "{{ item.mac_addr }}"
dhcp_addr: "{{ item.dhcp_addr }}"
dhcp_netmask: "{{ item.dhcp_mask }}"
dhcp_first: "{{ item.dhcp_first }}"
dhcp_last: "{{ item.dhcp_last }}"
dhcp_reserved: "{{ item.dhcp_reservation }}"
autostart: "{{ item.autostart }}"
active: "{{ item.active }}"
present: "{{ item.present }}"
libvirt_tmp_path: "{{ item.libvirt_tmp_path | default(libvirt_tmp_path) }}"
register: net_creation
loop: "{{ kvm_nets }}"
```

+ 3
- 0
TODO.md View File

@ -0,0 +1,3 @@
# TODO:
## Road map:

+ 12
- 0
kvm_net.xml View File

@ -0,0 +1,12 @@
<network>
<name></name>
<uuid></uuid>
<forward mode='nat'/>
<bridge name='virbr0' stp='on' delay='0'/>
<mac address=''/>
<ip address='' netmask=''>
<dhcp>
<range start='' end=''/>
</dhcp>
</ip>
</network>

+ 263
- 0
virt_net.py View File

@ -0,0 +1,263 @@
#!/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()

Loading…
Cancel
Save