mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
container: Add support for VMs using libvirt
- Use virsh command line tool to create and control VMs. - Use virtiofs for shared folder between host and guest. Tests: - Create a testing container and run unit tests on it. - Create a testing VM. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
parent
43d625f6f8
commit
296c25627e
439
container
439
container
@ -1,26 +1,33 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Script to manage systemd-nspawn container for FreedomBox development.
|
"""Script to manage a container or a VM for FreedomBox development.
|
||||||
|
|
||||||
This script creates a simple container using systemd-nspawn for developing
|
This script creates either a simple container using systemd-nspawn or a virtual
|
||||||
FreedomBox. It has many advantages over running a VM using Vagrant. RAM is
|
machine using libvirt for developing FreedomBox. Containers have many
|
||||||
allocated to processes in the container as needed without any fixed limit. Also
|
advantages over running a VM. RAM is allocated to processes in the container as
|
||||||
RAM does not have to be statically allocated so it is typically much lighter
|
needed without any fixed limit. Also RAM does not have to be statically
|
||||||
than running an VM. There is no hardware emulation when running a container
|
allocated so it is typically much lighter than running an VM. There is no
|
||||||
with same architecture, so processes run as fast as they would on the host
|
hardware emulation when running a container with same architecture, so
|
||||||
machine.
|
processes run as fast as they would on the host machine. On the other hand, VMs
|
||||||
|
have the advantage of full machine emulation. They allow full permissions as
|
||||||
|
required for mounting filesystems, USB passthrough of Wi-Fi devices, emulation
|
||||||
|
of multiple disks, etc. as required for testing some of the features of
|
||||||
|
FreedomBox.
|
||||||
|
|
||||||
Environment: The script will run only run on hosts having systemd-nspawn and
|
Environment: The script will run only run on hosts having systemd-nspawn,
|
||||||
network-manager installed, typical GNU/Linux distributions. It has been
|
virsh, and network-manager installed, typical GNU/Linux distributions. It has
|
||||||
primarily developed and tested on Debian Buster but should work on most modern
|
been primarily developed and tested on Debian Buster but should work on most
|
||||||
GNU/Linux distributions.
|
modern GNU/Linux distributions.
|
||||||
|
|
||||||
Disk image: systemd-nspawn accepts not only a directory for starting a
|
Disk image: For a container, systemd-nspawn accepts not only a directory for
|
||||||
container but also a disk image. This disk image is loop-back mounted and
|
starting a container but also a disk image. This disk image is loop-back
|
||||||
container is started from that mounted directory. The partition to use is
|
mounted and container is started from that mounted directory. The partition to
|
||||||
determined by looking at the boot flag in the partition table. This happens to
|
use is determined by looking at the boot flag in the partition table. This
|
||||||
work well with all existing FreedomBox images. In future, we may be able to run
|
happens to work well with all existing FreedomBox images. In future, we may be
|
||||||
different architectures in this manner.
|
able to run different architectures in this manner.
|
||||||
|
|
||||||
|
For a VM, a disk drive is created that is backed by the image file. The image
|
||||||
|
is a bootable image using GRUB as built by freedom-maker.
|
||||||
|
|
||||||
After downloading, the disk image is expanded along with the partition and file
|
After downloading, the disk image is expanded along with the partition and file
|
||||||
system inside so that development can be done without running into disk space
|
system inside so that development can be done without running into disk space
|
||||||
@ -35,32 +42,43 @@ script. Downloaded images are kept even after destroying the extracted raw
|
|||||||
image along with container. This allows for quickly resetting the container
|
image along with container. This allows for quickly resetting the container
|
||||||
without downloading again.
|
without downloading again.
|
||||||
|
|
||||||
Booting: systemd-nspawn is run in 'boot' mode. This means that init process
|
Booting: For a container, systemd-nspawn is run in 'boot' mode. This means that
|
||||||
(happens to be systemd) is started inside the container. It then spawns all the
|
init process (happens to be systemd) is started inside the container. It then
|
||||||
other necessary daemons including openssh-server, firewalld and
|
spawns all the other necessary daemons including openssh-server, firewalld and
|
||||||
network-manager. A login terminal can be opened using 'machinectl login'
|
network-manager. A login terminal can be opened using 'machinectl login'
|
||||||
because container is running systemd. SSH into the container is possible
|
because container is running systemd. SSH into the container is possible
|
||||||
because network is up, configured by network-manager, and openssh server is
|
because network is up, configured by network-manager, and openssh server is
|
||||||
running.
|
running.
|
||||||
|
|
||||||
Shared folder: Using systemd-nspawn, the project directory is mounted as
|
For a VM, when the virtual machine is started, the firmware of the machine
|
||||||
/freedombox inside the container. The project directory is determined as
|
boots the machine from the attached disk. The boot process is similar to a
|
||||||
directory in which this script resides. The project folder from the container
|
physical machine.
|
||||||
point of view will be read-only. Container should be able to write various
|
|
||||||
files such as build files into the /freedombox folder. To enable writing, an
|
|
||||||
additional read-write folder is overlayed onto /freedombox folder in the
|
|
||||||
container. This directory can't be created under the project folder and is
|
|
||||||
created instead in $XDG_DATA_HOME/freedombox-container/overlay/$DISTRIBUTION.
|
|
||||||
If XDG_DATA_HOME is not set, it is assumed to be $HOME/.local/shared/. Whenever
|
|
||||||
data is written into /freedombox directory inside the container, this directory
|
|
||||||
on the host receives the changes. See documentation for Overlay filesystem for
|
|
||||||
further details. When container is destroyed, this overlay folder is destroyed
|
|
||||||
to ensure clean state after bringing up the container again.
|
|
||||||
|
|
||||||
Users: PrivateUsers configuration flag for systemd-nspawn is currently off.
|
Shared folder: For a container, using systemd-nspawn, the project directory is
|
||||||
This means that each user's UID on the host is also the same UID in the
|
mounted as /freedombox inside the container. The project directory is
|
||||||
container as along as there is an entry in the container's password database.
|
determined as directory in which this script resides. The project folder from
|
||||||
In future, we may explore using private users inside the container.
|
the container point of view will be read-only. Container should be able to
|
||||||
|
write various files such as build files into the /freedombox folder. To enable
|
||||||
|
writing, an additional read-write folder is overlayed onto /freedombox folder
|
||||||
|
in the container. This directory can't be created under the project folder and
|
||||||
|
is created instead in
|
||||||
|
$XDG_DATA_HOME/freedombox-container/overlay/$DISTRIBUTION. If XDG_DATA_HOME is
|
||||||
|
not set, it is assumed to be $HOME/.local/shared/. Whenever data is written
|
||||||
|
into /freedombox directory inside the container, this directory on the host
|
||||||
|
receives the changes. See documentation for Overlay filesystem for further
|
||||||
|
details. When container is destroyed, this overlay folder is destroyed to
|
||||||
|
ensure clean state after bringing up the container again.
|
||||||
|
|
||||||
|
For a VM, the project directory is exposed into the virtual machine with the
|
||||||
|
mount token 'freedombox' using virtiofs. This is done as part of virtual
|
||||||
|
machine configuration. Inside the virtual machine, a systemd .mount unit will
|
||||||
|
mount the virtiofs filesystem using the 'freedombox' token onto the folder
|
||||||
|
/freedombox. The folder is read-write.
|
||||||
|
|
||||||
|
Users: In container, PrivateUsers configuration flag for systemd-nspawn is
|
||||||
|
currently off. This means that each user's UID on the host is also the same UID
|
||||||
|
in the container as along as there is an entry in the container's password
|
||||||
|
database. In future, we may explore using private users inside the container.
|
||||||
|
|
||||||
'fbx' is the development user and its UID is changed during setup phase to
|
'fbx' is the development user and its UID is changed during setup phase to
|
||||||
10000 hoping it would not match anything on the host system. 'fbx' user has
|
10000 hoping it would not match anything on the host system. 'fbx' user has
|
||||||
@ -74,16 +92,17 @@ whichever user owns the project directory. This allows the files to written by
|
|||||||
'plinth' container user in the project directory because UID of the owner of
|
'plinth' container user in the project directory because UID of the owner of
|
||||||
the directory is same as the 'plinth' user's UID in container.
|
the directory is same as the 'plinth' user's UID in container.
|
||||||
|
|
||||||
Network: A private network is created inside the container using systemd-nspawn
|
Network: For a container, a private network is created inside the container
|
||||||
feature. Network interfaces from the host are not available inside the
|
using systemd-nspawn feature. Network interfaces from the host are not
|
||||||
container. A new network interface called 'host0' is configured inside the
|
available inside the container. A new network interface called 'host0' is
|
||||||
container which is automatically configured by network-manager. On the host a
|
configured inside the container which is automatically configured by
|
||||||
new network interface is created. This script creates configuration for a
|
network-manager. On the host a new network interface is created. This script
|
||||||
'shared' network using network-manager. When bringing up the container, this
|
creates configuration for a 'shared' network using network-manager. When
|
||||||
network connection is also brought up. A DHCP server and a DNS server are
|
bringing up the container, this network connection is also brought up. A DHCP
|
||||||
started network-manager on the host side so that DHCP and DNS client functions
|
server and a DNS server are started network-manager on the host side so that
|
||||||
work inside the container. Traffic from the container is also masqueraded so
|
DHCP and DNS client functions work inside the container. Traffic from the
|
||||||
that Internet connectivity inside the container works if the host has one.
|
container is also masqueraded so that Internet connectivity inside the
|
||||||
|
container works if the host has one.
|
||||||
|
|
||||||
If necessary, the network interface on host side can be differently configured.
|
If necessary, the network interface on host side can be differently configured.
|
||||||
For example, it can be bridged with another interface to expose the container
|
For example, it can be bridged with another interface to expose the container
|
||||||
@ -95,6 +114,14 @@ messages. All ports in the container can be reached from the host using this IP
|
|||||||
address as long as the firewall inside the container allows it. There is no
|
address as long as the firewall inside the container allows it. There is no
|
||||||
need to perform port forwarding or mapping.
|
need to perform port forwarding or mapping.
|
||||||
|
|
||||||
|
For a VM, the network device is fully emulated. On the host it is exposed as
|
||||||
|
network interface that is bridged with the default libvirt bridge. The bridge
|
||||||
|
interface is configured by libvirt and it listens for DHCP requests from the
|
||||||
|
guests and also has a DNS server running. All traffic from the guest is NATed
|
||||||
|
and, as a result, the guest has full network access. The guest is accessible
|
||||||
|
from the host using the guest IP address which can be retrieved by asking
|
||||||
|
libvirt.
|
||||||
|
|
||||||
SSH: It is assumed that openssh-server is installed inside the container. SSH
|
SSH: It is assumed that openssh-server is installed inside the container. SSH
|
||||||
server keys in the container are created if missing. Client side keys are
|
server keys in the container are created if missing. Client side keys are
|
||||||
created in .container/ssh directory and the public key is installed in the
|
created in .container/ssh directory and the public key is installed in the
|
||||||
@ -102,12 +129,13 @@ authorized keys file of the 'fbx' user. The 'ssh' sub-command to this script is
|
|||||||
simply a convenience mechanism for quick launch of ssh with the right IP
|
simply a convenience mechanism for quick launch of ssh with the right IP
|
||||||
address, user name and identity file.
|
address, user name and identity file.
|
||||||
|
|
||||||
Role of machinectl: Most of the work is done by systemd-nspawn. machinectl is
|
Role of machinectl: For a container, most of the work is done by
|
||||||
useful for running systemd-nspawn in the background and querying its current
|
systemd-nspawn. machinectl is useful for running systemd-nspawn in the
|
||||||
state. It also helps with providing the IP address of the container. machinectl
|
background and querying its current state. It also helps with providing the IP
|
||||||
is made to recognize the container by creating a link in /var/lib/machines/ to
|
address of the container. machinectl is made to recognize the container by
|
||||||
the image file. systemd-nspawn options are added by creating a temporary file
|
creating a link in /var/lib/machines/ to the image file. systemd-nspawn options
|
||||||
in /run/systemd/nspawn. All machinectl commands should work.
|
are added by creating a temporary file in /run/systemd/nspawn. All machinectl
|
||||||
|
commands should work.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -251,6 +279,191 @@ chmod --silent a+w .coverage
|
|||||||
exit 0
|
exit 0
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
LIBVIRT_DOMAIN_XML_TEMPLATE = '''<domain type="kvm">
|
||||||
|
<name>{domain_name}</name>
|
||||||
|
<metadata>
|
||||||
|
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
|
||||||
|
<libosinfo:os id="http://debian.org/debian/testing"/>
|
||||||
|
</libosinfo:libosinfo>
|
||||||
|
</metadata>
|
||||||
|
<memory unit="MiB">{memory_mib}</memory>
|
||||||
|
<currentMemory unit="MiB">{memory_mib}</currentMemory>
|
||||||
|
<memoryBacking>
|
||||||
|
<source type="memfd"/>
|
||||||
|
<access mode="shared"/>
|
||||||
|
</memoryBacking>
|
||||||
|
<vcpu placement="static">{cpus}</vcpu>
|
||||||
|
<os>
|
||||||
|
<type arch="x86_64" machine="pc-q35-7.2">hvm</type>
|
||||||
|
<boot dev="hd"/>
|
||||||
|
</os>
|
||||||
|
<features>
|
||||||
|
<acpi/>
|
||||||
|
<apic/>
|
||||||
|
<vmport state="off"/>
|
||||||
|
</features>
|
||||||
|
<cpu mode="host-passthrough" check="none" migratable="on"/>
|
||||||
|
<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>
|
||||||
|
<disk type="file" device="disk">
|
||||||
|
<driver name="qemu" type="raw"/>
|
||||||
|
<source file="{image_file}"/>
|
||||||
|
<target dev="vda" bus="virtio"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0"/>
|
||||||
|
</disk>
|
||||||
|
<controller type="usb" index="0" model="qemu-xhci" ports="15">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0"/>
|
||||||
|
</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="pci" index="7" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="7" port="0x16"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x6"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="8" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="8" port="0x17"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x7"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="9" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="9" port="0x18"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x0" multifunction="on"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="10" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="10" port="0x19"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x1"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="11" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="11" port="0x1a"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x2"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="12" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="12" port="0x1b"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x3"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="13" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="13" port="0x1c"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x4"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="pci" index="14" model="pcie-root-port">
|
||||||
|
<model name="pcie-root-port"/>
|
||||||
|
<target chassis="14" port="0x1d"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x5"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="sata" index="0">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2"/>
|
||||||
|
</controller>
|
||||||
|
<controller type="virtio-serial" index="0">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0"/>
|
||||||
|
</controller>
|
||||||
|
<filesystem type="mount" accessmode="passthrough">
|
||||||
|
<driver type="virtiofs"/>
|
||||||
|
<source dir="{source_dir}"/>
|
||||||
|
<target dir="freedombox"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0"/>
|
||||||
|
</filesystem>
|
||||||
|
<interface type="network">
|
||||||
|
<source network="default"/>
|
||||||
|
<model type="virtio"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
|
||||||
|
</interface>
|
||||||
|
<serial type="pty">
|
||||||
|
<target type="isa-serial" port="0">
|
||||||
|
<model name="isa-serial"/>
|
||||||
|
</target>
|
||||||
|
</serial>
|
||||||
|
<console type="pty">
|
||||||
|
<target type="serial" port="0"/>
|
||||||
|
</console>
|
||||||
|
<channel type="unix">
|
||||||
|
<target type="virtio" name="org.qemu.guest_agent.0"/>
|
||||||
|
<address type="virtio-serial" controller="0" bus="0" port="1"/>
|
||||||
|
</channel>
|
||||||
|
<channel type="spicevmc">
|
||||||
|
<target type="virtio" name="com.redhat.spice.0"/>
|
||||||
|
<address type="virtio-serial" controller="0" bus="0" port="2"/>
|
||||||
|
</channel>
|
||||||
|
<input type="tablet" bus="usb">
|
||||||
|
<address type="usb" bus="0" port="1"/>
|
||||||
|
</input>
|
||||||
|
<input type="mouse" bus="ps2"/>
|
||||||
|
<input type="keyboard" bus="ps2"/>
|
||||||
|
<graphics type="spice" autoport="yes">
|
||||||
|
<listen type="address"/>
|
||||||
|
<image compression="off"/>
|
||||||
|
</graphics>
|
||||||
|
<sound model="ich9">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x1b" function="0x0"/>
|
||||||
|
</sound>
|
||||||
|
<audio id="1" type="spice"/>
|
||||||
|
<video>
|
||||||
|
<model type="virtio" heads="1" primary="yes"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0"/>
|
||||||
|
</video>
|
||||||
|
<redirdev bus="usb" type="spicevmc">
|
||||||
|
<address type="usb" bus="0" port="2"/>
|
||||||
|
</redirdev>
|
||||||
|
<redirdev bus="usb" type="spicevmc">
|
||||||
|
<address type="usb" bus="0" port="3"/>
|
||||||
|
</redirdev>
|
||||||
|
<memballoon model="virtio">
|
||||||
|
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
|
||||||
|
</memballoon>
|
||||||
|
<rng model="virtio">
|
||||||
|
<backend model="random">/dev/urandom</backend>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x06" slot="0x00" function="0x0"/>
|
||||||
|
</rng>
|
||||||
|
</devices>
|
||||||
|
</domain>
|
||||||
|
''' # noqa: E501
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -273,8 +486,10 @@ def parse_arguments() -> argparse.Namespace:
|
|||||||
subparser.add_argument('--distribution', choices=distributions,
|
subparser.add_argument('--distribution', choices=distributions,
|
||||||
default=default_distribution,
|
default=default_distribution,
|
||||||
help='Distribution to work with')
|
help='Distribution to work with')
|
||||||
subparser.add_argument('--machine-type', choices=('container', ),
|
subparser.add_argument(
|
||||||
default='container')
|
'--machine-type', choices=('container', 'vm'), default='container',
|
||||||
|
help='Type of the machine, container to virtual machine, to run '
|
||||||
|
'operation on')
|
||||||
|
|
||||||
# Up
|
# Up
|
||||||
subparser = subparsers.add_parser('up', help='Bring up the container',
|
subparser = subparsers.add_parser('up', help='Bring up the container',
|
||||||
@ -375,6 +590,7 @@ def _verify_dependencies() -> None:
|
|||||||
'dnsmasq': 'dnsmasq',
|
'dnsmasq': 'dnsmasq',
|
||||||
'ssh': 'openssh-client',
|
'ssh': 'openssh-client',
|
||||||
'ssh-keygen': 'openssh-client',
|
'ssh-keygen': 'openssh-client',
|
||||||
|
'virsh': 'libvirt-clients',
|
||||||
}
|
}
|
||||||
missing_commands = []
|
missing_commands = []
|
||||||
missing_packages = []
|
missing_packages = []
|
||||||
@ -744,6 +960,24 @@ def _setup_image(image_file: pathlib.Path):
|
|||||||
_runc(image_file, ['systemctl', 'disable', 'plinth'],
|
_runc(image_file, ['systemctl', 'disable', 'plinth'],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
logger.info('In image: Creating virtiofs mount at /freedombox')
|
||||||
|
mount = '''[Unit]
|
||||||
|
Description=FreedomBox development directory on host
|
||||||
|
ConditionVirtualization=kvm
|
||||||
|
|
||||||
|
[Mount]
|
||||||
|
What=freedombox
|
||||||
|
Where=/freedombox
|
||||||
|
Type=virtiofs
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'''
|
||||||
|
_runc(image_file, ['tee', '/usr/lib/systemd/system/freedombox.mount'],
|
||||||
|
input=mount.encode(), stdout=subprocess.DEVNULL)
|
||||||
|
_runc(image_file, ['systemctl', 'enable', 'freedombox.mount'],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
_setup_ssh(image_file)
|
_setup_ssh(image_file)
|
||||||
|
|
||||||
_setup_users(image_file)
|
_setup_users(image_file)
|
||||||
@ -884,6 +1118,9 @@ class Machine:
|
|||||||
if machine_type == 'container':
|
if machine_type == 'container':
|
||||||
return Container(distribution)
|
return Container(distribution)
|
||||||
|
|
||||||
|
if machine_type == 'vm':
|
||||||
|
return VM(distribution)
|
||||||
|
|
||||||
raise ValueError('Unknown machine type')
|
raise ValueError('Unknown machine type')
|
||||||
|
|
||||||
def get_status(self) -> bool:
|
def get_status(self) -> bool:
|
||||||
@ -1100,6 +1337,96 @@ VirtualEthernet=yes
|
|||||||
], check=False)
|
], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
class VM(Machine):
|
||||||
|
"""Handle VM specific operations."""
|
||||||
|
|
||||||
|
def get_status(self) -> bool:
|
||||||
|
"""Return whether the VM is currently running."""
|
||||||
|
process = self._virsh(['domstate', self.machine_name], check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
return process.stdout.decode().splitlines()[0] not in ('shut off', '')
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""Setup the infrastructure needed for the VM."""
|
||||||
|
try:
|
||||||
|
self._virsh(['dominfo', self.machine_name],
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return # Already exists
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
image_file = _get_image_file(self.distribution)
|
||||||
|
domain_xml = LIBVIRT_DOMAIN_XML_TEMPLATE.format(
|
||||||
|
domain_name=self.machine_name, memory_mib='2048', cpus='4',
|
||||||
|
image_file=image_file, source_dir=_get_project_folder())
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as file_handle:
|
||||||
|
file_handle.write(domain_xml.encode())
|
||||||
|
logger.info('Running `virsh define %s`', file_handle.name)
|
||||||
|
self._virsh(['define', file_handle.name],
|
||||||
|
stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def launch(self) -> None:
|
||||||
|
"""Start the VM."""
|
||||||
|
if self.get_status():
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Running `virsh start %s`', self.machine_name)
|
||||||
|
self._virsh(['start', self.machine_name], stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop the VM."""
|
||||||
|
if not self.get_status(): # Already shut off
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Running `virsh shutdown %s`', self.machine_name)
|
||||||
|
self._virsh(['shutdown', self.machine_name], stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def terminate(self) -> None:
|
||||||
|
"""Terminate, i.e., force stop the VM."""
|
||||||
|
if not self.get_status(): # Already shut off
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Running `virsh destroy %s`', self.machine_name)
|
||||||
|
self._virsh(['destroy', self.machine_name], stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def destory(self) -> None:
|
||||||
|
"""Remove all traces of the VM from the host."""
|
||||||
|
logger.info('Running `virsh undefine %s`', self.machine_name)
|
||||||
|
self._virsh(['undefine', self.machine_name], stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def get_ip_address(self) -> str | None:
|
||||||
|
"""Return the IP address assigned to the VM."""
|
||||||
|
try:
|
||||||
|
process = self._virsh(['domifaddr', self.machine_name],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lines = process.stdout.decode().splitlines()
|
||||||
|
if len(lines) < 3: # First two lines are header
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Example: 'vnet12 52:54:00:55:8c:68 ipv4 192.168.122.203/24'
|
||||||
|
return lines[2].rpartition(' ')[2].partition('/')[0]
|
||||||
|
|
||||||
|
def get_ssh_command(self) -> list[str]:
|
||||||
|
"""Return the SSH command to execute for the VM."""
|
||||||
|
ip_address = _wait_for(lambda: self.get_ip_address())
|
||||||
|
public_key = _get_work_directory() / 'ssh' / 'id_ed25519'
|
||||||
|
return [
|
||||||
|
'ssh', '-Y', '-C', '-t', '-i',
|
||||||
|
str(public_key), '-o', 'LogLevel=error', '-o',
|
||||||
|
'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null',
|
||||||
|
'-o', 'IdentitiesOnly=yes', f'fbx@{ip_address}'
|
||||||
|
]
|
||||||
|
|
||||||
|
def _virsh(self, args: list[str], check=True, **kwargs):
|
||||||
|
"""Run virsh to control the virtual machine."""
|
||||||
|
return subprocess.run(['sudo', 'virsh'] + args, check=check, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def subcommand_up(arguments: argparse.Namespace):
|
def subcommand_up(arguments: argparse.Namespace):
|
||||||
"""Download, setup and bring up the container."""
|
"""Download, setup and bring up the container."""
|
||||||
machine = Machine.get_instance(arguments.machine_type,
|
machine = Machine.get_instance(arguments.machine_type,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user