Created
July 18, 2026 18:04
-
-
Save smith153/04b4068b5a2d7b234f1c3d5992dafe25 to your computer and use it in GitHub Desktop.
Libvirt Powered Claude Sandbox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # uat provisioner — LXQt + Chrome acceptance-testing desktop under qemu:///system. | |
| # Runs entirely as your normal user (no sudo): disks live in a path you own, | |
| # and libvirtd (already root) does the privileged VM/network work. | |
| # Keep uat-cloud-init.yaml and uat-net.xml next to this script. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Pin a clean PATH so a custom python (pyenv/conda/etc.) can't shadow the system | |
| # python3 this script uses to render cloud-init. | |
| export PATH=/home/user/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games/:/usr/sbin/ | |
| VM=uat | |
| RAM=8192 # MB boot/resting target -> <currentMemory> (Chrome is hungry) | |
| MAXRAM=10240 # MB runtime ceiling -> <memory> | |
| VCPUS=4 | |
| DISK_GB=10 | |
| IMG_DIR=/home/Programs/VM/storage | |
| BASE=debian-13-generic-amd64.qcow2 | |
| # Official host cloud.debian.org is frequently down/slow. This ICM mirror is | |
| # reliable; swap back to the official URL if it ever lags. | |
| BASE_URL="https://ftp.icm.edu.pl/packages/linux-debian-cdimage/cloud/trixie/latest/${BASE}" | |
| #BASE_URL="https://cloud.debian.org/images/cloud/trixie/latest/${BASE}" | |
| MIN_SIZE=$((100 * 1024 * 1024)) # a real image is ~300 MB+; reject truncated files | |
| DISK="${IMG_DIR}/${VM}.qcow2" | |
| NET=uat-net | |
| CONN=qemu:///system | |
| HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| CI_TEMPLATE="${HERE}/uat-cloud-init.yaml" | |
| CI_RENDERED="${HERE}/.uat-cloud-init.rendered.yaml" | |
| msg(){ printf '\033[1;36m==>\033[0m %s\n' "$*"; } | |
| die(){ printf '\033[1;31mERR\033[0m %s\n' "$*" >&2; exit 1; } | |
| trap 'rm -f "$CI_RENDERED"' EXIT | |
| # ── Teardown: completely remove the VM (storage + snapshots) and its network ── | |
| do_delete() { | |
| command -v virsh >/dev/null || die "virsh not found; nothing to tear down with." | |
| # Confirm before doing anything destructive, unless --force/--yes was given. | |
| if (( ! FORCE )); then | |
| printf '\033[1;31m' | |
| printf 'This will PERMANENTLY delete VM %s (disk + all snapshots) and network %s.\n' "$VM" "$NET" | |
| printf '\033[0m' | |
| if [[ -t 0 ]]; then | |
| read -r -p "Type 'y' to proceed [y/N]: " reply | |
| case "$reply" in | |
| y|Y|yes|YES) ;; | |
| *) die "Aborted; nothing was deleted." ;; | |
| esac | |
| else | |
| die "Refusing to delete without a TTY. Re-run with --force to skip the prompt." | |
| fi | |
| fi | |
| # Domain: destroy if running (else undefine would leave a transient VM), then | |
| # undefine with its storage and snapshot metadata. | |
| if virsh -c "$CONN" dominfo "$VM" >/dev/null 2>&1; then | |
| state=$(virsh -c "$CONN" domstate "$VM" 2>/dev/null || echo unknown) | |
| if [[ "$state" != "shut off" ]]; then | |
| msg "Domain $VM is '$state'; destroying it first" | |
| virsh -c "$CONN" destroy "$VM" >/dev/null 2>&1 || true | |
| fi | |
| msg "Undefining $VM (removing all storage + snapshot metadata)" | |
| virsh -c "$CONN" undefine "$VM" --remove-all-storage --snapshots-metadata \ | |
| || die "Failed to undefine $VM" | |
| else | |
| msg "Domain $VM not defined; skipping undefine" | |
| fi | |
| # Belt-and-suspenders: drop the per-VM disk if undefine didn't (e.g. the disk | |
| # wasn't in a libvirt-managed pool). The shared base image is never touched. | |
| if [[ -f "$DISK" ]]; then | |
| msg "Removing leftover disk $DISK" | |
| rm -f "$DISK" | |
| fi | |
| # NAT network: this net is dedicated to this VM, so stop + undefine it. Always | |
| # destroy BEFORE undefining: 'net-undefine' on a still-running network only | |
| # strips its persistent config and leaves it running as a *transient* network, | |
| # which then blocks the next provision. Both steps are idempotent (|| true). | |
| if virsh -c "$CONN" net-info "$NET" >/dev/null 2>&1; then | |
| msg "Stopping network $NET (if running)" | |
| virsh -c "$CONN" net-destroy "$NET" >/dev/null 2>&1 || true | |
| msg "Undefining network $NET (if persistent)" | |
| virsh -c "$CONN" net-undefine "$NET" >/dev/null 2>&1 || true | |
| else | |
| msg "Network $NET not defined; skipping" | |
| fi | |
| msg "Teardown complete. Base image ${IMG_DIR}/${BASE} left in place for reuse." | |
| } | |
| # ── Argument parsing ───────────────────────────────────────────────────────── | |
| DELETE=0 | |
| FORCE=0 | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --delete) DELETE=1 ;; | |
| --force|--yes|-y) FORCE=1 ;; | |
| -h|--help) | |
| cat <<EOF | |
| Usage: ${0##*/} [--delete [--force]] | |
| (no args) Provision the ${VM} VM: fetch base image, create disk, define | |
| the ${NET} NAT network, and launch via virt-install. | |
| --delete COMPLETELY remove ${VM}: destroy it if running, undefine it | |
| with all storage and snapshot metadata, and tear down the | |
| ${NET} network. Prompts for confirmation. The shared base | |
| image (${BASE}) is left in place. | |
| --force, --yes, -y | |
| Skip the --delete confirmation prompt (for scripted/no-TTY use). | |
| EOF | |
| exit 0 ;; | |
| *) die "Unknown argument: $arg (try --help)" ;; | |
| esac | |
| done | |
| if (( FORCE )) && (( ! DELETE )); then | |
| die "--force/--yes only applies to --delete." | |
| fi | |
| if (( DELETE )); then | |
| do_delete | |
| exit 0 | |
| fi | |
| # 1. Host tooling ------------------------------------------------------------ | |
| msg "Checking host tools" | |
| missing=() | |
| for t in virt-install virsh qemu-img qemu-system-x86_64 wget osinfo-query python3; do | |
| command -v "$t" >/dev/null || missing+=("$t") | |
| done | |
| if ((${#missing[@]})); then | |
| die "Missing: ${missing[*]} | |
| Install the stack with: | |
| sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst \\ | |
| libosinfo-bin wget python3 virt-viewer \\ | |
| qemu-system-modules-spice | |
| sudo adduser \"\$USER\" libvirt # then log out/in (or: newgrp libvirt)" | |
| fi | |
| # virt-viewer is the SPICE client for connecting to the desktop: | |
| command -v virt-viewer >/dev/null || msg "NOTE: 'virt-viewer' not found — install it to view the desktop: sudo apt install virt-viewer" | |
| if ! id -nG "$USER" | grep -qw libvirt; then | |
| die "You're not in the 'libvirt' group yet: | |
| sudo adduser \"$USER\" libvirt | |
| then log out and back in (or run 'newgrp libvirt') and re-run this script." | |
| fi | |
| # 2. Storage dir + traversal preflight -------------------------------------- | |
| mkdir -p "$IMG_DIR" | |
| p="$IMG_DIR" | |
| while :; do | |
| mode=$(stat -c '%a' "$p" 2>/dev/null || echo 000) | |
| if (( (10#${mode: -1} & 1) == 0 )); then | |
| msg "WARNING: $p (mode $mode) isn't traversable by the qemu user." | |
| msg " Fix: chmod o+x '$p' (no sudo if you own it)" | |
| fi | |
| [[ "$p" == "/" ]] && break | |
| p=$(dirname "$p") | |
| done | |
| # 3. SSH key ----------------------------------------------------------------- | |
| PUBKEY="" | |
| for k in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do | |
| [[ -f "$k" ]] && { PUBKEY="$(cat "$k")"; break; } | |
| done | |
| [[ -n "$PUBKEY" ]] || die "No SSH public key in ~/.ssh/. Create one: ssh-keygen -t ed25519" | |
| msg "Using SSH key ${PUBKEY%% *} …" | |
| # 4. Render cloud-init (literal string replace; no regex surprises) ---------- | |
| [[ -f "$CI_TEMPLATE" ]] || die "Missing $CI_TEMPLATE next to this script." | |
| msg "Rendering cloud-init" | |
| KEY="$PUBKEY" TPL="$CI_TEMPLATE" OUT="$CI_RENDERED" python3 - <<'PY' | |
| import os | |
| tpl = open(os.environ["TPL"]).read().replace("__SSH_PUBKEY__", os.environ["KEY"]) | |
| open(os.environ["OUT"], "w").write(tpl) | |
| PY | |
| # 5. Base image | |
| img_size() { stat -c '%s' "$1" 2>/dev/null || echo 0; } | |
| if [[ -f "${IMG_DIR}/${BASE}" ]] && (( $(img_size "${IMG_DIR}/${BASE}") >= MIN_SIZE )); then | |
| msg "Base image already present ($(( $(img_size "${IMG_DIR}/${BASE}") / 1024 / 1024 )) MB)" | |
| else | |
| [[ -f "${IMG_DIR}/${BASE}" ]] && { msg "Existing image is truncated; refetching"; rm -f "${IMG_DIR}/${BASE}"; } | |
| msg "Downloading Debian 13 genericcloud image (~300 MB)" | |
| tmp="${IMG_DIR}/.${BASE}.partial" | |
| rm -f "$tmp" | |
| if ! wget -T 10 --tries=20 -O "$tmp" "$BASE_URL"; then | |
| rm -f "$tmp" | |
| die "Download failed (network/timeout). Try again, or edit BASE_URL to another mirror." | |
| fi | |
| if (( $(img_size "$tmp") < MIN_SIZE )); then | |
| rm -f "$tmp" | |
| die "Downloaded file is too small ($(img_size "$tmp") bytes) — mirror served an error page? Retry or switch BASE_URL." | |
| fi | |
| mv "$tmp" "${IMG_DIR}/${BASE}" | |
| msg "Downloaded $(( $(img_size "${IMG_DIR}/${BASE}") / 1024 / 1024 )) MB" | |
| fi | |
| # 6. Per-VM disk (full copy: base stays pristine) ---------------------------- | |
| if [[ -f "$DISK" ]]; then | |
| # A disk with a live domain behind it is a real VM — never clobber it here. | |
| # A disk with NO domain is a leftover from a provision that died before | |
| # virt-install; offer to remove it (confirmed) so a retry isn't blocked. | |
| if virsh -c "$CONN" dominfo "$VM" >/dev/null 2>&1; then | |
| die "$DISK exists and domain $VM is still defined. Tear the VM down first: | |
| virsh -c $CONN undefine $VM --remove-all-storage" | |
| fi | |
| msg "WARNING: $DISK exists but no '$VM' domain does — a leftover from a failed provision." | |
| if [[ -t 0 ]]; then | |
| read -r -p "Delete this leftover disk and continue? [y/N]: " reply | |
| case "$reply" in | |
| y|Y|yes|YES) rm -f "$DISK" ;; | |
| *) die "Aborted. Remove it yourself when ready: rm -f '$DISK'" ;; | |
| esac | |
| else | |
| die "Refusing to delete without a TTY. Remove it yourself: rm -f '$DISK'" | |
| fi | |
| fi | |
| msg "Creating ${VM} disk (${DISK_GB} G, thin-provisioned)" | |
| cp --reflink=auto "${IMG_DIR}/${BASE}" "$DISK" | |
| qemu-img resize "$DISK" "${DISK_GB}G" | |
| # 7. Network ----------------------------------------------------------------- | |
| if ! virsh -c "$CONN" net-info "$NET" >/dev/null 2>&1; then | |
| msg "Defining NAT network $NET" | |
| virsh -c "$CONN" net-define "${HERE}/uat-net.xml" | |
| else | |
| msg "Network $NET already defined" | |
| fi | |
| # Start it (no-op if already running) and CONFIRM it's actually up before letting | |
| # virt-install attach to it. Don't probe state with 'net-info | grep -q': grep -q | |
| # exits on the first match and, under 'set -o pipefail', that can make the whole | |
| # pipeline report failure even on a match — misreading a running network as | |
| # inactive (the bug that left a transient net and broke reprovisioning). Parse the | |
| # field with awk in $(...), which reads to EOF and can't trip pipefail. | |
| virsh -c "$CONN" net-start "$NET" 2>/dev/null || true | |
| active=$(virsh -c "$CONN" net-info "$NET" 2>/dev/null | awk '$1=="Active:"{print $2}') | |
| [[ "$active" == yes ]] || die "Network $NET did not come up (net-start failed)." | |
| virsh -c "$CONN" net-autostart "$NET" 2>/dev/null || true | |
| # 8. os-variant (fall back if the host's osinfo-db predates trixie) ---------- | |
| if osinfo-query --fields=short-id os 2>/dev/null | grep -w debian13 >/dev/null; then | |
| OSV=debian13 | |
| else | |
| OSV=debian12 | |
| msg "osinfo-db has no 'debian13'; using $OSV (affects device defaults only)" | |
| fi | |
| # 9. Create the VM ----------------------------------------------------------- | |
| : "${MAXRAM:=$RAM}" # unset/blank MAXRAM -> flat, like before | |
| (( MAXRAM >= RAM )) || die "MAXRAM (${MAXRAM} MB) must be >= RAM (${RAM} MB)." | |
| # Video: QXL with bumped memory. QXL keeps 2D-over-SPICE snappy (it streams draw | |
| # commands to the SPICE server, with client-side caching) — virtio-gpu's 2D path | |
| # is a framebuffer scrape that feels noticeably laggier for typing. BUT QXL's video | |
| # memory is fixed at launch and the defaults (16M vgamem / 64M surfaces) are too | |
| # small: a long-lived LXQt session exhausts the surface pool, TTM can't evict, and | |
| # Xorg hard-hangs in qxl_fence_wait ([TTM] Buffer eviction failed) — desktop freezes | |
| # until a host power-cycle. The bumped sizes below (64M vgamem, 128M ram/vram) give | |
| # ample headroom. Keep gl=off above (no virgl 3D — its own eviction bugs); UAT is 2D. | |
| msg "Launching virt-install (boot ${RAM}M, balloon ceiling ${MAXRAM}M)" | |
| virt-install --connect "$CONN" \ | |
| --events on_reboot=restart \ | |
| --name "$VM" \ | |
| --memory "memory=${RAM},maxmemory=${MAXRAM}" \ | |
| --vcpus "$VCPUS" \ | |
| --cpu host-passthrough \ | |
| --disk "path=${DISK},bus=virtio,discard=unmap,driver.detect_zeroes=unmap,cache=none" \ | |
| --network "network=${NET},model=virtio" \ | |
| --memballoon model=virtio,autodeflate=on,freePageReporting=on \ | |
| --graphics spice,gl=off,listen=127.0.0.1 \ | |
| --video model.type=qxl,model.ram=131072,model.vram=131072,model.vgamem=65536,model.heads=1 \ | |
| --os-variant "$OSV" \ | |
| --cloud-init user-data="${CI_RENDERED}" \ | |
| --import \ | |
| --noautoconsole | |
| msg "Done — first boot installs the desktop (~a few minutes), then the VM POWERS OFF (expected)." | |
| cat <<EOF | |
| NOTE: first boot installs the desktop, then the VM powers itself off. Once it | |
| shows 'shut off' (virsh -c ${CONN} domstate ${VM}), start it — the one manual step: | |
| Start the VM: virsh -c ${CONN} start ${VM} | |
| View the desktop: virt-viewer -c ${CONN} ${VM} (SPICE) | |
| Watch first boot: virsh -c ${CONN} console ${VM} (serial; Ctrl-] to exit) | |
| Find its IP: virsh -c ${CONN} domifaddr ${VM} | |
| SSH in: ssh user@<ip> | |
| Grow/shrink RAM live: virsh -c ${CONN} setmem ${VM} 9G --live --config (${RAM}M..${MAXRAM}M) | |
| Check RAM (max/used): virsh -c ${CONN} dominfo ${VM} | |
| Snapshot clean state: virsh -c ${CONN} snapshot-create-as ${VM} clean "post-provision" | |
| Revert after bad run: virsh -c ${CONN} snapshot-revert ${VM} clean | |
| First boot installs LXQt + Chrome (a few minutes), then powers off. After you | |
| start it (above), it boots into the desktop autologged in as 'user'. Then, in | |
| the desktop's terminal: | |
| - install the "Claude in Chrome" extension from the Chrome Web Store | |
| - run 'claude' to authenticate, then 'claude --chrome' | |
| Greeter/console login is 'user' / 'changeme' — change it with: passwd | |
| EOF |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| hostname: uat | |
| fqdn: uat.local | |
| manage_etc_hosts: true | |
| timezone: America/Cancun | |
| users: | |
| - name: user | |
| shell: /bin/bash | |
| groups: [sudo] | |
| sudo: "ALL=(ALL) NOPASSWD:ALL" | |
| lock_passwd: false | |
| ssh_authorized_keys: | |
| - __SSH_PUBKEY__ | |
| ssh_pwauth: false | |
| chpasswd: | |
| expire: false | |
| users: | |
| - name: user | |
| password: changeme # desktop greeter / console login. CHANGE IT: passwd | |
| type: text | |
| apt: | |
| generate_mirrorlists: false | |
| package_update: true | |
| package_upgrade: false | |
| packages: | |
| - lxqt-core # the LXQt desktop (lean metapackage, incl. qterminal + pcmanfm-qt) | |
| - lightdm # display manager; autologin configured below | |
| - xserver-xorg | |
| - xserver-xorg-video-qxl # X driver for the QXL/SPICE video device | |
| - dbus-x11 | |
| - spice-vdagent # clipboard + auto-resize with the SPICE viewer | |
| - qemu-guest-agent # lets `virsh domifaddr` see the IP, clean shutdown | |
| - curl | |
| - ca-certificates | |
| # Terminal/console environment + GitHub tooling | |
| - git | |
| - gh # GitHub CLI | |
| - jq # JSON parsing for gh output / scripts | |
| - python-is-python3 # `python` -> python3 (many tools/scripts assume plain `python`) | |
| - vim # $EDITOR/$VISUAL; uses the .vimrc fetched below | |
| - screen | |
| - bash-completion | |
| - fonts-powerline | |
| - less | |
| - fzf | |
| - earlyoom # userspace OOM killer — safety net beneath the balloon ceiling | |
| - zram-tools # compressed in-RAM swap (zram) — first pressure-relief valve | |
| write_files: | |
| # Force the apt mirrorlist to plain http so the LAN Squid can cache .debs. The | |
| # genericcloud image ships these with https URLs; write-files runs before the | |
| # package phase, so the first update+install already use http. | |
| - path: /etc/apt/mirrors/debian.list | |
| content: | | |
| http://deb.debian.org/debian | |
| - path: /etc/apt/mirrors/debian-security.list | |
| content: | | |
| http://deb.debian.org/debian-security | |
| - path: /etc/lightdm/lightdm.conf.d/50-autologin.conf | |
| content: | | |
| [Seat:*] | |
| autologin-user=user | |
| autologin-session=lxqt | |
| autologin-user-timeout=0 | |
| - path: /etc/motd | |
| content: | | |
| uat — browser acceptance-testing desktop (LXQt + Chrome) | |
| Connect from the host: virt-viewer -c qemu:///system uat | |
| A 'clean' snapshot exists on the host; revert freely. | |
| - path: /etc/profile.d/uat.sh | |
| content: | | |
| export EDITOR=vim | |
| export VISUAL=vim | |
| - path: /home/user/.gitconfig | |
| owner: user:user | |
| defer: true | |
| content: | | |
| [user] | |
| name = Me | |
| email = Me@gmail.com | |
| [core] | |
| editor = vim -c'set spell' | |
| - path: /home/user/.bashrc | |
| owner: user:user | |
| append: true | |
| defer: true | |
| content: | | |
| alias screen='screen -U' | |
| HISTSIZE=10000 | |
| HISTFILESIZE=10000 | |
| # Short bootloader menu delay. A grub.d drop-in overrides whatever timeout the | |
| # base image sets (grub-mkconfig sources /etc/default/grub.d/*.cfg last). | |
| - path: /etc/default/grub.d/99-timeout.cfg | |
| content: | | |
| GRUB_TIMEOUT=1 | |
| - path: /etc/default/earlyoom | |
| content: | | |
| EARLYOOM_ARGS="-r 0 -m 2" | |
| # zram: compressed swap in RAM (no disk). PERCENT = % of RAM. | |
| - path: /etc/default/zramswap | |
| content: | | |
| ALGO=zstd | |
| PERCENT=50 | |
| PRIORITY=100 | |
| # swappiness=100: favour fast zram swap. page-cluster=0: no swap readahead. | |
| - path: /etc/sysctl.d/99-zram.conf | |
| content: | | |
| vm.swappiness = 100 | |
| vm.page-cluster = 0 | |
| runcmd: | |
| - [ systemctl, mask, tmp.mount ] | |
| # Autologin prerequisites: Debian's lightdm wants these groups to exist and the | |
| # user to be a member. Harmless if autologin still falls back to a manual login. | |
| - [ groupadd, -f, autologin ] | |
| - [ gpasswd, -a, user, autologin ] | |
| - [ groupadd, -f, nopasswdlogin ] | |
| - [ gpasswd, -a, user, nopasswdlogin ] | |
| - [ systemctl, set-default, graphical.target ] | |
| - [ systemctl, enable, lightdm ] | |
| - [ systemctl, enable, --now, qemu-guest-agent ] | |
| - [ systemctl, enable, --now, earlyoom ] # OOM safety net (args in /etc/default/earlyoom) | |
| - [ systemctl, restart, earlyoom ] # ensure it picked up our EARLYOOM_ARGS | |
| - [ systemctl, enable, --now, zramswap ] # compressed RAM swap (config in /etc/default/zramswap) | |
| - [ systemctl, restart, zramswap ] # ensure it picked up our PERCENT/ALGO | |
| - [ sysctl, --system ] # apply /etc/sysctl.d/99-zram.conf now | |
| # Root fs tuning. Enable weekly batch trim (Debian doesn't by default), then | |
| # rewrite the root fstab line to drop the base image's continuous 'discard' and | |
| # add noatime — the timer now reclaims host space instead. Enable ext4 | |
| # fast_commit; regenerate grub.cfg for the new timeout. All active on the next | |
| # boot after the first-boot poweroff below (noatime is also remounted live). | |
| - [ systemctl, enable, --now, fstrim.timer ] | |
| - sed -i -E 's#^(\S+\s+/\s+ext4\s+)\S+#\1rw,noatime,errors=remount-ro,x-systemd.growfs#' /etc/fstab | |
| - mount -o remount,noatime / | |
| - tune2fs -O fast_commit "$(findmnt -no SOURCE /)" || true | |
| - update-grub | |
| # Google Chrome — needed for the "Claude in Chrome" extension used in UAT. | |
| - curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o /tmp/chrome.deb | |
| - apt-get install -y /tmp/chrome.deb | |
| - rm -f /tmp/chrome.deb | |
| # Claude Code + personal shell config, for the 'user' account. | |
| - [ install, -d, -o, user, -g, user, /home/user/projects ] | |
| - runuser -l user -c 'curl -fsSL https://claude.ai/install.sh | bash' || true | |
| # Power the VM off once first-boot provisioning finishes. virt-install's | |
| # --cloud-init forces a full shutdown on the first guest reboot anyway (to drop | |
| # the one-time cloud-init media), so we poweroff outright. Bring it up with | |
| # `virsh start uat`: it boots into the graphical target (lightdm -> autologin -> | |
| # LXQt) on its persistent config, with reboots working normally. | |
| power_state: | |
| mode: poweroff | |
| message: "First-boot cloud-init done — powering off; run 'virsh start uat'" | |
| timeout: 30 | |
| condition: true | |
| final_message: "uat provisioned in $UPTIME s — now powering off. Start it with 'virsh start uat' to boot into the LXQt desktop." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <network> | |
| <name>uat-net</name> | |
| <forward mode='nat'/> | |
| <bridge name='virbr-uat' stp='on' delay='0'/> | |
| <ip address='192.168.151.1' netmask='255.255.255.0'> | |
| <dhcp> | |
| <range start='192.168.151.10' end='192.168.151.100'/> | |
| </dhcp> | |
| </ip> | |
| </network> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment