From 70e6d80c2d367480478eacf46363ee88a0e994ff Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 29 Jan 2026 01:27:17 +0100 Subject: added lots of new dmenu scripts --- borg-internet.sh | 22 +++++ capture-clip | 49 +++++++++++ citekey-menu | 28 ++++++ dmenu-edit-config | 52 +++++++++++ dmenu-memo | 46 ++++++++++ dmenu-scripts | 38 ++++++++ dmenu-vimwiki-open | 75 ++++++++++++++++ fakeid | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ faker | 1 + kvm-lab-create | 148 +++++++++++++++---------------- kvm-lab-create.bak | 160 ++++++++++++++++++++++++++++++++++ kvm-lab-destroy | 71 +++++++-------- kvm-lab-destroy.bak | 52 +++++++++++ officium | 4 +- 14 files changed, 871 insertions(+), 119 deletions(-) create mode 100755 borg-internet.sh create mode 100755 capture-clip create mode 100755 citekey-menu create mode 100755 dmenu-edit-config create mode 100755 dmenu-memo create mode 100755 dmenu-scripts create mode 100755 dmenu-vimwiki-open create mode 100755 fakeid create mode 120000 faker create mode 100755 kvm-lab-create.bak create mode 100755 kvm-lab-destroy.bak diff --git a/borg-internet.sh b/borg-internet.sh new file mode 100755 index 0000000..84247d1 --- /dev/null +++ b/borg-internet.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +REPO="borg@sanctum-wg:/tank/backups/borg" +NAME="home-$(date +%F_%H-%M)" +SRC="/home/lukasz" + +export BORG_RSH="ssh -p 57385 -i $HOME/.ssh/sacrum.key -o IdentitiesOnly=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3" + +LOGDIR="$HOME/.local/state/borg" +mkdir -p "$LOGDIR" +LOGFILE="$LOGDIR/backup-$(date +%F).log" + +ARGS=(--compression lz4 --exclude-from "$HOME/.config/borg/excludes") +if [ -t 1 ]; then + ARGS+=(--progress --list --stats) +else + ARGS+=(--stats) +fi + +borg create "${ARGS[@]}" "$REPO"::"$NAME" "$SRC" 2>&1 | tee -a "$LOGFILE" + diff --git a/capture-clip b/capture-clip new file mode 100755 index 0000000..3bf17f1 --- /dev/null +++ b/capture-clip @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTFILE="${OUTFILE:-$HOME/.clips}" + +notify() { + # $1 = urgency (low|normal|critical) + # $2 = title + # $3 = body + if command -v notify-send >/dev/null 2>&1; then + notify-send -u "$1" "$2" "$3" + fi +} + +die() { + notify critical "capture-clip" "$1" + printf -- "ERROR: %s\n" "$1" >&2 + exit 1 +} + +get_clipboard() { + if command -v xclip >/dev/null 2>&1; then + xclip -o -selection clipboard 2>/dev/null || xclip -o -selection primary + return 0 + fi + if command -v xsel >/dev/null 2>&1; then + xsel --clipboard --output 2>/dev/null || xsel --primary --output + return 0 + fi + die "need xclip or xsel" +} + +clip="$(get_clipboard)" + +[[ -n "$clip" ]] || die "clipboard is empty" + +ts="$(date '+%y:%m:%d %H:%M:%S')" + +{ + printf -- "# caputred %s\n" "$ts" + printf -- "%s\n" "$clip" + printf -- "---\n" +} >> "$OUTFILE" + +lines="$(printf "%s" "$clip" | wc -l | tr -d ' ')" +chars="$(printf "%s" "$clip" | wc -c | tr -d ' ')" + +notify normal "capture-clip" "Saved to ~/.clips ($lines lines, $chars chars)" + diff --git a/citekey-menu b/citekey-menu new file mode 100755 index 0000000..3b94bbe --- /dev/null +++ b/citekey-menu @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +BIB="${BIB:-/home/lukasz/thesis_main/05_thesis/bibliography.bib}" +DMENU="${DMENU:-dmenu -i -l 20 -p Citekey}" +CLIP="${CLIP:-xclip -selection clipboard}" + +if [[ ! -r "$BIB" ]]; then + printf "ERROR: cannot read %s\n" "$BIB" >&2 + exit 1 +fi + +# Build menu lines: "typecitekey" +choices="$( + rg -n '^\@(\w+)\{([^,]+),' "$BIB" \ + | sed -E 's/^[0-9]+:\@(\w+)\{([^,]+),/\1\t\2/' \ + | sort -u +)" + +picked="$(printf "%s\n" "$choices" | eval "$DMENU" || true)" +[[ -z "$picked" ]] && exit 0 + +key="$(printf "%s" "$picked" | awk -F'\t' '{print $2}')" +[[ -z "$key" ]] && exit 0 + +printf "[@%s]" "$key" | eval "$CLIP" +printf "Copied: [@%s]\n" "$key" >&2 + diff --git a/dmenu-edit-config b/dmenu-edit-config new file mode 100755 index 0000000..47705e1 --- /dev/null +++ b/dmenu-edit-config @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional dmenu theming +# shellcheck disable=SC1090 +[[ -r "${HOME}/.local/bin/dmenu-theming" ]] && . "${HOME}/.local/bin/dmenu-theming" + +DMENU="${DMENU:-dmenu}" +PROMPT="${PROMPT:-Config: }" +EDITOR_CMD="${EDITOR_CMD:-vim}" +TERM_CMD="${TERM_CMD:-st -e}" # change to "alacritty -e" / "kitty -e" / "wezterm start --" if needed +INDEX="${INDEX:-${HOME}/.config/dmenu/configs.tsv}" + +[[ -r "$INDEX" ]] || { printf "ERROR: cannot read %s\n" "$INDEX" >&2; exit 1; } + +# Build menu: show label, keep path as hidden second field +menu="$( + awk -F'\t' 'NF>=2 {print $1 "\t" $2}' "$INDEX" +)" + +picked="$( + printf "%s\n" "$menu" \ + | ${DMENU} -i -l 20 -p "$PROMPT" ${lines:-} ${colors:-} ${font:-} \ + || true +)" +[[ -n "$picked" ]] || exit 0 + +path="$(printf "%s" "$picked" | awk -F'\t' '{print $2}')" +[[ -n "$path" ]] || exit 0 + +# Expand ~ manually (awk won’t expand it) +path="${path/#\~/$HOME}" + +# If path is relative, treat it as relative to $HOME +case "$path" in + /*) : ;; + *) path="${HOME}/${path}" ;; +esac + +# Decide whether we need sudo (simple heuristic: /etc, /usr, /var, /run) +needs_sudo=0 +case "$path" in + /etc/*|/usr/*|/var/*|/run/*) needs_sudo=1 ;; +esac + +# Open in terminal editor (no eval) +if [[ $needs_sudo -eq 1 ]]; then + exec ${TERM_CMD} sudo -E "$EDITOR_CMD" "$path" +else + exec ${TERM_CMD} "$EDITOR_CMD" "$path" +fi + diff --git a/dmenu-memo b/dmenu-memo new file mode 100755 index 0000000..597f191 --- /dev/null +++ b/dmenu-memo @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTFILE="${OUTFILE:-$HOME/.notes}" +DMENU="${DMENU:-dmenu}" +PROMPT="${PROMPT:-Memo: }" + +# Optional dmenu theming +# shellcheck disable=SC1090 +[[ -r "${HOME}/.local/bin/dmenu-theming" ]] && . "${HOME}/.local/bin/dmenu-theming" + +notify() { + # $1: urgency (low|normal|critical), $2: message + if command -v notify-send >/dev/null 2>&1; then + notify-send -u "$1" "dmenu-memo" "$2" + fi +} + +die() { + notify critical "$1" + exit 1 +} + +# dmenu returns non-zero on ESC; treat as cancel +note="$( + printf "" \ + | ${DMENU} -p "$PROMPT" ${lines:-} ${colors:-} ${font:-} \ + || true +)" + +# Cancel / empty input: notify low, then exit quietly +if [[ -z "${note}" ]]; then + notify low "Cancelled (empty memo)" + exit 0 +fi + +ts="$(date '+%y:%m:%d %H:%M:%S')" + +{ + printf -- "# %s\n" "$ts" + printf -- "%s\n" "$note" + printf -- "---\n" +} >> "$OUTFILE" || die "Failed to write to $OUTFILE" + +notify normal "Saved to ~/.notes" + diff --git a/dmenu-scripts b/dmenu-scripts new file mode 100755 index 0000000..6d9c9db --- /dev/null +++ b/dmenu-scripts @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_path="${HOME}/.local/bin" + +# Optional dmenu theming +# shellcheck disable=SC1090 +[[ -r "${HOME}/.local/bin/dmenu-theming" ]] && . "${HOME}/.local/bin/dmenu-theming" + +DMENU_CMD="${DMENU_CMD:-dmenu}" +PROMPT="${PROMPT:-Run: }" + +# List only executable non-binary files in ~/.local/bin +choices="$( + find "${script_path}" -maxdepth 1 -type f -executable -print0 \ + | xargs -0 -I{} sh -c 'grep -Iq . "$1" && printf "%s\n" "$(basename "$1")"' _ {} \ + | sort -u +)" + +picked="$( + printf "%s\n" "$choices" \ + | ${DMENU_CMD} -i -l 20 -p "${PROMPT}" ${lines:-} ${colors:-} ${font:-} \ + || true +)" + +[[ -n "${picked}" ]] || exit 0 + +# Safety: only allow basenames (no slashes) +case "$picked" in + */*|"") exit 0 ;; +esac + +target="${script_path}/${picked}" + +[[ -x "$target" ]] || { printf "ERROR: not executable: %s\n" "$target" >&2; exit 1; } + +exec "$target" + diff --git a/dmenu-vimwiki-open b/dmenu-vimwiki-open new file mode 100755 index 0000000..e8c4891 --- /dev/null +++ b/dmenu-vimwiki-open @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional dmenu theming +# shellcheck disable=SC1090 +[[ -r "${HOME}/.local/bin/dmenu-theming" ]] && . "${HOME}/.local/bin/dmenu-theming" + +DMENU="${DMENU:-dmenu}" +PROMPT="${PROMPT:-VimWiki: }" +EDITOR_CMD="${EDITOR_CMD:-vim}" +TERM_CMD="${TERM_CMD:-st -e}" + +# Set this to your VimWiki root directory +VIMWIKI_ROOT="${VIMWIKI_ROOT:-${HOME}/vimwiki}" # <-- CHANGE if needed + +CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/dmenu" +CACHE_FILE="${CACHE_DIR}/vimwiki_files.txt" +CACHE_TTL_SECONDS="${CACHE_TTL_SECONDS:-30}" # how long to trust the cache + +mkdir -p "$CACHE_DIR" + +now_epoch() { date +%s; } + +cache_is_fresh() { + [[ -r "$CACHE_FILE" ]] || return 1 + local mtime + mtime="$(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)" + local age=$(( $(now_epoch) - mtime )) + [[ "$age" -le "$CACHE_TTL_SECONDS" ]] +} + +build_cache() { + if command -v fd >/dev/null 2>&1; then + # Relative paths, ignore common junk + (cd "$VIMWIKI_ROOT" && fd -t f -H \ + -E .git -E .cache -E node_modules -E .venv -E __pycache__ \ + -E attachments -E media \ + .) > "$CACHE_FILE" + else + (cd "$VIMWIKI_ROOT" && find . -type f \ + ! -path './.git/*' \ + ! -path './.cache/*' \ + ! -path './node_modules/*' \ + ! -path './.venv/*' \ + ! -path './__pycache__/*' \ + ! -path './attachments/*' \ + ! -path './media/*' \ + -print | sed 's|^\./||') > "$CACHE_FILE" + fi + + # Sort for stable menu + sort -u -o "$CACHE_FILE" "$CACHE_FILE" +} + +[[ -d "$VIMWIKI_ROOT" ]] || { printf "ERROR: VimWiki root not found: %s\n" "$VIMWIKI_ROOT" >&2; exit 1; } + +cache_is_fresh || build_cache + +picked="$( + cat "$CACHE_FILE" \ + | ${DMENU} -i -l 20 -p "$PROMPT" ${lines:-} ${colors:-} ${font:-} \ + || true +)" +[[ -n "$picked" ]] || exit 0 + +# Safety: no parent traversal +case "$picked" in + *'..'* ) exit 0 ;; +esac + +target="${VIMWIKI_ROOT}/${picked}" +[[ -e "$target" ]] || { printf "ERROR: missing: %s\n" "$target" >&2; exit 1; } + +exec ${TERM_CMD} "$EDITOR_CMD" "$target" + diff --git a/fakeid b/fakeid new file mode 100755 index 0000000..01256bb --- /dev/null +++ b/fakeid @@ -0,0 +1,244 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <<'EOF' +Usage: + fakeid [count] + +Examples: + fakeid pl + fakeid de 5 + fakeid fr 2 + +Supported cc -> Faker locale mapping: + pl -> pl_PL + de -> de_DE + fr -> fr_FR + en -> en_US + it -> it_IT + es -> es_ES + nl -> nl_NL + pt -> pt_PT + cz -> cs_CZ + sk -> sk_SK + se -> sv_SE + no -> nb_NO + dk -> da_DK +EOF +} + +cc="${1:-}" +count="${2:-1}" + +[ -n "$cc" ] || { usage; exit 2; } + +case "$cc" in + pl) locale="pl_PL" ;; + de) locale="de_DE" ;; + fr) locale="fr_FR" ;; + en) locale="en_US" ;; + it) locale="it_IT" ;; + es) locale="es_ES" ;; + nl) locale="nl_NL" ;; + pt) locale="pt_PT" ;; + cz) locale="cs_CZ" ;; + sk) locale="sk_SK" ;; + se) locale="sv_SE" ;; + no) locale="nb_NO" ;; + dk) locale="da_DK" ;; + *) + echo "Unsupported country code: $cc" >&2 + usage >&2 + exit 2 + ;; +esac + +case "$count" in + ''|*[!0-9]*) + echo "Count must be a positive integer." >&2 + exit 2 + ;; +esac +[ "$count" -ge 1 ] || { echo "Count must be >= 1." >&2; exit 2; } + +have_iconv=0 +command -v iconv >/dev/null 2>&1 && have_iconv=1 + +# Convert to an ASCII-ish gmail-safe slug. +# - transliterate diacritics where possible +# - lowercase +# - allow only [a-z0-9.] (dots used as separators) +# - collapse multiple dots +# - trim leading/trailing dots +slugify() { + s="$1" + if [ "$have_iconv" -eq 1 ]; then + s="$(printf "%s" "$s" | iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null || printf "%s" "$s")" + fi + + # lowercase + s="$(printf "%s" "$s" | tr '[:upper:]' '[:lower:]')" + + # normalize common separators to dot (no tr ranges involved) + s="$(printf "%s" "$s" | sed "s/[[:space:]_'’\"-]\+/\./g")" + + # keep only a-z0-9 and dots + s="$(printf "%s" "$s" | tr -cd 'a-z0-9.')" + + # collapse repeated dots + trim leading/trailing dots + s="$(printf "%s" "$s" | sed 's/\.\{2,\}/./g; s/^\.*//; s/\.*$//')" + + printf "%s" "$s" +} + +# Reasonable gmail local-part length (keep it human-like). +# Gmail allows longer, but typical addresses are short. +clip_len() { + s="$1" + max="${2:-24}" + # POSIX-ish: use cut on bytes (fine for ASCII output) + printf "%s" "$s" | cut -c1-"$max" +} + +rand_int() { + # faker random_int sometimes expects args; simplest: + faker -l "$locale" random_int +} + +# Weighted pattern selection per country. +# Return an integer "pattern id" used by gmail_localpart(). +pick_pattern() { + r="$(rand_int)" + # reduce r to 0..99 + r=$((r % 100)) + + case "$cc" in + # Poland: firstname.lastname and firstlast are common; some initials. + pl) + if [ "$r" -lt 55 ]; then echo 1 + elif [ "$r" -lt 80 ]; then echo 2 + elif [ "$r" -lt 92 ]; then echo 3 + else echo 4 + fi + ;; + # Germany: firstname.lastname strong; initials less frequent. + de) + if [ "$r" -lt 65 ]; then echo 1 + elif [ "$r" -lt 85 ]; then echo 2 + elif [ "$r" -lt 95 ]; then echo 3 + else echo 4 + fi + ;; + # France: firstname.lastname and first.last; sometimes first initial + last. + fr) + if [ "$r" -lt 60 ]; then echo 1 + elif [ "$r" -lt 82 ]; then echo 2 + elif [ "$r" -lt 94 ]; then echo 3 + else echo 4 + fi + ;; + # US: firstlast + numbers more common. + en) + if [ "$r" -lt 35 ]; then echo 2 + elif [ "$r" -lt 70 ]; then echo 1 + elif [ "$r" -lt 90 ]; then echo 3 + else echo 4 + fi + ;; + # Default: balanced + *) + if [ "$r" -lt 55 ]; then echo 1 + elif [ "$r" -lt 80 ]; then echo 2 + elif [ "$r" -lt 92 ]; then echo 3 + else echo 4 + fi + ;; + esac +} + +# Build localpart from first/last with realistic patterns: +# 1: firstname.lastname +# 2: firstlast +# 3: f.lastname +# 4: firstname.l +gmail_localpart() { + first="$1" + last="$2" + + f="$(slugify "$first")" + l="$(slugify "$last")" + + # Fallback if Faker returns odd strings + [ -n "$f" ] || f="user" + [ -n "$l" ] || l="name" + + pat="$(pick_pattern)" + + case "$pat" in + 1) base="${f}.${l}" ;; + 2) base="${f}${l}" ;; + 3) base="$(printf "%s" "$f" | cut -c1).${l}" ;; + 4) base="${f}.$(printf "%s" "$l" | cut -c1)" ;; + *) base="${f}.${l}" ;; + esac + + base="$(slugify "$base")" + base="$(clip_len "$base" 24)" + + # Add numeric suffix sometimes (more common in en, but possible everywhere). + rr="$(rand_int)"; rr=$((rr % 100)) + add_num=0 + case "$cc" in + en) [ "$rr" -lt 45 ] && add_num=1 ;; + *) [ "$rr" -lt 25 ] && add_num=1 ;; + esac + + if [ "$add_num" -eq 1 ]; then + # Use a 2–3 digit suffix; avoid looking too synthetic. + n="$(rand_int)" + n=$(( (n % 900) + 10 )) # 10..909 + base="$(clip_len "$base" 21)${n}" + base="$(slugify "$base")" + fi + + # Final safety: ensure non-empty + [ -n "$base" ] || base="user$(( ( $(rand_int) % 9000 ) + 1000 ))" + + printf "%s" "$base" +} + +gmail_email() { + first="$(faker -l "$locale" first_name)" + last="$(faker -l "$locale" last_name)" + + lp="$(gmail_localpart "$first" "$last")" + + # De-dup within this run: if collision, append digits. + # (We store as newline-delimited list in $seen) + if printf "%s\n" "${seen:-}" | grep -qx "$lp" 2>/dev/null; then + k="$(rand_int)"; k=$(( (k % 900) + 10 )) + lp="$(clip_len "$lp" 21)${k}" + lp="$(slugify "$lp")" + fi + seen="${seen:-} +$lp" + + printf "%s@gmail.com\n" "$lp" +} + +i=1 +seen="" +while [ "$i" -le "$count" ]; do + faker -l "$locale" name + faker -l "$locale" address + faker -l "$locale" phone_number + gmail_email + + if [ "$i" -lt "$count" ]; then + printf "\n" + fi + + i=$((i + 1)) +done + diff --git a/faker b/faker new file mode 120000 index 0000000..2949d5d --- /dev/null +++ b/faker @@ -0,0 +1 @@ +/home/lukasz/.local/share/pipx/venvs/faker/bin/faker \ No newline at end of file diff --git a/kvm-lab-create b/kvm-lab-create index 43d2926..e574a4c 100755 --- a/kvm-lab-create +++ b/kvm-lab-create @@ -11,14 +11,20 @@ TEMPLATE_DOMAIN="debian13-template" TEMPLATE_BASE="$BASEDIR/debian13-template.qcow2" usage() { - echo "Usage: kvm-lab-create [--base template|work01|/abs/path.qcow2] [--start]" - echo "Examples:" - echo " kvm-lab-create lab10 --base template --start" - echo " kvm-lab-create lab11 --base work01 --start" - echo " kvm-lab-create lab12 --base /var/lib/libvirt/images/base/custom.qcow2 --start" + cat <<'EOF' +Usage: kvm-lab-create [--base template||||/abs/path.qcow2] [--start] + +Base resolution order: + 1) template -> /var/lib/libvirt/images/base/debian13-template.qcow2 + 2) /abs/path.qcow2 -> that file + 3) -> /var/lib/libvirt/images/base/ + 4) -> vda disk path of that domain (virsh domblklist) + 5) -> newest /var/lib/libvirt/images/base/-base-*.qcow2 +EOF } -# --- Parse args --- +die() { echo "ERROR: $*" >&2; exit 1; } + NAME="${1:-}" shift || true @@ -27,97 +33,76 @@ START="no" while [[ $# -gt 0 ]]; do case "$1" in - --base) - BASESEL="${2:-}" - shift 2 - ;; - --start) - START="yes" - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "ERROR: Unknown argument: $1" - usage - exit 1 - ;; + --base) BASESEL="${2:-}"; shift 2 ;; + --start) START="yes"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "Unknown argument: $1" ;; esac done -if [[ -z "$NAME" ]]; then - usage - exit 1 -fi +[[ -n "$NAME" ]] || { usage; exit 1; } DISK="${LABDIR}/${NAME}.qcow2" XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")" -cleanup() { rm -f "$XML_TMP"; } + +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 selection --- resolve_base() { local sel="$1" + if [[ "$sel" == "template" ]]; then - echo "$TEMPLATE_BASE" - return 0 + echo "$TEMPLATE_BASE"; return 0 fi - if [[ "$sel" == "work01" ]]; then - # Pick newest promoted work01 base (must exist first) - local latest - latest="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)" - if [[ -z "$latest" ]]; then - echo "ERROR: No work01 base found in $BASEDIR (expected work01-base-*.qcow2)." - echo "Action: run your promote script to create one." - return 1 - fi - echo "$latest" - return 0 + if [[ "$sel" == /* ]]; then + echo "$sel"; return 0 fi - # Absolute path base - if [[ "$sel" == /* ]]; then - echo "$sel" - return 0 + if [[ -f "$BASEDIR/$sel" ]]; then + echo "$BASEDIR/$sel"; return 0 fi - echo "ERROR: Invalid --base '$sel' (use template|work01|/abs/path.qcow2)" - return 1 -} + 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 -BASE="$(resolve_base "$BASESEL")" + 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 (read-only checks) --- -if [[ ! -d "$LABDIR" ]]; then - echo "ERROR: Lab directory missing: $LABDIR" - exit 1 +# 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 -if ! sudo test -r "$BASE"; then - echo "ERROR: Base image not readable (via sudo): $BASE" - exit 1 -fi +BASE="$(resolve_base "$BASESEL")" +echo "Base selection: $BASESEL -> $BASE" -if [[ -e "$DISK" ]]; then - echo "ERROR: Disk already exists: $DISK" - exit 1 -fi +[[ -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 - echo "ERROR: Domain already exists: $NAME" - exit 1 + die "Domain already exists: $NAME" fi -if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then - echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN" - exit 1 -fi - -# --- Create overlay disk --- -echo "Using base: $BASE" +# 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" @@ -126,28 +111,37 @@ sudo chmod 0660 "$DISK" echo "Validating backing file..." sudo qemu-img info "$DISK" | grep -E "file format:|virtual size:|backing file:" || true -# --- Clone domain XML --- +# Clone template XML and adjust echo "Cloning domain XML from: $TEMPLATE_DOMAIN" virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP" -# Set name, disk path, and force NAT-only network "default" +# Update: name, disk source, network default +# - replace ... once +# - replace template disk path with overlay path (global) +# - force network='default' sed -i \ -e "s|${TEMPLATE_DOMAIN}|${NAME}|" \ -e "s|${TEMPLATE_BASE}|${DISK}|g" \ -e "s|[^<]*<\/uuid>/d' "$XML_TMP" +sed -i -E "s///g" "$XML_TMP" -# Remove any host filesystem passthrough (if present) +# Remove filesystem passthrough (if any) perl -0777 -i -pe 's/\n?//g' "$XML_TMP" -# --- Define domain --- +# Validate substitutions happened +grep -q "${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 --- +# Optional start if [[ "$START" == "yes" ]]; then echo "Starting domain: $NAME" virsh -c "$URI" start "$NAME" >/dev/null diff --git a/kvm-lab-create.bak b/kvm-lab-create.bak new file mode 100755 index 0000000..43d2926 --- /dev/null +++ b/kvm-lab-create.bak @@ -0,0 +1,160 @@ +#!/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() { + echo "Usage: kvm-lab-create [--base template|work01|/abs/path.qcow2] [--start]" + echo "Examples:" + echo " kvm-lab-create lab10 --base template --start" + echo " kvm-lab-create lab11 --base work01 --start" + echo " kvm-lab-create lab12 --base /var/lib/libvirt/images/base/custom.qcow2 --start" +} + +# --- Parse args --- +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 + ;; + *) + echo "ERROR: Unknown argument: $1" + usage + exit 1 + ;; + esac +done + +if [[ -z "$NAME" ]]; then + usage + exit 1 +fi + +DISK="${LABDIR}/${NAME}.qcow2" +XML_TMP="$(mktemp "/tmp/${NAME}.xml.XXXXXX")" +cleanup() { rm -f "$XML_TMP"; } +trap cleanup EXIT + +# --- Resolve base selection --- +resolve_base() { + local sel="$1" + if [[ "$sel" == "template" ]]; then + echo "$TEMPLATE_BASE" + return 0 + fi + + if [[ "$sel" == "work01" ]]; then + # Pick newest promoted work01 base (must exist first) + local latest + latest="$(ls -1t "$BASEDIR"/work01-base-*.qcow2 2>/dev/null | head -n 1 || true)" + if [[ -z "$latest" ]]; then + echo "ERROR: No work01 base found in $BASEDIR (expected work01-base-*.qcow2)." + echo "Action: run your promote script to create one." + return 1 + fi + echo "$latest" + return 0 + fi + + # Absolute path base + if [[ "$sel" == /* ]]; then + echo "$sel" + return 0 + fi + + echo "ERROR: Invalid --base '$sel' (use template|work01|/abs/path.qcow2)" + return 1 +} + +BASE="$(resolve_base "$BASESEL")" + +# --- Preconditions (read-only checks) --- +if [[ ! -d "$LABDIR" ]]; then + echo "ERROR: Lab directory missing: $LABDIR" + exit 1 +fi + +if ! sudo test -r "$BASE"; then + echo "ERROR: Base image not readable (via sudo): $BASE" + exit 1 +fi + +if [[ -e "$DISK" ]]; then + echo "ERROR: Disk already exists: $DISK" + exit 1 +fi + +if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then + echo "ERROR: Domain already exists: $NAME" + exit 1 +fi + +if ! virsh -c "$URI" dominfo "$TEMPLATE_DOMAIN" &>/dev/null; then + echo "ERROR: Template domain not found in $URI: $TEMPLATE_DOMAIN" + exit 1 +fi + +# --- Create overlay disk --- +echo "Using base: $BASE" +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 domain XML --- +echo "Cloning domain XML from: $TEMPLATE_DOMAIN" +virsh -c "$URI" dumpxml "$TEMPLATE_DOMAIN" > "$XML_TMP" + +# Set name, disk path, and force NAT-only network "default" +sed -i \ + -e "s|${TEMPLATE_DOMAIN}|${NAME}|" \ + -e "s|${TEMPLATE_BASE}|${DISK}|g" \ + -e "s|[^<]*<\/uuid>/d' "$XML_TMP" + +# Remove any host filesystem passthrough (if present) +perl -0777 -i -pe 's/\n?//g' "$XML_TMP" + +# --- Define domain --- +echo "Defining domain: $NAME" +virsh -c "$URI" define "$XML_TMP" >/dev/null + +# --- 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 + diff --git a/kvm-lab-destroy b/kvm-lab-destroy index 3548ef9..c608831 100755 --- a/kvm-lab-destroy +++ b/kvm-lab-destroy @@ -2,51 +2,42 @@ set -euo pipefail URI="qemu:///system" -LABDIR="/var/lib/libvirt/images/lab" +IMGROOT="/var/lib/libvirt/images" +LABDIR="$IMGROOT/lab" -NAME="${1:-}" - -if [[ -z "$NAME" ]]; then +usage() { echo "Usage: kvm-lab-destroy " - exit 1 -fi - -if [[ "$NAME" == "work01" || "$NAME" == "debian13-template" ]]; then - echo "Refusing to destroy protected VM: $NAME" - exit 1 -fi - -if ! virsh -c "$URI" dominfo "$NAME" &>/dev/null; then - echo "Domain not found: $NAME" - exit 1 -fi +} -DISK="$(virsh -c "$URI" domblklist "$NAME" --details | awk '$3=="vda"{print $4}')" - -if [[ -z "$DISK" || "$DISK" != "$LABDIR/"* ]]; then - echo "Refusing to destroy: disk not under $LABDIR" - echo "Detected disk: $DISK" - exit 1 -fi - -STATE="$(virsh -c "$URI" domstate "$NAME")" - -if [[ "$STATE" == "running" ]]; then - echo "Shutting down $NAME..." - virsh -c "$URI" shutdown "$NAME" - sleep 3 +NAME="${1:-}" +[[ -n "$NAME" ]] || { usage; exit 1; } + +DISK="${LABDIR}/${NAME}.qcow2" + +# Domain cleanup (best-effort) +if virsh -c "$URI" dominfo "$NAME" &>/dev/null; then + state="$(virsh -c "$URI" domstate "$NAME" 2>/dev/null || true)" + if [[ "$state" == *running* ]]; then + virsh -c "$URI" shutdown "$NAME" >/dev/null 2>&1 || true + sleep 1 + virsh -c "$URI" destroy "$NAME" >/dev/null 2>&1 || true + else + virsh -c "$URI" destroy "$NAME" >/dev/null 2>&1 || true + fi + + virsh -c "$URI" undefine "$NAME" --nvram >/dev/null 2>&1 || \ + virsh -c "$URI" undefine "$NAME" >/dev/null 2>&1 || true +else + echo "WARN: Domain not found: $NAME" fi -if [[ "$(virsh -c "$URI" domstate "$NAME")" == "running" ]]; then - echo "Force stopping $NAME..." - virsh -c "$URI" destroy "$NAME" +# Disk cleanup +if [[ -e "$DISK" ]]; then + echo "Removing disk: $DISK" + sudo rm -f "$DISK" +else + echo "WARN: Disk not found: $DISK" fi -echo "Undefining $NAME..." -virsh -c "$URI" undefine "$NAME" - -echo "Deleting disk: $DISK" -sudo rm -v -- "$DISK" - -echo "Lab '$NAME' destroyed." +echo "OK: Destroyed lab '$NAME'." diff --git a/kvm-lab-destroy.bak b/kvm-lab-destroy.bak new file mode 100755 index 0000000..3548ef9 --- /dev/null +++ b/kvm-lab-destroy.bak @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +URI="qemu:///system" +LABDIR="/var/lib/libvirt/images/lab" + +NAME="${1:-}" + +if [[ -z "$NAME" ]]; then + echo "Usage: kvm-lab-destroy " + exit 1 +fi + +if [[ "$NAME" == "work01" || "$NAME" == "debian13-template" ]]; then + echo "Refusing to destroy protected VM: $NAME" + exit 1 +fi + +if ! virsh -c "$URI" dominfo "$NAME" &>/dev/null; then + echo "Domain not found: $NAME" + exit 1 +fi + +DISK="$(virsh -c "$URI" domblklist "$NAME" --details | awk '$3=="vda"{print $4}')" + +if [[ -z "$DISK" || "$DISK" != "$LABDIR/"* ]]; then + echo "Refusing to destroy: disk not under $LABDIR" + echo "Detected disk: $DISK" + exit 1 +fi + +STATE="$(virsh -c "$URI" domstate "$NAME")" + +if [[ "$STATE" == "running" ]]; then + echo "Shutting down $NAME..." + virsh -c "$URI" shutdown "$NAME" + sleep 3 +fi + +if [[ "$(virsh -c "$URI" domstate "$NAME")" == "running" ]]; then + echo "Force stopping $NAME..." + virsh -c "$URI" destroy "$NAME" +fi + +echo "Undefining $NAME..." +virsh -c "$URI" undefine "$NAME" + +echo "Deleting disk: $DISK" +sudo rm -v -- "$DISK" + +echo "Lab '$NAME' destroyed." + diff --git a/officium b/officium index fae202f..c40c35e 100755 --- a/officium +++ b/officium @@ -1,9 +1,9 @@ #!/bin/bash # Scriptum ad preces e Divinum Officium extrahendas -# Auctor: Adam Kasprzak +# Auctores: Adam Gomułka, Łukasz Kasprzak # Data: 2026-01-13 - +# Versio: 0.2 set -o pipefail if [ $# -eq 0 ]; then -- cgit v1.3