292 lines
11 KiB
Python
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
|