Wren Linux

wren-linux / lib / vm-size.sh

# shellcheck shell=bash
# lib/vm-size.sh: how much memory and disk claude-vm's VM gets (extras/claude-vm.sh;
# bin/claude-vm-session reads the file). Sourced after lib.sh. Sizes are in MiB.
#
# The choice is kept in a file only root can change, one line per VM user:
# "USER MEMORY DISK". The VM's user can't give itself more. Memory counts from
# the VM's next start, the disk's size from its next build (claude-vm rebuild).
#
# What this computer can spare, so the VM never takes so much that the computer
# itself runs short:
#  - memory: suggested a third of it (2 to 8 GB); at most half, and always
#    4 GB left for the computer;
#  - disk: suggested 40 GB; at most what is free (the VM's own disk counts as
#    free, a rebuild replaces it) less 20 GB. The VM's disk file grows as the
#    VM fills it, so its size is what it may grow to, not what it takes at once;
#  - none at all with less than 4 GB of memory or 25 GB of disk free.

VM_SIZE_FILE=${WREN_VM_SIZE_FILE:-/etc/wren/claude-vm-size}
VM_MEM_DEFAULT=2048 VM_DISK_DEFAULT=20000
VM_MEM_MIN=2048 VM_DISK_MIN=20000
VM_HOST_KEEP_MEM=4096 VM_HOST_KEEP_DISK=20000

# vm_size_limits TOTAL-MEMORY FREE-DISK: "SUGGESTED-MEMORY MOST-MEMORY
# SUGGESTED-DISK MOST-DISK" for a computer with that much (MiB). The most is
# never under the minimum (the VM needs that much to run at all): vm_size_tight
# says when that is more than this computer can spare.
vm_size_limits() {
    local total=$1 free=$2 rec_mem max_mem rec_disk max_disk
    max_mem=$((total / 2))
    [ $((total - VM_HOST_KEEP_MEM)) -ge "$max_mem" ] || max_mem=$((total - VM_HOST_KEEP_MEM))
    max_mem=$((max_mem / 256 * 256))
    [ "$max_mem" -ge "$VM_MEM_MIN" ] || max_mem=$VM_MEM_MIN
    rec_mem=$((total / 3 / 256 * 256))
    [ "$rec_mem" -ge "$VM_MEM_MIN" ] || rec_mem=$VM_MEM_MIN
    [ "$rec_mem" -le 8192 ] || rec_mem=8192
    [ "$rec_mem" -le "$max_mem" ] || rec_mem=$max_mem
    max_disk=$(((free - VM_HOST_KEEP_DISK) / 1000 * 1000))
    [ "$max_disk" -ge "$VM_DISK_MIN" ] || max_disk=$VM_DISK_MIN
    rec_disk=40000
    [ "$rec_disk" -le "$max_disk" ] || rec_disk=$max_disk
    echo "$rec_mem $max_mem $rec_disk $max_disk"
}

# vm_size_fits TOTAL-MEMORY FREE-DISK: fails, saying why, when even the smallest
# VM would leave this computer too little to keep running (2 GB of memory, 5 GB
# of disk)
vm_size_fits() {
    [ $(($1 - VM_MEM_MIN)) -ge 2048 ] ||
        { echo "This computer has $((($1 + 512) / 1024)) GB of memory: the smallest VM ($((VM_MEM_MIN / 1024)) GB) would leave it less than 2 GB, too little to keep running."; return 1; }
    [ $(($2 - VM_DISK_MIN)) -ge 5000 ] ||
        { echo "Only $(($2 / 1000)) GB of disk is free: the smallest VM disk ($((VM_DISK_MIN / 1000)) GB) could fill it. Make room first (at least $(((VM_DISK_MIN + 5000) / 1000)) GB free)."; return 1; }
}

# vm_size_tight TOTAL-MEMORY FREE-DISK: what the minimum leaves too little of, one line each
vm_size_tight() {
    [ $(($1 - VM_MEM_MIN)) -ge "$VM_HOST_KEEP_MEM" ] ||
        echo "This computer has $((($1 + 512) / 1024)) GB of memory: even the smallest VM ($((VM_MEM_MIN / 1024)) GB) leaves it less than $((VM_HOST_KEEP_MEM / 1024)) GB. Close other programs while the VM runs."
    [ $(($2 - VM_DISK_MIN)) -ge "$VM_HOST_KEEP_DISK" ] ||
        echo "Only $(($2 / 1000)) GB of disk is free: a full VM disk ($((VM_DISK_MIN / 1000)) GB) would leave less than $((VM_HOST_KEEP_DISK / 1000)) GB. Make room first."
    return 0
}

# vm_size_host USER: "TOTAL-MEMORY FREE-DISK" here, in MiB; the disk is the one
# USER's home is on, its VM's disk file counted as free
vm_size_host() {
    local total free vmdisk
    total=$(awk '$1 == "MemTotal:" { print int($2 / 1024) }' /proc/meminfo)
    free=$(df -Pm /home | awk 'NR == 2 { print $4 }')
    vmdisk=$(sudo du -sm "/home/$1/vm" 2>/dev/null | cut -f1) || vmdisk=0
    echo "$total $((free + ${vmdisk:-0}))"
}

# vm_size_get USER: "MEMORY DISK" kept for USER, or nothing
vm_size_get() {
    awk -v u="$1" '$1 == u && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ { print $2, $3; exit }' "$VM_SIZE_FILE" 2>/dev/null || true
}

