# Virsh

Virtual Machine Management

- [Virsh](#virsh)
  - [Before you Begin](#before-you-begin)
  - [Connecting to External Servers via SSH](#connecting-to-external-servers-via-ssh)
    - [Configuring Aliases](#configuring-aliases)
    - [One-off Connections](#one-off-connections)
  - [Useful Virsh Commands](#useful-virsh-commands)
  - [Virsh Networking](#virsh-networking)
    - [Create a Virtual Network](#create-a-virtual-network)
    - [Attach a New Virtual Network](#attach-a-new-virtual-network)
    - [Detach a Virtual Network](#detach-a-virtual-network)
    - [Destroy a Virtual Network](#destroy-a-virtual-network)
    - [Set a Static IP](#set-a-static-ip)
  - [Creating VMs](#creating-vms)
    - [Create VM with No Graphics and use an Existing QCOW2 Disk](#create-vm-with-no-graphics-and-use-an-existing-qcow2-disk)
    - [Create a Cloud Init Compatible VM](#create-a-cloud-init-compatible-vm)
    - [Create VM with Graphics using an ISO Installation Disk](#create-vm-with-graphics-using-an-iso-installation-disk)
    - [Create VM using Host Device as Disk](#create-vm-using-host-device-as-disk)
    - [Create a Home Assistant VM](#create-a-home-assistant-vm)
  - [Snapshots](#snapshots)
  - [Creating and Attaching Disks](#creating-and-attaching-disks)
    - [Disk Testing and Scripts](#disk-testing-and-scripts)
  - [Virt Builder](#virt-builder)

## Before you Begin

1. Add yourself to the `qemu` and `libvirt` groups: `usermod -aG libvirt,qemu ducoterra`
2. Change the images ownership to qemu: `chown -R qemu:qemu /var/lib/libvirt/images`
3. Change the iso ownership to qemu: `chown -R qemu:qemu /var/lib/libvirt/iso`
4. Allow group write access to images: `chmod 770 /var/lib/libvirt/images`
5. Allow group write access to iso: `chmod 770 /var/lib/libvirt/iso`
6. Tell virsh to connect to your root system rather than your user: `export LIBVIRT_DEFAULT_URI='qemu:///system'`
7. Export your editor so virsh knows what to use: `export EDITOR=vim`

## Connecting to External Servers via SSH

<https://libvirt.org/uri.html#ssh-transport>

### Configuring Aliases

1. Edit `~/.config/libvirt/libvirt.conf`
2. Add your aliases

    ```bash
    uri_aliases = [
        "3dserver=qemu+ssh://3dserver/system",
    ]
    ```

3. Export the alias: `export LIBVIRT_DEFAULT_URI=3dserver`

### One-off Connections

```bash
export LIBVIRT_DEFAULT_URI='qemu+ssh://user@server/system'
```

Or for Truenas

```bash
export LIBVIRT_DEFAULT_URI='qemu+ssh://root@truenas/system?socket=/run/truenas_libvirt/libvirt-sock'
```

## Useful Virsh Commands

```bash
# Show node info
virsh nodeinfo

# List OS variants
osinfo-query os

# List all current machines
virsh list --all

# Connect to console VM
virsh console fedora42-test

# Connect to graphical VM
virt-viewer --wait fedora42-test

# Get leased IP Addresses for the default network
virsh net-dhcp-leases default

# Reboot a VM
virsh reboot <domain>

# Shutdown a VM
virsh shutdown <domain>

# Force shutdown a VM
virsh destroy <domain>

# Remove a VM
virsh undefine --nvram <domain>

# Remove a VM including storage
virsh undefine <domain> --nvram --remove-all-storage
```

## Virsh Networking

### Create a Virtual Network

Creating a new network will require an XML configuration file. To see the
default network's configuration, use 

```bash
virsh net-dumpxml default > virbr0.xml
```

To create a dual-stack network, use the following. (Note, I generated a unique
local ipv6 address [here](https://www.unique-local-ipv6.com/)).

```xml
<network>
  <name>dual-stack</name>
  <forward mode="nat"/>
  <domain name="dual-stack"/>
  <ip address="192.168.100.1" netmask="255.255.255.0">
    <dhcp>
      <range start="192.168.100.2" end="192.168.100.254"/>
    </dhcp>
  </ip>
  <ip family="ipv6" address="fd4d:58e7:17f6:1::1" prefix="64"/>
</network>
```

I've already defined this network in `active/software_virsh/dual-stack-dhcp.xml`. Install it with

```bash
# Define and autostart the network
virsh net-define active/software_virsh/dual-stack-dhcp.xml
virsh net-start dual-stack-dhcp
virsh net-autostart dual-stack-dhcp

# List networks to ensure it created
virsh net-list --all

# Get the UUID of the created network
virsh net-uuid dual-stack-dhcp
```

### Attach a New Virtual Network

```bash
export VM_NAME=my_vm
virsh attach-interface \
--type bridge \
--source virbr1 \
--model virtio \
--config \
--live \
--domain ${VM_NAME}
```

### Detach a Virtual Network

```bash
# List mac addresses of connected interfaces'
export VM_NAME=my_vm
virsh domiflist --domain $VM_NAME
virsh detach-interface --domain k0s-worker0 --type bridge --mac "52:54:00:f6:b9:83" --live
```

### Destroy a Virtual Network

```bash
export NETWORK_NAME=mynetwork
virsh net-undefine --network $NETWORK_NAME
virsh net-destroy --network $NETWORK_NAME
```

### Set a Static IP

To set a static IP, run `virsh net-edit default` and add the following between `<dhcp>` and `</dhcp>`

```bash
# Add a host
virsh net-update default add-last ip-dhcp-host \
      '<host mac="52:54:00:6f:78:f3" ip="192.168.122.222"/>' \
      --live --config --parent-index 0

# Modify a host
virsh net-update default modify ip-dhcp-host \
      '<host mac="52:54:00:6f:78:f3" ip="192.168.122.222"/>' \
      --live --config --parent-index 0

# Delete a host
virsh net-update default delete ip-dhcp-host \
      '<host mac="52:54:00:6f:78:f3" ip="192.168.122.222"/>' \
      --live --config --parent-index 0
```

## Creating VMs

If you have [an osbuild
image](/active/software_osbuild/image_builder.md#installing) you can run the
following to generate a qcow2 disk image. Then you can [create a VM with an
existing qcow2
disk](#create-vm-with-no-graphics-and-use-an-existing-qcow2-disk) and skip the
installation process altogether.

```bash
sudo systemctl start osbuild-composer.socket
composer-cli compose list
export IMAGE_UUID=
export VM_DISK_PATH=/var/lib/libvirt/images/fedora43-test.qcow2
composer-cli compose image --filename ${VM_DISK_PATH} ${IMAGE_UUID}
```

### Create VM with No Graphics and use an Existing QCOW2 Disk

```bash
# Start the  default network if it isn't already
virsh net-start --network default

export VM_NAME="fedora43-test"
export VM_DISK_PATH=/var/lib/libvirt/images/fedora43-test.qcow2

# OPTIONAL: export your qcow2 disk now if using osbuild
export IMAGE_UUID=
composer-cli compose image --filename ${VM_DISK_PATH} ${IMAGE_UUID}

# Install
# `--location /path/to/image.iso` supplies a disk installer. (Remove `--import`)
# `--import` skips the installation process.
# `--graphics spice --video qxl,model.ram=131072,model.vram=131072,model.vgamem=131072 --channel spicevmc` installs graphics
# `--console pty,target.type=virtio` adds a console connection
# For any command, use `virt-install --arg=?` to see all available options
virt-install \
--name "${VM_NAME}" \
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
--ram=8192 \
--os-variant=fedora41 \
--network bridge:virbr0 \
--graphics none \
--console pty,target.type=virtio \
--import --disk "path=${VM_DISK_PATH},bus=virtio"
```

### Create a Cloud Init Compatible VM

<https://cloudinit.readthedocs.io/en/latest/reference/examples.html>

```bash
# Fedora
# https://fedoraproject.org/cloud/download
export VM_NAME="cloud-init-test-fedora"
export VM_DISK_PATH=/var/lib/libvirt/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2

# Rocky
# https://rockylinux.org/download
export VM_NAME="cloud-init-test-rocky"
export VM_DISK_PATH=/var/lib/libvirt/images/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2

# Ubuntu
# https://cloud-images.ubuntu.com/noble/current/
export VM_NAME="cloud-init-test-ubuntu"
export VM_DISK_PATH=/var/lib/libvirt/images/noble-server-cloudimg-amd64.img

# Debian
# https://cloud.debian.org/images/cloud/trixie/20251117-2299/
export VM_NAME="cloud-init-test-debian"
export VM_DISK_PATH=/var/lib/libvirt/images/debian-13-generic-amd64-20251117-2299.qcow2

# Set --cloud-init disable=no to allow cloud-init to run again after first boot
virt-install \
--name "${VM_NAME}" \
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
--ram=8192 \
--os-variant=fedora41 \
--network bridge:virbr0 \
--graphics none \
--import --disk "path=${VM_DISK_PATH},bus=virtio" \
--cloud-init disable=yes,user-data="active/software_virsh/cloud-init/user-data,meta-data=active/software_virsh/cloud-init/meta-data"
```

### Create VM with Graphics using an ISO Installation Disk

```bash
# `--cdrom /path/to/image.iso` supplies a disk installer. (Remove `--import`)
# `--import` skips the installation process.
# `--graphics spice --video qxl --channel spicevmc` installs graphics
# `--console pty,target.type=virtio` adds a console connection
# For any command, use `virt-install --arg=?` to see all available options
export VM_NAME="fedora43-kinoite-test"
export VM_ISO_PATH=/var/lib/libvirt/iso/fedora43.iso
export VM_DISK_PATH=/var/lib/libvirt/images/fedora43.qcow2
virt-install \
--name "${VM_NAME}" \
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
--ram=8192 \
--os-variant=fedora41 \
--network bridge:virbr0 \
--graphics spice --video virtio --channel spicevmc \
--cdrom ${VM_ISO_PATH} \
--disk "path=${VM_DISK_PATH},size=64,bus=virtio,format=qcow2"
```

### Create VM using Host Device as Disk

```bash
# `--cdrom /path/to/image.iso` supplies a disk installer. (Remove `--import`)
# `--import` skips the installation process.
# `--graphics spice --video qxl --channel spicevmc` installs graphics
# `--console pty,target.type=virtio` adds a console connection
# `--hostdev 0x1234:0x5678` adds a block storage device
# For any command, use `virt-install --arg=?` to see all available options
export VM_NAME="usb-linux"
virt-install \
--name "${VM_NAME}" \
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
--import \
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
--ram=8192 \
--os-variant=fedora41 \
--network bridge:virbr0 \
--graphics spice --video qxl --channel spicevmc \
--hostdev 0x13fe:0x6500,boot.order=1 \
--disk none
```

### Create a Home Assistant VM

```bash
virt-install \
--name haos \
--description "Home Assistant OS" \
--os-variant=generic \
--ram=4096 \
--vcpus=2 \
--disk /var/lib/libvirt/images/haos_ova-16.3.qcow2,bus=scsi \
--controller type=scsi,model=virtio-scsi \
--import \
--graphics none \
--boot uefi
```

## Snapshots

See [qemu qcow2 snapshots](/active/software_qemu/qemu.md#qcow2-snapshots)

## Creating and Attaching Disks

To create and attach one disk:

```bash
# QCOW
qemu-img create -f qcow2 /var/lib/libvirt/images/my-disk.qcow2 1G

# Raw (better performance, no snapshots or copy on write)
qemu-img create -f raw /var/lib/libvirt/images/my-disk.raw 1G

# Query disk info (like actual size on disk)
qemu-img info /var/lib/libvirt/images/my-disk.raw

virsh attach-disk ${VM_NAME} \
--source /var/lib/libvirt/images/${VM_DISK_NAME} \
--target vdb \
--persistent
--live
```

### Disk Testing and Scripts

To create and attach multiple disks (for raid testing)

```bash
export VM_NAME="cloud-init-test-fedora"
# Max supported for this script is 25
export VM_NUM_DISKS=8
export VM_DISK_SIZE=4G

##### Attach #####
# Create the disks and target mounts from our array
letters=($(echo {a..z}))
for disk_num in $(seq 1 $VM_NUM_DISKS); do
    VM_DISK_NAME="test-${disk_num}"
    VM_DISK_TARGET=vd${letters[$disk_num]}

    echo "Creating /var/lib/libvirt/images/${VM_DISK_NAME}.qcow2"
    sudo qemu-img create -f qcow2 /var/lib/libvirt/images/${VM_DISK_NAME}.qcow2 ${VM_DISK_SIZE}

    echo "Attaching vd${letters[$disk_num]} to ${VM_NAME}"
    virsh attach-disk ${VM_NAME} \
    --source /var/lib/libvirt/images/${VM_DISK_NAME}.qcow2 \
    --target vd${letters[$disk_num]} \
    --persistent \
    --subdriver qcow2 \
    --live
done;

##### Cleanup #####
# Detach the disks from our VMs
letters=($(echo {a..z}))
for disk_num in $(seq 1 $VM_NUM_DISKS); do
    VM_DISK_NAME="test-${disk_num}"
    VM_DISK_TARGET=vd${letters[$disk_num]}

    echo "Detaching vd${letters[$disk_num]} from ${VM_NAME}"
    virsh detach-disk ${VM_NAME} \
    --target vd${letters[$disk_num]} \
    --persistent
done;

# Optionally delete images
letters=($(echo {a..z}))
for disk_num in $(seq 1 $VM_NUM_DISKS); do
    VM_DISK_NAME="test-${disk_num}"
    VM_DISK_TARGET=vd${letters[$disk_num]}

    echo "Removing /var/lib/libvirt/images/${VM_DISK_NAME}.qcow2"
    sudo rm /var/lib/libvirt/images/${VM_DISK_NAME}.qcow2
done;
```

## Virt Builder

<https://docs.fedoraproject.org/en-US/fedora-server/virtualization/vm-install-diskimg-virtbuilder/#_minimal_effort_customization>

You can use virt-builder to build vm images

```bash
export VM_NAME=fedora42-vb
export VM_DISK_PATH=/var/lib/libvirt/images/fedora42-vb.qcow2

# Build the image
virt-builder fedora-42 \
--format qcow2 --output ${VM_DISK_PATH} \
--root-password locked:disabled \
--hostname ${VM_NAME} \
--selinux-relabel \
--firstboot-command 'useradd -m -G wheel -p "" ducoterra ; chage -d 0 ducoterra'

# Run the built image
virt-install \
--name "${VM_NAME}" \
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
--ram=8192 \
--os-variant=fedora41 \
--network bridge:virbr0 \
--graphics none \
--console pty,target.type=virtio \
--import --disk "path=${VM_DISK_PATH},bus=virtio"
```