脚本:虚拟机管理kvm

命令说明

kvm.sh —— 虚拟机管理

  kvm list                                    列出所有虚拟机 + 开机自启列表
  kvm info  [虚拟机]                          看静态配置 + 运行信息(缺省取 list 第一个)
  kvm create [虚拟机] <目录|qcow2路径> [方案]   创建(定义)虚拟机,创建后为 off,不自动运行
  kvm switch [虚拟机] [方案] [on|off|run]      切换 xml 方案(缺省方案=虚拟机名.xml;缺省 run)
  kvm run|boot|start|up|on    [虚拟机]         开机(已开机不报错)
  kvm stop|down|off           [虚拟机]         关机(ACPI 优雅)
  kvm reboot|restart          [虚拟机]         重启
  kvm poweroff|destroy        [虚拟机]         强制断电
  kvm autostart [--disable]   [虚拟机]         设/取消 开机自启
  kvm login|desktop           [虚拟机]         进入图形桌面
  kvm exec|console            [虚拟机]         进入文本控制台

/etc/nixos/modules/system/service/qemu.nix

  # 把组件脚本装成系统命令 `kvm`(内部函数名/用法均不变)
  kvmCmd = pkgs.runCommand "kvm" { } ''
    install -Dm755 ${c.script} $out/bin/kvm
    sed -i '1s|.*|#!${pkgs.bash}/bin/bash|' $out/bin/kvm
  '';

/etc/nixos/modules/system/service/qemu.nix

#!/usr/bin/env bash
# ============================================================
#  kvm.sh —— 本机 KVM/libvirt 虚拟机管理命令
#  约定: 一台虚拟机 = /s500data/kvm/<虚拟机名>/ 一个子目录
#        方案文件: <虚拟机名>.xml(默认) 或 <虚拟机名>-<方案名>.xml
#        系统盘:   <虚拟机名>.qcow2
#  建议: alias kvm=/s500data/kvm/kvm.sh   之后即可用 `kvm <命令>`
# ============================================================
set -uo pipefail

KVM_ROOT="/s500data/kvm"
CONN="qemu:///system"
V="virsh -c $CONN"

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

# ---------- 辅助 ----------
scheme_xml(){ # <vm> [scheme] -> 方案 xml 路径
  if [ -z "${2:-}" ]; then echo "$KVM_ROOT/$1/$1.xml"; else echo "$KVM_ROOT/$1/$1-$2.xml"; fi
}
xml_name(){ sed -n 's:.*<name>\(.*\)</name>.*:\1:p' "$1" 2>/dev/null | head -1; }

