0aaa25150c
- category hub with basket counts, review screen, single confirm - GPU: nvidia-smi → /proc/driver/nvidia → vendor scan detection - host devices: lsusb/tty/video/snd/lsblk + manual input, dedupe - dir mounts with (system disk — careful) labels - SHOULD tier: image/ports/cpus/mem, flag contract --gpu/--device/--dir/--port/--cpus/--memory/--network - zero-flag run byte-identical to pre-edit - cmd_unpersist not-found exit 0 → return 0 in both clients - DOC/howto/docker.md: vbox categorized create section
1126 lines
40 KiB
Bash
Executable File
1126 lines
40 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
# POS: docker vbox — Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
|
|
# POS_SUBCMDS: create enter stop start rm ls menu
|
|
# POS_FLAGS: --dir --gpu --device --port --cpus --memory --network
|
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
|
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
pos docker vbox create <name> [image] [options]
|
|
pos docker vbox enter <name>
|
|
pos docker vbox stop <name>
|
|
pos docker vbox start <name>
|
|
pos docker vbox rm <name>
|
|
pos docker vbox ls
|
|
|
|
Manage disposable Docker containers as lightweight VMs.
|
|
|
|
Bare \`pos docker vbox\` on a terminal (or \`pos docker vbox menu\`) opens an
|
|
interactive menu wrapping these verbs; arguments stay scriptable. The
|
|
interactive create flow picks image, GPU, host devices, dir mounts, ports
|
|
and CPU/RAM from categorized menus, shows a review screen, then runs the
|
|
same \`create\` verb shown below.
|
|
|
|
Each container gets a bind-mounted host directory so files persist
|
|
on the host even after the container is removed.
|
|
|
|
Options:
|
|
--dir <path> Bind-mount a host directory at the same path inside
|
|
the VM (repeatable). The first --dir is the VM's
|
|
working directory and defaults to ~/<name>;
|
|
use "." for the current directory.
|
|
--gpu Full GPU access (--gpus all)
|
|
--device </dev/node> Pass a host device through to the VM (repeatable)
|
|
--port HOST:CONTAINER Publish a port mapping (repeatable, e.g. 8080:80)
|
|
--cpus N Limit CPUs (e.g. 2 or 1.5; default: Docker default)
|
|
--memory SIZE Limit memory (e.g. 512m, 2g; default: Docker default)
|
|
--network MODE Docker network mode (bridge/host/none)
|
|
|
|
Examples:
|
|
pos docker vbox create lab1
|
|
pos docker vbox create lab1 --dir .
|
|
pos docker vbox create lab1 --dir /mnt/data/lab1
|
|
pos docker vbox create kali kalilinux/kali-rolling
|
|
pos docker vbox create ai --gpu --cpus 4 --memory 8g
|
|
pos docker vbox create iot --device /dev/ttyUSB0 --port 8080:80
|
|
pos docker vbox enter lab1
|
|
pos docker vbox stop lab1
|
|
pos docker vbox start lab1
|
|
pos docker vbox rm lab1
|
|
pos docker vbox ls
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# ── Interactive menu (opt-in front door, Pattern B via lib/menu-lib.sh) ──
|
|
# Every item re-enters the existing CLI verbs below unchanged — the menu adds
|
|
# picks/prompts/confirms only (same self-invocation the tool itself uses for
|
|
# tmux handovers). 'enter' hands the terminal to the container shell and
|
|
# returns to the loop when the shell exits.
|
|
menu_self() { # run this tool's own verb as a child process
|
|
"$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" "$@"
|
|
}
|
|
|
|
menu_list_vms() { # vbox VM names, one per line (same label filter as 'ls')
|
|
docker ps -a --filter label=linux_post_install.vbox=true --format '{{.Names}}' | LC_ALL=C sort
|
|
}
|
|
|
|
menu_pick_vm() { # $1 = prompt → picked VM name on stdout · rc 1 = cancelled / none
|
|
local -a vms=()
|
|
mapfile -t vms < <(menu_list_vms 2>/dev/null)
|
|
if [ ${#vms[@]} -eq 0 ]; then
|
|
if docker info &>/dev/null; then
|
|
warn "no vbox VMs found — create one first ('pos docker vbox create <name>')"
|
|
else
|
|
warn "docker daemon not reachable — start Docker first"
|
|
fi
|
|
return 1
|
|
fi
|
|
local idx
|
|
idx="$(menu_pick "$1" "${vms[@]}")" || return 1
|
|
printf '%s\n' "${vms[$((idx - 1))]}"
|
|
}
|
|
|
|
# ── Categorized interactive create ────────────────────────────────────────
|
|
# Pure sugar over the scripted 'create' verb (single execution path): this
|
|
# flow composes arguments through Name → category hub → Review, then re-enters
|
|
# 'menu_self create …'. All state lives in C_*/CC_* globals for the lifetime
|
|
# of the flow only.
|
|
#
|
|
# Quit/EOF discipline (binding): nothing is ever created unless the review
|
|
# confirm is answered 'y', and EVERY teardown ends in ONE
|
|
# "[!] setup discarded — nothing was created" line, back at the main vbox
|
|
# menu, exit clean. Helpers never print the line themselves — they unwind
|
|
# with rc 77, and the orchestrator converts that into the single print.
|
|
# menu_run/menu_pick/confirm report a typed quit and a dead stream
|
|
# identically (rc 1), and no probe can always tell them apart, so
|
|
# arbitration is streak-based: TWO consecutive quit signals with no
|
|
# successful interaction between them tear down (a vanished terminal fails
|
|
# every read instantly; a live user quitting twice gets decisive teardown),
|
|
# while a single quit keeps gentle per-site handling (hub quit asks first).
|
|
# Raw `read` calls need no arbitration — their rc 1 IS a definitive EOF.
|
|
# Confirmed discards (hub quit answered 'y') are self-explanatory and silent;
|
|
# at the NAME prompt an empty answer and a dead stream are indistinguishable
|
|
# (menu_ask_value has no default there), so both tear down loudly.
|
|
C_discarded="setup discarded — nothing was created"
|
|
|
|
cc_log() { # stderr twin of log() — capture-safe inside $( )
|
|
printf '%s\n' "${GREEN}[+]${RESET} $*" >&2
|
|
}
|
|
|
|
cc_warn() { # stderr twin of warn() — capture-safe inside $( )
|
|
printf '%s\n' "${YELLOW}[!]${RESET} $*" >&2
|
|
}
|
|
|
|
cc_info() { # stderr [i] info line
|
|
printf '%s\n' "${CYAN:-}[i]${RESET} $*" >&2
|
|
}
|
|
|
|
cc_die() { # print the discard line once; caller returns 77
|
|
# Best-effort emit: on a vanished terminal even this write fails (EIO)
|
|
# and must not trip set -e — the cleanup contract is exit-clean.
|
|
cc_warn "$C_discarded" || true
|
|
}
|
|
|
|
# Quit-vs-EOF arbitration: menu_run/menu_pick/confirm report a typed quit and
|
|
# a dead input stream identically (rc 1), and no read/write probe can always
|
|
# distinguish them (bash `read -t` reports success on a dead pty; write-EIO is
|
|
# kernel-dependent). Rule instead: TWO consecutive quit signals with no
|
|
# successful interaction in between tear the setup down — a dead terminal
|
|
# fails every read instantly, so it tears down at once; a live user quitting
|
|
# twice in a row gets the same decisive treatment; a single quit keeps the
|
|
# gentle per-site handling. Successful interactions reset the streak.
|
|
cc_quit_tick() { # rc 0 = streak reached ⇒ caller tears down · rc 1 = keep going
|
|
C_quits=$(( ${C_quits:-0} + 1 ))
|
|
[ "${C_quits}" -ge 2 ]
|
|
}
|
|
cc_quit_reset() { C_quits=0; }
|
|
|
|
cc_reset() {
|
|
C_image="ubuntu:22.04"
|
|
C_image_set=0 # 0 = Image never visited (hub shows "(default)")
|
|
C_gpu_mode="" # "" | all | nodes
|
|
C_gpu_devs=() # explicit Nvidia/exotic nodes (mode=nodes)
|
|
C_devs=() # host devices basket (raw paths)
|
|
C_mounts=() # host dir mounts basket (validated absolutes)
|
|
C_ports=() # ports basket (HOST:CONTAINER)
|
|
C_cpus=""
|
|
C_mem=""
|
|
}
|
|
|
|
# ── device helpers ────────────────────────────────────────────────────────
|
|
cc_perm_suffix() { # unreadable-but-present nodes stay offered (dockerd is root)
|
|
[ -r "$1" ] && return 0
|
|
printf '%s' " (perm-restricted for you — dockerd may still access)"
|
|
}
|
|
|
|
cc_node_label() { # <node> → display label on stdout · rc 1 = unusable, drop
|
|
[ -e "$1" ] || return 1
|
|
stat "$1" &>/dev/null || return 1
|
|
printf '%s%s' "$1" "$(cc_perm_suffix "$1")"
|
|
}
|
|
|
|
cc_root_disk() { # basename of the disk hosting / — best effort, may fail
|
|
local src pk
|
|
src="$(findmnt -n -o SOURCE / 2>/dev/null)"
|
|
[ -n "$src" ] || return 1
|
|
pk="$(lsblk -sno PKNAME "$src" 2>/dev/null | tail -n1)"
|
|
[ -n "$pk" ] || pk="${src##*/}"
|
|
printf '%s' "$pk"
|
|
}
|
|
|
|
cc_is_system_disk() { # <node> rc 0 iff it is (a partition of) the root disk
|
|
local root base="${1##*/}"
|
|
root="$(cc_root_disk)" || return 1
|
|
[ "$base" = "$root" ] && return 0
|
|
[[ "$base" =~ ^${root}(p[0-9]+|[0-9]+)$ ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Candidate host devices → parallel arrays (names + labels). Every source is
|
|
# optional: a missing binary means one info line, never an error. Block
|
|
# devices list ALL disks, system disks labelled — never hidden.
|
|
cc_collect_devices() { # $1 names-out · $2 labels-out (global array names)
|
|
local -n o_names="$1"
|
|
local -n o_labels="$2"
|
|
local line bus dev node lbl typ size rm
|
|
o_names=()
|
|
o_labels=()
|
|
|
|
if command -v lsusb &>/dev/null; then
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^Bus\ ([0-9]+)\ Device\ ([0-9]+): ]] || continue
|
|
bus="$(printf '%03d' "${BASH_REMATCH[1]}")"
|
|
dev="$(printf '%03d' "${BASH_REMATCH[2]}")"
|
|
node="/dev/bus/usb/$bus/$dev"
|
|
lbl="$(cc_node_label "$node")" || continue
|
|
o_names+=("$node")
|
|
o_labels+=("$lbl")
|
|
done < <(lsusb 2>/dev/null)
|
|
else
|
|
cc_info "lsusb not available — skipping USB device scan"
|
|
fi
|
|
|
|
for node in /dev/ttyUSB* /dev/ttyACM* /dev/video*; do
|
|
[ -e "$node" ] || continue
|
|
lbl="$(cc_node_label "$node")" || continue
|
|
o_names+=("$node")
|
|
o_labels+=("$lbl")
|
|
done
|
|
# sound offered as ONE entry (fine-grained snd nodes are out of scope)
|
|
if [ -e /dev/snd ]; then
|
|
o_names+=("/dev/snd")
|
|
o_labels+=("/dev/snd (sound subsystem)")
|
|
fi
|
|
|
|
if command -v lsblk &>/dev/null; then
|
|
while read -r node typ size rm; do
|
|
[ -e "$node" ] || continue
|
|
stat "$node" &>/dev/null || continue
|
|
lbl="$node ($typ, $size"
|
|
[ "$rm" = "1" ] && lbl+=", removable"
|
|
cc_is_system_disk "$node" && lbl+=", system disk — careful"
|
|
lbl+="$(cc_perm_suffix "$node")"
|
|
lbl+=")"
|
|
o_names+=("$node")
|
|
o_labels+=("$lbl")
|
|
done < <(lsblk -rnpo NAME,TYPE,SIZE,RM 2>/dev/null)
|
|
else
|
|
cc_info "lsblk not available — skipping block-device scan"
|
|
fi
|
|
}
|
|
|
|
# ── GPU / Nvidia detection (cheap-first, once per category entry) ──────────
|
|
cc_nvidia_nodes() { # fills C_nv_nodes · rc 0 iff /dev/nvidiactl anchor exists
|
|
C_nv_nodes=()
|
|
local f
|
|
[ -e /dev/nvidiactl ] || return 1
|
|
for f in /dev/nvidia*; do
|
|
[ -e "$f" ] || continue
|
|
[[ "${f##*/}" =~ ^nvidia[0-9]+$ ]] || continue
|
|
cc_node_label "$f" >/dev/null && C_nv_nodes+=("$f")
|
|
done
|
|
for f in /dev/nvidiactl /dev/nvidia-uvm; do
|
|
[ -e "$f" ] || continue
|
|
cc_node_label "$f" >/dev/null && C_nv_nodes+=("$f")
|
|
done
|
|
[ "${#C_nv_nodes[@]}" -gt 0 ]
|
|
}
|
|
|
|
cc_gpu_toolkit() { # rc 0 iff an Nvidia container runtime is plausibly present
|
|
[[ "$(docker info --format '{{json .Runtimes}}' 2>/dev/null)" == *nvidia* ]] && return 0
|
|
command -v nvidia-container-runtime &>/dev/null && return 0
|
|
command -v nvidia-container-ctk &>/dev/null
|
|
}
|
|
|
|
# ── shared basket mechanics (loop-of-single-picks, no lib changes) ────────
|
|
# Baskets are addressed by GLOBAL array name, passed verbatim down the chain
|
|
# and bound one hop deep with local -n (never nameref-to-nameref).
|
|
cc_add_loop() { # $1 basket-name · $2 noun · $3 picker prompt · $4 manual prompt
|
|
local -n basket="$1"
|
|
local noun="$2" add_prompt="$3" manual_prompt="$4"
|
|
local idx sel p q skip joined ci clbl
|
|
local -a items disp
|
|
while true; do
|
|
if [ "${#basket[@]}" -gt 0 ]; then
|
|
joined="$(
|
|
IFS=','
|
|
echo "${basket[*]}"
|
|
)"
|
|
printf '[i] %s — selected: %d: %s\n' "$noun" "${#basket[@]}" "$joined" >&2
|
|
else
|
|
printf '[i] %s — selected: 0: (none yet)\n' "$noun" >&2
|
|
fi
|
|
items=()
|
|
disp=()
|
|
for p in "${CC_CAND_NAMES[@]}"; do
|
|
skip=0
|
|
for q in "${basket[@]}"; do
|
|
[ "$q" = "$p" ] && {
|
|
skip=1
|
|
break
|
|
}
|
|
done
|
|
[ "$skip" -eq 1 ] && continue # already selected → excluded
|
|
items+=("$p")
|
|
clbl=""
|
|
for ((ci = 0; ci < ${#CC_CAND_NAMES[@]}; ci++)); do
|
|
if [ "${CC_CAND_NAMES[$ci]}" = "$p" ]; then
|
|
clbl="${CC_CAND_LABELS[$ci]}"
|
|
break
|
|
fi
|
|
done
|
|
disp+=("${clbl:-$p}")
|
|
done
|
|
items+=("__manual__")
|
|
disp+=("Type a device path manually")
|
|
items+=("__done__")
|
|
disp+=("✓ Done adding")
|
|
if ! idx="$(menu_pick "$add_prompt" "${disp[@]}")"; then
|
|
cc_quit_tick && return 77 # second consecutive quit ⇒ teardown
|
|
return 0 # back/Done ends the loop, basket kept
|
|
fi
|
|
cc_quit_reset
|
|
sel="${items[$((idx - 1))]}"
|
|
case "$sel" in
|
|
__done__)
|
|
return 0
|
|
;;
|
|
__manual__)
|
|
while true; do
|
|
if ! read -rp "$manual_prompt: " p; then
|
|
return 77 # raw read rc 1 = definitive EOF
|
|
fi
|
|
[ -z "$p" ] && break # empty = cancel
|
|
[[ "$p" == /dev/* ]] || {
|
|
cc_warn "device path must start with /dev/ — try again"
|
|
continue
|
|
}
|
|
skip=0
|
|
for q in "${basket[@]}"; do
|
|
[ "$q" = "$p" ] && skip=1
|
|
done
|
|
if [ "$skip" -eq 1 ]; then
|
|
cc_warn "$p is already selected — skipped"
|
|
break
|
|
fi
|
|
if ! stat "$p" &>/dev/null; then
|
|
cc_warn "cannot stat $p — skipped"
|
|
break
|
|
fi
|
|
basket+=("$p")
|
|
cc_log "added $p — pick another, or choose ✓ Done"
|
|
break
|
|
done
|
|
;;
|
|
*)
|
|
basket+=("$sel")
|
|
cc_log "added $sel — pick another, or choose ✓ Done"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
cc_remove_one() { # $1 basket-name · $2 remove prompt
|
|
local -n basket="$1"
|
|
local rm_prompt="$2"
|
|
local idx p i
|
|
local -a disp keep
|
|
if [ "${#basket[@]}" -eq 0 ]; then
|
|
cc_log "nothing selected yet"
|
|
return 0
|
|
fi
|
|
disp=()
|
|
for p in "${basket[@]}"; do disp+=("$p"); done
|
|
if ! idx="$(menu_pick "$rm_prompt" "${disp[@]}")"; then
|
|
cc_quit_tick && return 77
|
|
return 0
|
|
fi
|
|
cc_quit_reset
|
|
p="${basket[$((idx - 1))]}"
|
|
keep=()
|
|
for ((i = 0; i < ${#basket[@]}; i++)); do
|
|
[ "$i" -ne $((idx - 1)) ] && keep+=("${basket[i]}")
|
|
done
|
|
basket=("${keep[@]}")
|
|
cc_log "removed $p"
|
|
}
|
|
|
|
cc_clear_basket() { # $1 basket-name · $2 noun phrase ("device(s)")
|
|
local -n basket="$1"
|
|
local word="$2" n="${#basket[@]}"
|
|
if [ "$n" -eq 0 ]; then
|
|
cc_log "nothing selected yet"
|
|
return 0
|
|
fi
|
|
if ! confirm "Remove all $n selected $word?" n; then
|
|
cc_quit_tick && return 77
|
|
return 0
|
|
fi
|
|
cc_quit_reset
|
|
basket=()
|
|
cc_log "all $n $word removed"
|
|
}
|
|
|
|
# Shared basket shape: Add … / Remove one (N selected) / Clear all / Back.
|
|
# $5 = add function; remaining args forwarded to it verbatim.
|
|
cc_basket_menu() { # $1 basket-name · $2 noun · $3 remove-prompt · $4 clear-word · $5 add-fn …
|
|
local -n bref="$1"
|
|
local bname="$1" noun="$2" rm_prompt="$3" clear_word="$4" add_fn="$5"
|
|
shift 5
|
|
local idx
|
|
while true; do
|
|
if [ "${#bref[@]}" -eq 0 ]; then
|
|
"$add_fn" "$bname" "$@" || return 77
|
|
return 0 # empty basket → straight to adding
|
|
fi
|
|
if ! idx="$(menu_run "$noun" \
|
|
"Add …" \
|
|
"Remove one (${#bref[@]} selected)" \
|
|
"Clear all")"; then
|
|
cc_quit_tick && return 77
|
|
return 0 # Back
|
|
fi
|
|
cc_quit_reset
|
|
case "$idx" in
|
|
1) "$add_fn" "$bname" "$@" || return 77 ;;
|
|
2) cc_remove_one "$bname" "$rm_prompt" || return 77 ;;
|
|
3) cc_clear_basket "$bname" "$clear_word" || return 77 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Typed-value add loop (mounts, ports). Raw read — NOT menu_ask_value — so an
|
|
# EMPTY answer ("done") stays distinguishable from EOF (discard everything).
|
|
# Validator contract: $1 raw input · rest = basket values · normalized value
|
|
# on stdout · rc 1 = rejected (diagnostics printed by the validator).
|
|
cc_add_typed_loop() { # $1 basket-name · $2 prompt · $3 validator fn
|
|
local -n basket="$1"
|
|
local prompt="$2" vfn="$3"
|
|
local v norm
|
|
local -a vals
|
|
while true; do
|
|
if ! read -rp "$prompt: " v; then
|
|
return 77 # raw read rc 1 = definitive EOF
|
|
fi
|
|
[ -z "$v" ] && return 0 # empty = done
|
|
vals=("${basket[@]}")
|
|
if norm="$("$vfn" "$v" ${vals[@]+"${vals[@]}"})"; then
|
|
basket+=("$norm")
|
|
fi
|
|
done
|
|
}
|
|
|
|
cc_validate_mount() { # stdout: normalized absolute path · rc 1 = rejected
|
|
local raw="$1" v
|
|
shift
|
|
v="${raw/#\~/$HOME}"
|
|
case "$v" in
|
|
/*) ;;
|
|
*) cc_warn "not an absolute path: $raw — try again"; return 1 ;;
|
|
esac
|
|
[ -d "$v" ] || {
|
|
cc_warn "directory not found: $raw — try again"
|
|
return 1
|
|
}
|
|
local q
|
|
for q in "$@"; do
|
|
[ "$q" = "$v" ] && {
|
|
cc_warn "$v is already mounted — skipped"
|
|
return 1
|
|
}
|
|
done
|
|
printf '%s\n' "$v"
|
|
cc_log "will mount $v:$v"
|
|
}
|
|
|
|
cc_validate_port() { # stdout: validated HOST:CONTAINER · rc 1 = rejected
|
|
local v="$1"
|
|
shift
|
|
[[ "$v" =~ ^[0-9]+(:[0-9]+){1,2}$ ]] || {
|
|
cc_warn "not a HOST:CONTAINER pair: $v — try again (e.g. 8080:80)"
|
|
return 1
|
|
}
|
|
local hp="${v%%:*}" q hq
|
|
for q in "$@"; do
|
|
hq="${q%%:*}"
|
|
[ "$hq" = "$hp" ] && {
|
|
cc_warn "host port $hp already mapped — rejected"
|
|
return 1
|
|
}
|
|
done
|
|
printf '%s\n' "$v"
|
|
cc_log "will publish $v"
|
|
}
|
|
|
|
# ── categories ────────────────────────────────────────────────────────────
|
|
cc_category_image() {
|
|
local -a picks=("ubuntu:22.04" "ubuntu:24.04" "debian:12" "kalilinux/kali-rolling" "archlinux" "fedora:latest" "alpine:latest" "Other (type image ref)")
|
|
local idx ref
|
|
while true; do
|
|
if ! idx="$(menu_pick "Pick image" "${picks[@]}")"; then
|
|
cc_quit_tick && return 77
|
|
return 0 # back keeps the current image
|
|
fi
|
|
cc_quit_reset
|
|
if [ "$idx" -lt "${#picks[@]}" ]; then
|
|
C_image="${picks[$((idx - 1))]}"
|
|
C_image_set=1
|
|
cc_log "image set to $C_image"
|
|
return 0
|
|
fi
|
|
if ! ref="$(menu_ask_value "Image ref")"; then
|
|
cc_quit_tick && return 77
|
|
continue # cancelled typing → picker again
|
|
fi
|
|
cc_quit_reset
|
|
if [[ "$ref" =~ [[:space:]] ]]; then
|
|
cc_warn "not a valid image ref — spaces not allowed"
|
|
continue
|
|
fi
|
|
C_image="$ref"
|
|
C_image_set=1
|
|
cc_log "image set to $C_image"
|
|
return 0
|
|
done
|
|
}
|
|
|
|
cc_category_gpu() {
|
|
local nodes_present=0 toolkit_present=0 pci_line="" choice sem nl
|
|
if cc_nvidia_nodes; then nodes_present=1; fi
|
|
if cc_gpu_toolkit; then toolkit_present=1; fi
|
|
if command -v lspci &>/dev/null; then
|
|
pci_line="$(lspci 2>/dev/null | grep -Ei 'vga|3d controller' | grep -i nvidia | head -n1)" || pci_line=""
|
|
[ -n "$pci_line" ] && cc_log "Nvidia hardware: ${pci_line#* }"
|
|
fi
|
|
|
|
if [ "$nodes_present" -eq 0 ]; then
|
|
# Path C — no GPU found: informational, never an error, never blocking.
|
|
# Category stays enterable via the manual escape hatch.
|
|
cc_info "no Nvidia driver/GPU detected on this host — skipping GPU setup"
|
|
cc_info "an exotic device path can still be added manually"
|
|
CC_CAND_NAMES=()
|
|
CC_CAND_LABELS=()
|
|
cc_add_loop C_gpu_devs "GPU (manual)" \
|
|
"Pick device to ADD" \
|
|
"Device path (must start with /dev/, empty = cancel)" || return 77
|
|
[ "${#C_gpu_devs[@]}" -gt 0 ] && C_gpu_mode="nodes"
|
|
return 0
|
|
fi
|
|
|
|
local i_all="Full GPU access (--gpus all)"
|
|
local i_nodes="Explicit Nvidia device nodes"
|
|
local i_none="none (clears GPU configuration)"
|
|
local -a opts=() sems=()
|
|
if [ "$toolkit_present" -eq 1 ]; then
|
|
# Path A — full support: recommend --gpus all (highlight ≠ preselect).
|
|
cc_info "Nvidia container toolkit detected — --gpus all available"
|
|
opts=("$i_all (recommended)" "$i_nodes" "$i_none")
|
|
sems=("all" "nodes" "none")
|
|
else
|
|
# Path B — nodes without toolkit: steer to explicit nodes first.
|
|
cc_info "nvidia-container-toolkit not detected — --gpus all will likely fail; offering explicit device nodes instead (install nvidia-container-toolkit for CUDA workloads)"
|
|
opts=("$i_nodes (recommended here)" "Configure anyway (--gpus all)" "$i_none")
|
|
sems=("nodes" "all" "none")
|
|
fi
|
|
case "$C_gpu_mode" in
|
|
all) opts[0]+=" (current)" ;;
|
|
nodes) opts[1]+=" (current)" ;;
|
|
*) opts[2]+=" (current)" ;;
|
|
esac
|
|
if ! choice="$(menu_run "GPU / Nvidia" "${opts[@]}")"; then
|
|
cc_quit_tick && return 77
|
|
return 0
|
|
fi
|
|
cc_quit_reset
|
|
sem="${sems[$((choice - 1))]}"
|
|
case "$sem" in
|
|
all)
|
|
C_gpu_mode="all"
|
|
C_gpu_devs=()
|
|
cc_log "GPU: --gpus all"
|
|
;;
|
|
nodes)
|
|
C_gpu_mode="nodes"
|
|
C_gpu_devs=()
|
|
cc_info "a working set is usually /dev/nvidia0 + /dev/nvidiactl + /dev/nvidia-uvm"
|
|
CC_CAND_NAMES=("${C_nv_nodes[@]}")
|
|
CC_CAND_LABELS=()
|
|
for nl in "${C_nv_nodes[@]}"; do
|
|
lbl="$(cc_node_label "$nl")" || lbl="$nl" # raced-away node: fall back to the bare path
|
|
CC_CAND_LABELS+=("$lbl")
|
|
done
|
|
cc_add_loop C_gpu_devs "Nvidia devices" \
|
|
"Pick device to ADD" \
|
|
"Device path (must start with /dev/, empty = cancel)" || return 77
|
|
[ "${#C_gpu_devs[@]}" -eq 0 ] && C_gpu_mode="" # nothing picked ⇒ back to unconfigured
|
|
;;
|
|
none)
|
|
C_gpu_mode=""
|
|
C_gpu_devs=()
|
|
cc_log "GPU configuration cleared"
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
cc_category_devices() {
|
|
cc_collect_devices CC_CAND_NAMES CC_CAND_LABELS
|
|
if [ "${#CC_CAND_NAMES[@]}" -eq 0 ]; then
|
|
cc_info "no candidate devices found — type a path manually"
|
|
fi
|
|
cc_basket_menu C_devs "Host devices" \
|
|
"Remove which device?" "device(s)" cc_add_loop \
|
|
"Host devices" \
|
|
"Pick device to ADD" \
|
|
"Device path (must start with /dev/, empty = cancel)"
|
|
}
|
|
|
|
cc_category_mounts() {
|
|
cc_basket_menu C_mounts "Host dir mounts" \
|
|
"Remove which mount?" "mount(s)" cc_add_typed_loop \
|
|
"Host directory to mount (empty = done)" cc_validate_mount
|
|
}
|
|
|
|
cc_category_ports() {
|
|
cc_basket_menu C_ports "Ports" \
|
|
"Remove which port?" "port mapping(s)" cc_add_typed_loop \
|
|
"Publish port HOST:CONTAINER (empty = done)" cc_validate_port
|
|
}
|
|
|
|
cc_category_resources() {
|
|
local v
|
|
cc_info "press Enter to keep Docker defaults"
|
|
while true; do
|
|
v=""
|
|
[ -n "$C_cpus" ] && v=" (current: $C_cpus)"
|
|
if ! read -rp "--cpus$v — blank = Docker default: " v; then
|
|
return 77 # raw read rc 1 = definitive EOF
|
|
fi
|
|
if [ -z "$v" ]; then
|
|
C_cpus=""
|
|
break
|
|
fi
|
|
[[ "$v" =~ ^[0-9]+(\.[0-9]+)?$ ]] || {
|
|
cc_warn "CPUs must be a number (e.g. 2 or 1.5)"
|
|
continue
|
|
}
|
|
C_cpus="$v"
|
|
break
|
|
done
|
|
while true; do
|
|
v=""
|
|
[ -n "$C_mem" ] && v=" (current: $C_mem)"
|
|
if ! read -rp "--memory$v — blank = Docker default (e.g. 512m, 2g): " v; then
|
|
return 77 # raw read rc 1 = definitive EOF
|
|
fi
|
|
if [ -z "$v" ]; then
|
|
C_mem=""
|
|
break
|
|
fi
|
|
[[ "$v" =~ ^[0-9]+(b|k|m|g|mb|gb)?$ ]] || {
|
|
cc_warn "memory formats: 500b, 100k, 512m, 2g (mb/gb accepted)"
|
|
continue
|
|
}
|
|
C_mem="$v"
|
|
break
|
|
done
|
|
cc_log "resources: ${C_cpus:-Docker default} cpu(s), ${C_mem:-Docker default} memory"
|
|
return 0
|
|
}
|
|
|
|
# ── compose + review ──────────────────────────────────────────────────────
|
|
cc_compose() { # fills CC_ARGS (argv for menu_self) + CC_FLAGS_TXT (display)
|
|
# The UI never asks for the primary dir — it stays ~/<name> (the verb's
|
|
# default). Pass it as the FIRST --dir explicitly, so mount baskets map
|
|
# to the repeatable --dir form without displacing the working directory.
|
|
# It must exist before the verb resolves it (cd && pwd), so create it here.
|
|
mkdir -p "$HOME/$C_name"
|
|
CC_ARGS=(create "$C_name" "$C_image" --dir "$HOME/$C_name")
|
|
CC_FLAGS_TXT=""
|
|
local d p m
|
|
if [ "$C_gpu_mode" = "all" ]; then
|
|
CC_ARGS+=(--gpu)
|
|
CC_FLAGS_TXT="--gpus all"
|
|
fi
|
|
for d in "${C_gpu_devs[@]}"; do
|
|
CC_ARGS+=(--device "$d")
|
|
CC_FLAGS_TXT+=" --device $d"
|
|
done
|
|
for d in "${C_devs[@]}"; do
|
|
CC_ARGS+=(--device "$d")
|
|
CC_FLAGS_TXT+=" --device $d"
|
|
done
|
|
for p in "${C_ports[@]}"; do
|
|
CC_ARGS+=(--port "$p")
|
|
CC_FLAGS_TXT+=" --port $p"
|
|
done
|
|
[ -n "$C_cpus" ] && {
|
|
CC_ARGS+=(--cpus "$C_cpus")
|
|
CC_FLAGS_TXT+=" --cpus $C_cpus"
|
|
}
|
|
[ -n "$C_mem" ] && {
|
|
CC_ARGS+=(--memory "$C_mem")
|
|
CC_FLAGS_TXT+=" --memory $C_mem"
|
|
}
|
|
for m in "${C_mounts[@]}"; do
|
|
CC_ARGS+=(--dir "$m")
|
|
CC_FLAGS_TXT+=" --dir $m"
|
|
done
|
|
}
|
|
|
|
cc_render_group() { # $1 label · rest = rendered values · truncates at 6 rows
|
|
local label="$1"
|
|
shift
|
|
local max=6 shown=0 v n="$#"
|
|
if [ "$n" -eq 0 ]; then
|
|
printf ' %-10s %s\n' "$label" "none" >&2
|
|
return 0
|
|
fi
|
|
for v in "$@"; do
|
|
[ "$shown" -lt "$max" ] || break
|
|
if [ "$shown" -eq 0 ]; then
|
|
printf ' %-10s %s\n' "$label" "$v" >&2
|
|
else
|
|
printf ' %-10s %s\n' "" "$v" >&2
|
|
fi
|
|
shown=$((shown + 1))
|
|
done
|
|
if [ "$n" -gt "$max" ]; then
|
|
printf ' %-10s … (+%d more)\n' "" "$((n - max))" >&2
|
|
fi
|
|
}
|
|
|
|
cc_review() { # rc 0 = confirmed (CC_ARGS ready) · rc 1 = back to hub · rc 77 = discarded
|
|
local -a gpu_vals=() dev_vals=() mnt_vals=() port_vals=() rv=()
|
|
local d p m res
|
|
case "$C_gpu_mode" in
|
|
all) gpu_vals+=("--gpus all") ;;
|
|
nodes)
|
|
for d in "${C_gpu_devs[@]}"; do gpu_vals+=("--device $d"); done
|
|
;;
|
|
esac
|
|
for d in "${C_devs[@]}"; do dev_vals+=("--device $d"); done
|
|
for m in "${C_mounts[@]}"; do mnt_vals+=("$m:$m"); done
|
|
for p in "${C_ports[@]}"; do port_vals+=("-p $p"); done
|
|
|
|
{
|
|
echo
|
|
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
|
echo "${CYAN} Create VM '$C_name' — review plan${RESET}"
|
|
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
|
} >&2
|
|
cc_render_group "image" "$C_image"
|
|
cc_render_group "host dir" "~/$C_name (bind-mounted at same path, cwd inside VM)"
|
|
cc_render_group "gpus" ${gpu_vals[@]+"${gpu_vals[@]}"}
|
|
cc_render_group "devices" ${dev_vals[@]+"${dev_vals[@]}"}
|
|
cc_render_group "mounts" ${mnt_vals[@]+"${mnt_vals[@]}"}
|
|
cc_render_group "ports" ${port_vals[@]+"${port_vals[@]}"}
|
|
[ -n "$C_cpus" ] && rv+=("--cpus $C_cpus")
|
|
[ -n "$C_mem" ] && rv+=("--memory $C_mem")
|
|
if [ "${#rv[@]}" -gt 0 ]; then
|
|
res="$(
|
|
IFS=' '
|
|
echo "${rv[*]}"
|
|
)"
|
|
else
|
|
res="Docker defaults"
|
|
fi
|
|
cc_render_group "cpu/ram" "$res"
|
|
cc_render_group "network" "Docker default (bridge)"
|
|
|
|
cc_compose
|
|
{
|
|
echo "----------------------------------------" >&2
|
|
echo " docker create -it --name $C_name --label linux_post_install.vbox=true \\" >&2
|
|
echo " ${CC_FLAGS_TXT} $C_image bash" >&2
|
|
}
|
|
|
|
# A CUDA image is ever only a hint — never forced, never auto-applied.
|
|
if [ -n "$C_gpu_mode" ] && [[ "$C_image" != *cuda* && "$C_image" != nvidia/* ]]; then
|
|
cc_info "tip: for CUDA inside the VM try an image like nvidia/cuda:12.4-base-ubuntu22.04 (set it under Image & distro)"
|
|
fi
|
|
|
|
if confirm "Create?" n; then
|
|
cc_quit_reset
|
|
return 0
|
|
fi
|
|
cc_quit_tick && return 77 # second consecutive quit ⇒ teardown
|
|
return 1 # 'n' → back to the hub, edits preserved
|
|
}
|
|
|
|
cc_hub_items() { # rebuilds CC_ITEMS with live basket counts
|
|
local img gpu dev mnt prt res
|
|
if [ "$C_image_set" -eq 1 ]; then
|
|
img="$C_image"
|
|
else
|
|
img="$C_image (default)"
|
|
fi
|
|
case "$C_gpu_mode" in
|
|
all) gpu="--gpus all" ;;
|
|
nodes) gpu="${#C_gpu_devs[@]} node(s)" ;;
|
|
*) gpu="not configured" ;;
|
|
esac
|
|
if [ "${#C_devs[@]}" -gt 0 ]; then
|
|
dev="${#C_devs[@]} selected"
|
|
else
|
|
dev="none"
|
|
fi
|
|
if [ "${#C_mounts[@]}" -gt 0 ]; then
|
|
mnt="~/$C_name (auto) + ${#C_mounts[@]} more"
|
|
else
|
|
mnt="~/$C_name (auto)"
|
|
fi
|
|
if [ "${#C_ports[@]}" -gt 0 ]; then
|
|
prt="${#C_ports[@]} mapped"
|
|
else
|
|
prt="none"
|
|
fi
|
|
local -a rv=()
|
|
[ -n "$C_cpus" ] && rv+=("cpus=$C_cpus")
|
|
[ -n "$C_mem" ] && rv+=("mem=$C_mem")
|
|
if [ "${#rv[@]}" -gt 0 ]; then
|
|
res="$(
|
|
IFS=' '
|
|
echo "${rv[*]}"
|
|
)"
|
|
else
|
|
res="Docker defaults"
|
|
fi
|
|
CC_ITEMS=(
|
|
"Image & distro ....... $img"
|
|
"GPU / Nvidia ......... $gpu"
|
|
"Host devices ......... $dev"
|
|
"Host dir mounts ...... $mnt"
|
|
"Ports ................ $prt"
|
|
"CPU / RAM ............ $res"
|
|
"Review & create"
|
|
)
|
|
}
|
|
|
|
menu_vbox_create() {
|
|
local name choice
|
|
local crc=0 rrc=0
|
|
# §1: an EMPTY answer at the identity prompt backs out with nothing
|
|
# created. menu_ask_value has no default here, so an empty answer and a
|
|
# dead stream are indistinguishable (both rc 1) — both take the loud
|
|
# teardown (one discard line), matching §3's "EOF at ANY depth" rule.
|
|
if ! name="$(menu_ask_value "VM name")"; then
|
|
cc_die
|
|
return 0
|
|
fi
|
|
C_name="$name"
|
|
cc_reset
|
|
cc_quit_reset
|
|
while true; do
|
|
cc_hub_items
|
|
if ! choice="$(menu_run "Configure VM '$C_name' — capabilities" "${CC_ITEMS[@]}")"; then
|
|
cc_quit_tick && { cc_die; return 0; } # dead input / second quit
|
|
if confirm "Discard this VM setup?" n; then
|
|
return 0 # confirmed discard → main menu
|
|
fi
|
|
continue # single quit → guarded redraw
|
|
fi
|
|
cc_quit_reset
|
|
# Categories honour a strict rc contract: 0 = normal · 77 = EOF
|
|
# teardown somewhere below. Calls are ||-captured because a bare
|
|
# nonzero from a simple command would trip set -e before the check.
|
|
crc=0
|
|
case "$choice" in
|
|
1) cc_category_image || crc=$? ;;
|
|
2) cc_category_gpu || crc=$? ;;
|
|
3) cc_category_devices || crc=$? ;;
|
|
4) cc_category_mounts || crc=$? ;;
|
|
5) cc_category_ports || crc=$? ;;
|
|
6) cc_category_resources || crc=$? ;;
|
|
esac
|
|
[ "$crc" -eq 77 ] && { cc_die; return 0; } # EOF teardown somewhere below
|
|
if [ "$choice" -eq 7 ]; then
|
|
rrc=0
|
|
cc_review || rrc=$?
|
|
case "$rrc" in
|
|
77)
|
|
cc_die
|
|
return 0
|
|
;;
|
|
0)
|
|
menu_self "${CC_ARGS[@]}"
|
|
return 0
|
|
;;
|
|
*) continue ;; # 'n' → hub with edits preserved
|
|
esac
|
|
fi
|
|
done
|
|
}
|
|
|
|
menu_vbox_enter() {
|
|
local vm
|
|
vm="$(menu_pick_vm "Enter which VM?")" || return 0
|
|
log "Entering '$vm' — exit its shell to return to the menu"
|
|
menu_self enter "$vm"
|
|
}
|
|
|
|
menu_vbox_verb() { # $1 = start|stop — pick a VM, run the verb
|
|
local verb="$1" vm
|
|
vm="$(menu_pick_vm "${verb} which VM?")" || return 0
|
|
menu_self "$verb" "$vm"
|
|
}
|
|
|
|
menu_vbox_remove() {
|
|
local vm
|
|
vm="$(menu_pick_vm "Remove which VM?")" || return 0
|
|
confirm "Permanently remove VM '$vm' (docker rm -f; the host folder is kept)?" n \
|
|
|| { log "Cancelled — '$vm' kept"; return 0; }
|
|
menu_self rm "$vm"
|
|
}
|
|
|
|
run_menu() {
|
|
menu_guard || exit 1
|
|
while true; do
|
|
local choice
|
|
choice="$(menu_run "Docker VBox — disposable VMs" \
|
|
"List VMs" \
|
|
"Create a VM" \
|
|
"Enter a VM (interactive shell)" \
|
|
"Start a VM" \
|
|
"Stop a VM" \
|
|
"Remove a VM (permanent)")" || return 0
|
|
case "$choice" in
|
|
1) menu_self ls ;;
|
|
2) menu_vbox_create ;;
|
|
3) menu_vbox_enter ;;
|
|
4) menu_vbox_verb start ;;
|
|
5) menu_vbox_verb stop ;;
|
|
6) menu_vbox_remove ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Menu door: explicit verb, or zero args on a terminal. Everything below —
|
|
# including zero args without a terminal — stays byte-compatible with the
|
|
# pre-menu CLI.
|
|
if [ "${1:-}" = "menu" ]; then
|
|
run_menu
|
|
exit 0
|
|
fi
|
|
if [ $# -eq 0 ] && [ -t 0 ]; then
|
|
run_menu
|
|
exit 0
|
|
fi
|
|
|
|
case "${1:-}" in
|
|
-h|--help) usage ;;
|
|
esac
|
|
|
|
cmd="${1:-}"
|
|
[ -z "$cmd" ] && usage
|
|
|
|
container_exists() {
|
|
docker container inspect "$1" &>/dev/null
|
|
}
|
|
|
|
container_running() {
|
|
[[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" == "true" ]]
|
|
}
|
|
|
|
case "$cmd" in
|
|
create)
|
|
name="${2:-}"
|
|
[ -z "$name" ] && usage
|
|
|
|
# Parse remaining args: [image] [--dir <path>]… [--device </dev/node>]…
|
|
# [--gpu] [--port HOST:CONTAINER]… [--cpus N] [--memory SIZE] [--network MODE]
|
|
# The new flags are additive; an invocation that uses none of them
|
|
# behaves byte-identically to the pre-categorized verb.
|
|
image="ubuntu:22.04"
|
|
custom_dir=""
|
|
extra_dirs=()
|
|
want_gpu=0
|
|
devices=()
|
|
ports=()
|
|
cpus=""
|
|
memory=""
|
|
network=""
|
|
shift 2 || true
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dir)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --dir"; exit 1; }
|
|
if [ -z "$custom_dir" ]; then
|
|
custom_dir="$2"
|
|
else
|
|
extra_dirs+=("$2")
|
|
fi
|
|
shift 2
|
|
;;
|
|
--gpu)
|
|
want_gpu=1
|
|
shift
|
|
;;
|
|
--device)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --device"; exit 1; }
|
|
[[ "$2" == /dev/* ]] || { echo "[!] --device expects a path under /dev/: $2"; exit 1; }
|
|
devices+=("$2")
|
|
shift 2
|
|
;;
|
|
--port)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --port"; exit 1; }
|
|
[[ "$2" =~ ^[0-9]+(:[0-9]+){1,2}$ ]] || { echo "[!] --port expects HOST:CONTAINER (e.g. 8080:80): $2"; exit 1; }
|
|
ports+=("$2")
|
|
shift 2
|
|
;;
|
|
--cpus)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --cpus"; exit 1; }
|
|
[[ "$2" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { echo "[!] --cpus expects a number (e.g. 2 or 1.5): $2"; exit 1; }
|
|
cpus="$2"
|
|
shift 2
|
|
;;
|
|
--memory)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --memory"; exit 1; }
|
|
[[ "$2" =~ ^[0-9]+(b|k|m|g|mb|gb)?$ ]] || { echo "[!] --memory expects a size (e.g. 512m or 2g): $2"; exit 1; }
|
|
memory="$2"
|
|
shift 2
|
|
;;
|
|
--network)
|
|
[ -z "${2:-}" ] && { echo "Missing value for --network"; exit 1; }
|
|
network="$2"
|
|
shift 2
|
|
;;
|
|
-*)
|
|
echo "[!] Unknown option: $1 — see 'pos docker vbox --help'"
|
|
exit 1
|
|
;;
|
|
*)
|
|
image="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if container_exists "$name"; then
|
|
echo "[!] Container already exists: $name"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "$custom_dir" ]; then
|
|
lab_dir="$(cd "$custom_dir" 2>/dev/null && pwd)" || { echo "[!] Directory not found: $custom_dir"; exit 1; }
|
|
else
|
|
lab_dir="$HOME/$name"
|
|
fi
|
|
mkdir -p "$lab_dir"
|
|
echo "[+] Lab directory: $lab_dir"
|
|
|
|
# Additional bind mounts (repeated --dir): same-path convention like
|
|
# the primary lab dir.
|
|
mount_flags=()
|
|
for d in "${extra_dirs[@]}"; do
|
|
mnt_dir="$(cd "$d" 2>/dev/null && pwd)" || { echo "[!] Directory not found: $d"; exit 1; }
|
|
mkdir -p "$mnt_dir"
|
|
echo "[+] Bind mount: $mnt_dir:$mnt_dir"
|
|
mount_flags+=(-v "$mnt_dir:$mnt_dir")
|
|
done
|
|
|
|
# Optional flags in a fixed order: GPU → devices → ports → resources
|
|
# → network. Mounts are appended after the primary -v below.
|
|
create_flags=()
|
|
[ "$want_gpu" -eq 1 ] && create_flags+=(--gpus all)
|
|
for d in "${devices[@]}"; do create_flags+=(--device "$d"); done
|
|
for p in "${ports[@]}"; do create_flags+=(-p "$p"); done
|
|
[ -n "$cpus" ] && create_flags+=(--cpus "$cpus")
|
|
[ -n "$memory" ] && create_flags+=(--memory "$memory")
|
|
[ -n "$network" ] && create_flags+=(--network "$network")
|
|
|
|
echo "[+] Pulling image: $image"
|
|
docker pull "$image"
|
|
|
|
echo "[+] Creating: $name"
|
|
docker create \
|
|
-it \
|
|
--name "$name" \
|
|
--label linux_post_install.vbox=true \
|
|
${create_flags[@]+"${create_flags[@]}"} \
|
|
-v "$lab_dir:$lab_dir" \
|
|
${mount_flags[@]+"${mount_flags[@]}"} \
|
|
-w "$lab_dir" \
|
|
"$image" \
|
|
bash >/dev/null
|
|
echo "[+] Done"
|
|
|
|
if confirm "Enter now?"; then
|
|
docker start "$name" >/dev/null
|
|
exec docker exec -it -w "$lab_dir" "$name" bash
|
|
fi
|
|
;;
|
|
|
|
enter)
|
|
name="${2:-}"
|
|
[ -z "$name" ] && usage
|
|
|
|
if ! container_exists "$name"; then
|
|
echo "[!] Container not found: $name"
|
|
exit 1
|
|
fi
|
|
|
|
if ! container_running "$name"; then
|
|
docker start "$name" >/dev/null
|
|
fi
|
|
|
|
# Detect working dir from container mounts
|
|
lab_dir=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination .Destination}}{{.Source}}{{end}}{{end}}' "$name" 2>/dev/null | head -1)
|
|
if [ -n "$lab_dir" ] && [ -d "$lab_dir" ]; then
|
|
exec docker exec -it -w "$lab_dir" "$name" bash
|
|
else
|
|
exec docker exec -it "$name" bash
|
|
fi
|
|
;;
|
|
|
|
start)
|
|
name="${2:-}"
|
|
[ -z "$name" ] && usage
|
|
docker start "$name"
|
|
;;
|
|
|
|
stop)
|
|
name="${2:-}"
|
|
[ -z "$name" ] && usage
|
|
docker stop "$name"
|
|
;;
|
|
|
|
rm)
|
|
name="${2:-}"
|
|
[ -z "$name" ] && usage
|
|
docker rm -f "$name"
|
|
;;
|
|
|
|
ls)
|
|
docker ps -a --filter label=linux_post_install.vbox=true --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
|
;;
|
|
|
|
*)
|
|
usage
|
|
;;
|
|
esac
|