wg-mesh-test/virt.py
2021-09-05 23:12:56 +02:00

292 lines
11 KiB
Python

import uuid
from pathlib import Path
from typing import Optional, Iterable
import libvirt
from libvirt import virConnect, virDomain, virStoragePool
from cloud_config import gen_cloud_config, _gen_cloudinit_iso_image
def _gen_libvirt_volume_config(name: str, path: Path, backing_image: Path, capacity: int = 1000,
image_format: str = 'qcow2') -> str:
"""
creates the XML config for a volume that will hold the data of a vm.
:param name: volume name
:param path: path to save volume to
:param backing_image: path to a backing image to base this one on
:param capacity: capacity in MiB
:param image_format: qcow2, iso, raw or alike.
A list of valid values can be found here: https://libvirt.org/storage.html#StorageBackendFS
:return: XML config as string
"""
xml_config = f"""
<volume type='file'>
<name>{name}.{image_format}</name>
<capacity unit='MiB'>{capacity}</capacity>
<allocation unit='MiB'>0</allocation>
<target>
<path>{path}</path>
<format type='{image_format}'/>
</target>
<backingStore>
<path>{backing_image}</path>
<format type='qcow2'/>
</backingStore>
</volume>
"""
return xml_config
def _gen_libvirt_storage_pool_config(name: str, path: Path) -> str:
"""
create the libvirt XML config for a storage pool with directory backend.
:param name: name of the storage pool
:param path: the path to save images in the pool to
:return: XML config as string
"""
xml_config = f"""
<pool type='dir'>
<name>{name}</name>
<target>
<path>{path}</path>
</target>
</pool>
"""
return xml_config
def _gen_libvirt_config(name: str, hdd_path: Path, cdrom_path: Path, cpu: int = 1, mem: int = 512) -> str:
"""
return libvirt XML config for a virtual machine
:param name: name of the machine
:param hdd_path: path to the image to use as primary disk
:param cdrom_path: path to the cloud-init image
:param cpu: number of cpus
:param mem: amount of ram in MiB
:return: XML config as string
"""
vm_uuid = uuid.uuid4()
xml_config = f"""
<domain type="kvm">
<name>{name}</name>
<uuid>{vm_uuid}</uuid>
<metadata>
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
<libosinfo:os id="http://ubuntu.com/ubuntu/20.04"/>
</libosinfo:libosinfo>
</metadata>
<memory unit="MiB">{mem}</memory>
<currentMemory unit="MiB">{mem}</currentMemory>
<vcpu placement="static">{cpu}</vcpu>
<os>
<type arch="x86_64" machine="pc-q35-5.2">hvm</type>
</os>
<features>
<acpi/>
<apic/>
<vmport state="off"/>
</features>
<cpu mode="host-model" check="partial"/>
<clock offset="utc">
<timer name="rtc" tickpolicy="catchup"/>
<timer name="pit" tickpolicy="delay"/>
<timer name="hpet" present="no"/>
</clock>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<pm>
<suspend-to-mem enabled="no"/>
<suspend-to-disk enabled="no"/>
</pm>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<console type='pty'>
<target type='serial'/>
</console>
<video>
<model type="virtio" heads="1" primary="yes">
<acceleration accel3d="yes"/>
</model>
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0"/>
</video>
<channel type="spicevmc">
<target type="virtio" name="com.redhat.spice.0"/>
<address type="virtio-serial" controller="0" bus="0" port="2"/>
</channel>
<channel type="unix">
<target type="virtio" name="org.qemu.guest_agent.0"/>
<address type="virtio-serial" controller="0" bus="0" port="1"/>
</channel>
<graphics type="spice" autoport="yes">
<listen type="address"/>
<image compression="off"/>
</graphics>
<disk type="file" device="disk">
<driver name="qemu" type="qcow2"/>
<source file="{hdd_path}"/>
<target dev="vda" bus="virtio"/>
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0"/>
<boot order="1"/>
</disk>
<disk type="file" device="cdrom">
<driver name="qemu" type="raw"/>
<source file="{cdrom_path}"/>
<target dev="sda" bus="sata"/>
<readonly/>
<address type="drive" controller="0" bus="0" target="0" unit="0"/>
<boot order="2"/>
</disk>
<controller type="usb" index="0" model="ich9-ehci1">
<address type="pci" domain="0x0000" bus="0x00" slot="0x1d" function="0x7"/>
</controller>
<controller type="usb" index="0" model="ich9-uhci1">
<master startport="0"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x1d" function="0x0" multifunction="on"/>
</controller>
<controller type="usb" index="0" model="ich9-uhci2">
<master startport="2"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x1d" function="0x1"/>
</controller>
<controller type="usb" index="0" model="ich9-uhci3">
<master startport="4"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x1d" function="0x2"/>
</controller>
<controller type="sata" index="0">
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2"/>
</controller>
<controller type="pci" index="0" model="pcie-root"/>
<controller type="pci" index="1" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="1" port="0x10"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x0" multifunction="on"/>
</controller>
<controller type="pci" index="2" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="2" port="0x11"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x1"/>
</controller>
<controller type="pci" index="3" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="3" port="0x12"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x2"/>
</controller>
<controller type="pci" index="4" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="4" port="0x13"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x3"/>
</controller>
<controller type="pci" index="5" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="5" port="0x14"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x4"/>
</controller>
<controller type="pci" index="6" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="6" port="0x15"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x5"/>
</controller>
<controller type="virtio-serial" index="0">
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0"/>
</controller>
<interface type="network">
<source network="default"/>
<model type="virtio"/>
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
</interface>
<input type="mouse" bus="ps2"/>
<input type="keyboard" bus="ps2"/>
<audio id="1" type="spice"/>
<memballoon model="virtio">
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0"/>
</memballoon>
<rng model="virtio">
<backend model="random">/dev/urandom</backend>
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
</rng>
</devices>
</domain>
"""
return xml_config
def spawn_vm(connection: virConnect,
name: str,
storage_pool_path: Path,
wireguard_config: str,
ssh_authorized_keys: Optional[Iterable[str]] = None,
storage_pool_name: str = 'wireguard_test'
) -> virDomain:
"""
spawm a VM provisioned via cloud-init that is a mesh node
:param connection: libvirt connection object
:param name: name of the VM
:param storage_pool_path: path storage pool objects are saved in
:param wireguard_config: wireguard config file as string
:param ssh_authorized_keys: ssh public keys as list to add to the image
:param storage_pool_name: name of the storage pool (will be created if nonexistent)
:return: a libvirt virDomain object representing a virtual machine
"""
# if storage pool does not exist, create it. Otherwise, fetch it.
storage_pools = (p.name() for p in connection.listAllStoragePools())
if storage_pool_name not in storage_pools:
storage_pool_path.mkdir(exist_ok=True)
storage_pool_xml_config = _gen_libvirt_storage_pool_config(storage_pool_name, storage_pool_path)
storage_pool: virStoragePool = connection.storagePoolCreateXML(storage_pool_xml_config)
else:
storage_pool = connection.storagePoolLookupByName(storage_pool_name)
image_format = 'qcow2'
volume_path = storage_pool_path / f'{name}.{image_format}'
# if image exists, fetch and wipe it. Otherwise, create it.
if str(volume_path) in (v.path() for v in storage_pool.listAllVolumes()):
volume = storage_pool.storageVolLookupByName(volume_path.name)
volume.wipe()
else:
backing_file_path = Path('/tmp/focal-server-cloudimg-amd64.qcow2')
volume_xml_config = _gen_libvirt_volume_config(name, volume_path, backing_file_path, image_format=image_format)
# volume_create_flags = libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA
storage_pool.createXML(volume_xml_config)
# if the cloud-init images exist, delete them.
cloud_init_volume_name = f'cloudinit-{name}.iso'
try:
cloud_init_volume = storage_pool.storageVolLookupByName(cloud_init_volume_name)
cloud_init_volume.delete()
except libvirt.libvirtError:
pass
# recreate cloud-init images
cloud_init_iso_path = storage_pool_path / cloud_init_volume_name
user_data_path, meta_data_path = gen_cloud_config(name, Path('/tmp/'), wireguard_config, ssh_authorized_keys)
_gen_cloudinit_iso_image(cloud_init_iso_path, user_data_path, meta_data_path)
# generate VM XML config
vm_xml_config = _gen_libvirt_config(
name,
volume_path,
cloud_init_iso_path,
cpu=1,
mem=512
)
vm_create_flags = libvirt.VIR_DOMAIN_START_PAUSED
# create the virtual machine
# might use defineXML for persistence. VMs disappear after shutdown
vm = connection.createXML(vm_xml_config, flags=vm_create_flags)
return vm