list_vms(){ # 所有虚拟机(含 .qcow2 或 .xml 的子目录),按名排序
  local d
  for d in "$KVM_ROOT"/*/; do
    [ -d "$d" ] || continue
    if compgen -G "${d}*.qcow2" >/dev/null || compgen -G "${d}*.xml" >/dev/null; then
      basename "$d"
    fi
  done | sort
}
dom_of_vm(){ # 该虚拟机当前已定义的域(按“磁盘路径归属”识别),无则空
  local vm="$1" d
  while read -r d; do
    [ -n "$d" ] || continue
    if $V dumpxml --inactive "$d" 2>/dev/null | grep -q "source file='$KVM_ROOT/$vm/"; then
      echo "$d"; return 0
    fi
  done < <($V list --all --name 2>/dev/null | sed '/^$/d')
  return 1
}
vm_for(){ # 目标虚拟机:给了就用;没给取 list 第一个
  if [ -n "${1:-}" ]; then echo "$1"; return; fi
  local f; f="$(list_vms | head -1)"
  [ -n "$f" ] || die "没有可用虚拟机"
  echo "$f"
}
avail_schemes(){ ls "$KVM_ROOT/$1/$1"*.xml 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' '; }

usage(){
cat <<'EOF'
kvm.sh —— 虚拟机管理

  kvm list                                    列出所有虚拟机 + 开机自启列表
  kvm info  [虚拟机]                          看静态配置 + 运行信息(缺省取 list 第一个)
  kvm create [虚拟机] <目录|qcow2路径> [方案]   创建(定义)虚拟机,创建后为 off,不自动运行
  kvm switch [虚拟机] [方案] [on|off|run]      切换 xml 方案(缺省方案=虚拟机名.xml;缺省 run)
  kvm run|boot|start|up|on    [虚拟机]         开机(已开机不报错)
  kvm stop|down|off           [虚拟机]         关机(ACPI 优雅)
  kvm reboot|restart          [虚拟机]         重启
  kvm poweroff|destroy        [虚拟机]         强制断电
  kvm autostart [--disable]   [虚拟机]         设/取消 开机自启
  kvm login|desktop           [虚拟机]         进入图形桌面
  kvm exec|console            [虚拟机]         进入文本控制台

  方案名: 空=默认(<虚拟机>.xml) | 例如 fast / t14 / max2  →  <虚拟机>-<方案>.xml
EOF
}

# ---------- 命令 ----------
cmd_list(){
  printf "%-14s %-20s %-9s %-6s %s\n" "虚拟机" "当前方案/域" "状态" "自启" "磁盘"
  local vm cur st as disk
  for vm in $(list_vms); do
    cur="$(dom_of_vm "$vm" || true)"
    if [ -n "$cur" ]; then
      st="$($V domstate "$cur" 2>/dev/null)"; [ "$st" = running ] && st="运行中" || st="已关机"
      as="$($V dominfo "$cur" 2>/dev/null | awk '/^Autostart:/{print $2}')"
      [ "$as" = enable ] && as="是" || as="否"
    else
      st="未定义"; as="-"; cur="—"
    fi
    disk="$(ls "$KVM_ROOT/$vm"/*.qcow2 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ')"
    printf "%-14s %-20s %-9s %-6s %s\n" "$vm" "$cur" "$st" "$as" "${disk:-—}"
  done
  echo
  echo "【开机自启列表】"
  $V list --autostart --all 2>/dev/null
}

cmd_info(){
  local vm cur X
  vm="$(vm_for "${1:-}")"
  echo "==================== 虚拟机: $vm ===================="
  echo "实例目录: $KVM_ROOT/$vm"
  cur="$(dom_of_vm "$vm" || true)"
  if [ -n "$cur" ]; then
    echo "当前域  : $cur  ($($V domstate "$cur" 2>/dev/null))"
    X="$($V dumpxml --inactive "$cur" 2>/dev/null)"
  else
    echo "当前域  : (未定义)"
    X="$(cat "$KVM_ROOT/$vm/$vm.xml" 2>/dev/null)"
  fi
  [ -n "$X" ] || die "无可用配置(既未定义域,也无默认 $vm.xml)"

  echo "---- 静态配置 (KVM 设定) ----"
  echo "$X" | sed -n 's/^[[:space:]]*//;s/^\(<name>\|<uuid>\|<vcpu[^>]*>\|<memory[^>]*>\|<currentMemory[^>]*>\|.*machine=[^ ]*\|<source file=.*\|<model type=.*\|<mac address=.*\|<graphics type=.*\|<source network=.*\)/\1/p'
  echo "---- 运行信息 ----"
  if [ -n "$cur" ] && [ "$($V domstate "$cur" 2>/dev/null)" = running ]; then
    $V domstats "$cur" 2>/dev/null | grep -E "state.state|cpu.time|vcpu.current|vcpu.maximum|balloon.rss|block.1.(rd|wr)_bytes|net.0.(rx|tx).bytes" | sed 's/^/  /'
    echo "  -- 磁盘 --"; $V domblklist "$cur" 2>/dev/null | sed 's/^/  /'
    echo "  -- 网卡 --"; $V domiflist "$cur" 2>/dev/null | sed 's/^/  /'
    echo "  -- IP  --"; $V domifaddr "$cur" --source lease 2>/dev/null | sed 's/^/  /'
  else
    echo "  (未运行)"
  fi
  echo "---- 存储池 / 卷 ----"
  $V pool-list --all 2>/dev/null | sed 's/^/  /'
  $V vol-list "$vm" 2>/dev/null | sed 's/^/  /'
  echo "---- 网络 ----"
  $V net-list --all 2>/dev/null | sed 's/^/  /'
  ip -4 addr show virbr0 2>/dev/null | awk '/inet /{print "  virbr0 "$2}'
}