# vm_size_set USER MEMORY DISK: keeps it (root's file, readable by all)
vm_size_set() {
    local tmp
    tmp=$(mktemp)
    { awk -v u="$1" '$1 != u' "$VM_SIZE_FILE" 2>/dev/null || true; echo "$1 $2 $3"; } >"$tmp"
    sudo install -d -o root -g root -m 0755 "${VM_SIZE_FILE%/*}"
    sudo install -o root -g root -m 0644 "$tmp" "$VM_SIZE_FILE"
    rm -f "$tmp"
}

# vm_size_remove USER: forgets USER's line
vm_size_remove() {
    if [ -f "${VM_SIZE_FILE%-size}-cpus" ]; then
        sudo sed -i "/^$1 /d" "${VM_SIZE_FILE%-size}-cpus"
    fi
    [ -f "$VM_SIZE_FILE" ] || return 0
    local tmp
    tmp=$(mktemp)
    awk -v u="$1" '$1 != u' "$VM_SIZE_FILE" >"$tmp"
    if [ -s "$tmp" ]; then sudo install -o root -g root -m 0644 "$tmp" "$VM_SIZE_FILE"; else sudo rm -f "$VM_SIZE_FILE"; fi
    rm -f "$tmp"
}

# vm_size_choose USER [MEMORY] [DISK]: the VM's size. Given ones are checked
# against what this computer can spare; with none given, a size kept already
# stays, and otherwise you are shown this computer's and asked (Enter takes
# the suggestion).
vm_size_choose() {
    local user=$1 mem=${2:-} disk=${3:-} total free rec_mem max_mem rec_disk max_disk have line
    have=$(vm_size_get "$user")
    if [ -z "$mem" ] && [ -z "$disk" ] && [ -n "$have" ]; then
        read -r mem disk <<<"$have"
        note "The VM keeps its size: $mem MB of memory, a disk of up to $disk MB (change it: --memory MB --disk MB)."
        return 0
    fi
    read -r total free <<<"$(vm_size_host "$user")"
    read -r rec_mem max_mem rec_disk max_disk <<<"$(vm_size_limits "$total" "$free")"
    say "How big the VM is"
    note "This computer: $(((total + 512) / 1024)) GB of memory, $((free / 1000)) GB of disk free."
    line=$(vm_size_fits "$total" "$free") || die "$line"
    note "Suggested: $rec_mem MB of memory (at most $max_mem), a disk of up to $rec_disk MB (at most $max_disk)."
    while read -r line; do [ -z "$line" ] || warn "$line"; done < <(vm_size_tight "$total" "$free")
    if [ -z "$mem" ] && [ -z "$disk" ]; then
        tty_read mem "    Memory in MB (Enter for $rec_mem): "
        tty_read disk "    Disk in MB (Enter for $rec_disk): "
    fi
    # A space typed before the number (the prompt reader keeps it) shouldn't fail
    # it; and a space-only answer counts as Enter, falling back to the suggestion.
    read -r mem <<<"$mem"
    read -r disk <<<"$disk"
    [ -n "$mem" ] || mem=${have%% *}
    [ -n "$mem" ] || mem=$rec_mem
    [ -n "$disk" ] || disk=${have##* }
    [ -n "$disk" ] || disk=$rec_disk
    [[ $mem =~ ^[0-9]{1,6}$ ]] && [ "$mem" -ge "$VM_MEM_MIN" ] && [ "$mem" -le "$max_mem" ] ||
        die "Memory: $mem MB isn't between $VM_MEM_MIN and $max_mem (more would leave this computer short). Nothing was changed."
    [[ $disk =~ ^[0-9]{1,7}$ ]] && [ "$disk" -ge "$VM_DISK_MIN" ] && [ "$disk" -le "$max_disk" ] ||
        die "Disk: $disk MB isn't between $VM_DISK_MIN and $max_disk (more could fill this computer's disk). Nothing was changed."
    vm_size_set "$user" "$mem" "$disk"
    note "Kept: $mem MB of memory (from the VM's next start), a disk of up to $disk MB (from its next build: claude-vm rebuild)."
}

# vm_cpus_choose USER N: keeps how many CPUs the VM gets, from its next start: at least 1, at
# most all but 2 of this computer's (or 2, the engine's own default, on a small one). Root's file,
# as for the size.
VM_CPUS_FILE=${VM_SIZE_FILE%-size}-cpus
vm_cpus_max() {
    local n=$1
    if [ "$n" -gt 4 ]; then echo "$((n - 2))"; else echo 2; fi
}
vm_cpus_choose() {
    local tmp max
    max=$(vm_cpus_max "$(nproc)")
    [[ $2 =~ ^[0-9]{1,2}$ ]] && [ "$2" -ge 1 ] && [ "$2" -le "$max" ] ||
        die "CPUs: $2 isn't between 1 and $max (this computer has $(nproc), and needs some for itself). Nothing changed."
    tmp=$(mktemp)
    { awk -v u="$1" '$1 != u' "$VM_CPUS_FILE" 2>/dev/null || true; echo "$1 $2"; } >"$tmp"
    sudo install -d -o root -g root -m 0755 "${VM_CPUS_FILE%/*}"
    sudo install -o root -g root -m 0644 "$tmp" "$VM_CPUS_FILE"
    rm -f "$tmp"
    note "Kept: $2 CPUs, from the VM's next start."
}