aboutsummaryrefslogtreecommitdiff
path: root/kvm-lab-create
blob: e574a4c9c6af2871e40c0d2f02875768a5765f1b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env bash
set -euo pipefail

URI="qemu:///system"

IMGROOT="/var/lib/libvirt/images"
BASEDIR="$IMGROOT/base"
LABDIR="$IMGROOT/lab"

TEMPLATE_DOMAIN="debian13-template"
TEMPLATE_BASE="$BASEDIR/debian13-template.qcow2"

usage() {
  cat <<'EOF'
Usage: kvm-lab-create <lab-name> [--base template|<prefix>|<domain>|<filename-in-base>|/abs/path.qcow2] [--start]

Base resolution order:
  1) template -> /var/lib/libvirt/images/base/debian13-template.qcow2
  2) /abs/path.qcow2 -> that file
  3) <filename in /var/lib/libvirt/images/base> -> /var/lib/libvirt/images/base/<filename>
  4) <domain name> -> vda disk path of that domain (virsh domblklist)
  5) <prefix> -> newest /var/lib/libvirt/images/base/<prefix>-base-*.qcow2
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }

NAME="${1:-}"
shift || true

BASESEL="template"
START="no"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --base)  BASESEL="${2:-}"; shift 2 ;;
    --start) START="yes"; shift ;;
    -h|--help) usage; exit 0 ;;
    *) die "Unknown argument: $1" ;;
  esac
done

[[ -n "$NAME" ]] || { usage; exit 1; }

DISK="${LABDIR}/${NAME}.qcow2"
XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")"

defined="no"
cleanup() {
  rm -f "$XML_TMP"
  if [[ "$defined" != "yes" && -e "$DISK" ]]; then
    echo "WARN: Removing orphan disk: $DISK"
    sudo rm -f "$DISK" || true
  fi
}
trap cleanup EXIT

resolve_base() {
  local sel="$1"

  if [[ "$sel" == "template" ]]; then
    echo "$TEMPLATE_BASE"; return 0
  fi

  if [[ "$sel" == /* ]]; then
    echo "$sel"; return 0
  fi

  if [[ -f "$BASEDIR/$sel" ]]; then
    echo "$BASEDIR/$sel"; return 0
  fi

  if virsh -c "$URI" dominfo "$sel" &>/dev/null; then
    local src
    src="$(virsh -c "$URI" domblklist "$sel" --details \
      | awk '$2=="disk" && $3=="vda" {print $4; exit}')"
    [[ -n "$src" ]] || die "Domain '$sel' found but could not resolve vda disk path."
    echo "$src"; return 0
  fi

  local latest
  latest="$(ls -1t "$BASEDIR"/"${sel}"-base-*.qcow2 2>/dev/null | head -n 1 || true)"
  [[ -n "$latest" ]] || die "Invalid --base '$sel' (no matching domain/file/promoted base in $BASEDIR)."
  echo "$latest"
}

# Preconditions
[[ -d "$LABDIR" ]] || die "Lab directory missing: $LABDIR"
[[ -d "$BASEDIR" ]] || die "Base directory missing: $BASEDIR"
if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then
  die "Template domain not found in $URI: $TEMPLATE_DOMAIN"
fi

BASE="$(resolve_base "$BASESEL")"
echo "Base selection: $BASESEL -> $BASE"

[[ -f "$BASE" ]] || die "Base image not found: $BASE"
sudo test -r "$BASE" || die "Base image not readable (via sudo): $BASE"

[[ ! -e "$DISK" ]] || die "Disk already exists: $DISK"
if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then
  die "Domain already exists: $NAME"
fi

# Create overlay
echo "Creating overlay disk: $DISK"
sudo qemu-img create -f qcow2 -F qcow2 -b "$BASE" "$DISK" >/dev/null
sudo chown libvirt-qemu:libvirt "$DISK"
sudo chmod 0660 "$DISK"

echo "Validating backing file..."
sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true

# Clone template XML and adjust
echo "Cloning domain XML from: $TEMPLATE_DOMAIN"
virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP"

# Update: name, disk source, network default
# - replace <name>...</name> once
# - replace template disk path with overlay path (global)
# - force network='default'
sed -i \
  -e "s|<name>${TEMPLATE_DOMAIN}</name>|<name>${NAME}</name>|" \
  -e "s|${TEMPLATE_BASE}|${DISK}|g" \
  -e "s|<source network='[^']*'|<source network='default'|g" \
  "$XML_TMP"

# Remove UUID and MAC addresses to avoid collisions
sed -i -E '/<uuid>[^<]*<\/uuid>/d' "$XML_TMP"
sed -i -E "s/<mac address='[^']*'\/>//g" "$XML_TMP"

# Remove filesystem passthrough (if any)
perl -0777 -i -pe 's/<filesystem[\s\S]*?<\/filesystem>\n?//g' "$XML_TMP"

# Validate substitutions happened
grep -q "<name>${NAME}</name>" "$XML_TMP" || die "XML name substitution failed"
grep -qF "$DISK" "$XML_TMP" || die "XML disk substitution failed"

# Define domain
echo "Defining domain: $NAME"
virsh -c "$URI" define "$XML_TMP" >/dev/null
defined="yes"

# Optional start
if [[ "$START" == "yes" ]]; then
  echo "Starting domain: $NAME"
  virsh -c "$URI" start "$NAME" >/dev/null
fi

echo "OK: Lab '$NAME' created (disk: $DISK)."
if [[ "$START" != "yes" ]]; then
  echo "To start: virsh -c $URI start $NAME"
fi