cmd_create(){
  local a1="${1:-}" a2="${2:-}" a3="${3:-}" vm path sc dir disk xml cur
  [ -n "$a1" ] || die "用法: kvm create [虚拟机] <目录|qcow2> [方案]"
  if [[ "$a1" == */* || -e "$a1" ]]; then vm=""; path="$a1"; sc="$a2"; else vm="$a1"; path="$a2"; sc="$a3"; fi
  [ -n "$path" ] || die "缺少路径"
  if [ -d "$path" ]; then dir="${path%/}"; else dir="$(dirname "$path")"; fi
  [ -n "$vm" ] || vm="$(basename "$dir")"
  if [ -d "$path" ]; then disk="$dir/$vm.qcow2"; else disk="$path"; fi
  xml="$(scheme_xml "$vm" "$sc")"
  [ -d "$dir" ] || die "目录不存在: $dir"
  [ -f "$disk" ] || die "磁盘不存在: $disk"
  [ -f "$xml" ] || die "方案文件不存在: $xml  (现有: $(avail_schemes "$vm"))"
  if ! $V pool-info "$vm" >/dev/null 2>&1; then
    $V pool-define-as "$vm" dir --target "$dir" >/dev/null 2>&1
    $V pool-build "$vm" >/dev/null 2>&1; $V pool-start "$vm" >/dev/null 2>&1; $V pool-autostart "$vm" >/dev/null 2>&1
    echo "已建存储池: $vm → $dir"
  fi
  cur="$(dom_of_vm "$vm" || true)"
  if [ -n "$cur" ]; then
    [ "$($V domstate "$cur" 2>/dev/null)" = running ] && die "虚拟机 $vm 正在运行($cur),请先 stop/poweroff 再 create"
    $V undefine "$cur" --nvram >/dev/null 2>&1 || $V undefine "$cur" >/dev/null 2>&1
  fi
  $V define "$xml" >/dev/null || die "define 失败: $xml"
  echo "已创建: $vm  方案=${sc:-默认}  域='$(xml_name "$xml")'  状态=off(create 不自动运行)"
}

cmd_switch(){
  local vm="${1:-}" sc="${2:-}" mode="${3:-}"
  case "$sc" in on|off|run|start) mode="$sc"; sc="";; esac
  [ -n "$mode" ] || mode="run"
  vm="$(vm_for "$vm")"
  local dir="$KVM_ROOT/$vm" xml cur dn
  xml="$(scheme_xml "$vm" "$sc")"
  [ -f "$xml" ] || die "找不到方案文件: $xml  (现有: $(avail_schemes "$vm"))"
  dn="$(xml_name "$xml")"; [ -n "$dn" ] || die "方案文件缺 <name>: $xml"
  cur="$(dom_of_vm "$vm" || true)"
  if [ -n "$cur" ]; then
    [ "$($V domstate "$cur" 2>/dev/null)" = running ] && $V destroy "$cur" >/dev/null 2>&1
    { $V undefine "$cur" --nvram >/dev/null 2>&1 || $V undefine "$cur" >/dev/null 2>&1; }
  fi
  $V define "$xml" >/dev/null || die "define 失败: $xml"
  echo "已切换: $vm  方案=${sc:-(默认)}  →  域 '$dn'  (${xml##*/})"
  if [ "$mode" = off ]; then echo "(off:切换后未启动)"; return 0; fi
  $V start "$dn" >/dev/null && echo "已启动 '$dn'"
}

cmd_run(){
  local vm cur dx
  vm="$(vm_for "${1:-}")"
  cur="$(dom_of_vm "$vm" || true)"
  if [ -z "$cur" ]; then
    dx="$KVM_ROOT/$vm/$vm.xml"
    [ -f "$dx" ] || die "虚拟机 $vm 未定义,且无默认方案 $vm.xml(请先 create)"
    cur="$(xml_name "$dx")"; $V define "$dx" >/dev/null || die "define 失败: $dx"
    echo "未定义 → 已按默认方案 $vm.xml 定义域 '$cur'"
  fi
  if [ "$($V domstate "$cur" 2>/dev/null)" = running ]; then echo "虚拟机 $vm('$cur')已在运行"; return 0; fi
  $V start "$cur" >/dev/null && echo "已启动 $vm('$cur')"
}

cmd_stop(){
  local vm cur; vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  [ "$($V domstate "$cur" 2>/dev/null)" = running ] || { echo "虚拟机 $vm 未运行"; return 0; }
  $V shutdown "$cur" >/dev/null && echo "已发送关机(ACPI): $vm('$cur')"
}
cmd_reboot(){
  local vm cur; vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  [ "$($V domstate "$cur" 2>/dev/null)" = running ] || die "虚拟机 $vm 未运行,无法重启"
  $V reboot "$cur" >/dev/null && echo "已重启: $vm('$cur')"
}
cmd_poweroff(){
  local vm cur; vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  [ "$($V domstate "$cur" 2>/dev/null)" = running ] || { echo "虚拟机 $vm 未运行"; return 0; }
  $V destroy "$cur" >/dev/null && echo "已强制断电: $vm('$cur')"
}
cmd_autostart(){
  local disable=0 vm cur
  if [ "${1:-}" = "--disable" ]; then disable=1; shift; fi
  vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  if [ "$disable" = 1 ]; then $V autostart --disable "$cur" >/dev/null && echo "已取消开机自启: $vm('$cur')"
  else $V autostart "$cur" >/dev/null && echo "已设为开机自启: $vm('$cur')"; fi
}
cmd_login(){
  local vm cur; vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  [ "$($V domstate "$cur" 2>/dev/null)" = running ] || die "虚拟机 $vm 未运行,请先 run"
  command -v virt-viewer >/dev/null || die "缺 virt-viewer"
  echo "打开桌面: $vm('$cur')..."; exec virt-viewer -c "$CONN" "$cur"
}
cmd_console(){
  local vm cur; vm="$(vm_for "${1:-}")"; cur="$(dom_of_vm "$vm" || true)"
  [ -n "$cur" ] || die "虚拟机 $vm 未定义"
  [ "$($V domstate "$cur" 2>/dev/null)" = running ] || die "虚拟机 $vm 未运行,请先 run"
  echo "进入文本控制台: $vm('$cur')  (退出: Ctrl+] )"
  exec $V console "$cur"
}

# ---------- 分发 ----------
cmd="${1:-}"; [ $# -gt 0 ] && shift
case "$cmd" in
  list|ls)                    cmd_list ;;
  info)                       cmd_info "${1:-}" ;;
  create)                     cmd_create "${1:-}" "${2:-}" "${3:-}" ;;
  switch|sw)                  cmd_switch "${1:-}" "${2:-}" "${3:-}" ;;
  run|boot|start|up|on)       cmd_run "${1:-}" ;;
  stop|down|off)              cmd_stop "${1:-}" ;;
  reboot|restart)             cmd_reboot "${1:-}" ;;
  poweroff|destroy|destory)   cmd_poweroff "${1:-}" ;;
  autostart)                  cmd_autostart "${1:-}" "${2:-}" ;;
  login|desktop|view)         cmd_login "${1:-}" ;;
  exec|console)               cmd_console "${1:-}" ;;
  ""|-h|--help|help)          usage ;;
  *) echo "未知命令: $cmd"; echo; usage; exit 2 ;;
esac