@rxtx4816/cockpit-plugin-base-react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.base.js +85 -0
- package/package.json +116 -0
- package/scripts/test-vm.sh +608 -0
- package/src/bootstrap.tsx +7 -0
- package/src/cockpit.d.ts +63 -0
- package/src/components/ConfirmDialog.test.tsx +61 -0
- package/src/components/ConfirmDialog.tsx +65 -0
- package/src/components/ErrorBoundary.test.tsx +50 -0
- package/src/components/ErrorBoundary.tsx +30 -0
- package/src/components/HelpPopover.tsx +30 -0
- package/src/components/LogViewer.tsx +108 -0
- package/src/components/StatusBadge.test.tsx +32 -0
- package/src/components/StatusBadge.tsx +18 -0
- package/src/components/ToastProvider.css +20 -0
- package/src/components/ToastProvider.test.tsx +61 -0
- package/src/components/ToastProvider.tsx +76 -0
- package/src/components/index.ts +8 -0
- package/src/css.d.ts +4 -0
- package/src/dark-theme.ts +30 -0
- package/src/hooks/useAsyncAction.test.ts +59 -0
- package/src/hooks/useAsyncAction.ts +31 -0
- package/src/hooks/useAsyncStream.test.ts +122 -0
- package/src/hooks/useAsyncStream.ts +94 -0
- package/src/hooks/useAutoRefresh.test.ts +106 -0
- package/src/hooks/useAutoRefresh.ts +23 -0
- package/src/hooks/useConfirmAction.test.ts +68 -0
- package/src/hooks/useConfirmAction.ts +35 -0
- package/src/hooks/usePollingFetch.ts +41 -0
- package/src/i18n.ts +51 -0
- package/src/index.ts +11 -0
- package/src/systemd/ServiceControl.tsx +172 -0
- package/src/systemd/api.test.ts +83 -0
- package/src/systemd/api.ts +36 -0
- package/src/systemd/index.ts +5 -0
- package/src/systemd/types.ts +1 -0
- package/src/systemd/useServiceStatus.test.ts +96 -0
- package/src/systemd/useServiceStatus.ts +29 -0
- package/src/testing/helpers.ts +30 -0
- package/src/testing/setup.ts +57 -0
- package/tsconfig.base.json +17 -0
- package/vitest.config.base.ts +47 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/test-vm.sh — Generic QEMU VM test harness for Cockpit plugins
|
|
3
|
+
#
|
|
4
|
+
# Spins up cloud VMs (Arch, Debian, Fedora by default) for end-to-end testing.
|
|
5
|
+
#
|
|
6
|
+
# Usage from your plugin directory:
|
|
7
|
+
# ./node_modules/@rxtx4816/cockpit-plugin-base/scripts/test-vm.sh <command> [vm ...]
|
|
8
|
+
#
|
|
9
|
+
# Or add to your package.json scripts:
|
|
10
|
+
# "vm": "node_modules/@rxtx4816/cockpit-plugin-base/scripts/test-vm.sh"
|
|
11
|
+
#
|
|
12
|
+
# Each plugin provides a scripts/test-vm.config.sh that customises:
|
|
13
|
+
# - PLUGIN_NAME, MOUNT_TAG, INSTALL_PATH
|
|
14
|
+
# - ALL_VMS, SSH_BASE, COCKPIT_BASE
|
|
15
|
+
# - extra_packages(distro) — echo packages to install
|
|
16
|
+
# - extra_runcmd(vm) — echo cloud-init runcmd lines
|
|
17
|
+
# - pre_staged_files(vm) — echo cloud-init write_files blocks
|
|
18
|
+
#
|
|
19
|
+
# Dependencies (Arch): qemu-full cloud-image-utils wget
|
|
20
|
+
# sudo pacman -S qemu-full cloud-image-utils wget
|
|
21
|
+
#
|
|
22
|
+
# Quick start:
|
|
23
|
+
# npm run build
|
|
24
|
+
# npm run vm download debian
|
|
25
|
+
# npm run vm start debian
|
|
26
|
+
# npm run vm wait debian
|
|
27
|
+
# # Open https://localhost:$COCKPIT_BASE — login: test / test
|
|
28
|
+
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
PROJECT_DIR="$(pwd)"
|
|
33
|
+
VM_DIR="$PROJECT_DIR/.vms"
|
|
34
|
+
DIST_DIR="$PROJECT_DIR/src"
|
|
35
|
+
|
|
36
|
+
# ── Defaults (overridden by test-vm.config.sh) ────────────────────────────────
|
|
37
|
+
|
|
38
|
+
PLUGIN_NAME="cockpit-plugin"
|
|
39
|
+
MOUNT_TAG="cockpit_plugin"
|
|
40
|
+
INSTALL_PATH="/usr/share/cockpit/cockpit-plugin"
|
|
41
|
+
|
|
42
|
+
ALL_VMS=(arch debian fedora)
|
|
43
|
+
SSH_BASE=2220
|
|
44
|
+
COCKPIT_BASE=9090
|
|
45
|
+
|
|
46
|
+
VM_MEM="${VM_MEM:-1024}"
|
|
47
|
+
VM_CPUS="${VM_CPUS:-2}"
|
|
48
|
+
VM_DISK_SIZE="${VM_DISK_SIZE:-12G}"
|
|
49
|
+
|
|
50
|
+
# Default no-op hooks — plugins override these
|
|
51
|
+
extra_packages() { :; }
|
|
52
|
+
extra_runcmd() { :; }
|
|
53
|
+
pre_staged_files() { :; }
|
|
54
|
+
|
|
55
|
+
# ── Load plugin config ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
CONFIG_FILE="$PROJECT_DIR/scripts/test-vm.config.sh"
|
|
58
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
59
|
+
# shellcheck source=/dev/null
|
|
60
|
+
source "$CONFIG_FILE"
|
|
61
|
+
else
|
|
62
|
+
echo "WARNING: $CONFIG_FILE not found — using defaults (PLUGIN_NAME=$PLUGIN_NAME)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ── Cloud image URLs ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
ARCH_IMAGE_URL="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
|
|
68
|
+
DEBIAN_IMAGE_URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2"
|
|
69
|
+
FEDORA_VERSION="41"
|
|
70
|
+
FEDORA_BUILD="1.4"
|
|
71
|
+
FEDORA_IMAGE_URL="https://download.fedoraproject.org/pub/fedora/linux/releases/${FEDORA_VERSION}/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-${FEDORA_VERSION}-${FEDORA_BUILD}.x86_64.qcow2"
|
|
72
|
+
|
|
73
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
76
|
+
info() { echo "==> $*"; }
|
|
77
|
+
ok() { echo " ✓ $*"; }
|
|
78
|
+
|
|
79
|
+
usage() {
|
|
80
|
+
cat <<EOF
|
|
81
|
+
Usage: $(basename "$0") <command> [vm ...]
|
|
82
|
+
|
|
83
|
+
VM identifiers: ${ALL_VMS[*]}
|
|
84
|
+
|
|
85
|
+
Commands:
|
|
86
|
+
download [vm|all] Download base cloud images
|
|
87
|
+
build Run npm run build
|
|
88
|
+
start [vm ...] Start VM(s) in background
|
|
89
|
+
wait <vm> Block until cloud-init finishes (~2-5 min first boot)
|
|
90
|
+
stop [vm ...] Stop VM(s)
|
|
91
|
+
status Show all VMs with ports and running state
|
|
92
|
+
ssh <vm> Open SSH session
|
|
93
|
+
logs <vm> Tail VM serial console
|
|
94
|
+
clean [vm ...] Wipe disk + state (base image kept)
|
|
95
|
+
rebuild [vm ...] clean + start in one step
|
|
96
|
+
reset [vm ...] Remove all VM files including base image
|
|
97
|
+
|
|
98
|
+
Ports (Cockpit / SSH):
|
|
99
|
+
$(for i in "${!ALL_VMS[@]}"; do
|
|
100
|
+
vm="${ALL_VMS[$i]}"
|
|
101
|
+
printf " %-18s → https://localhost:%d ssh -p %d test@localhost\n" \
|
|
102
|
+
"$vm" "$((COCKPIT_BASE + i))" "$((SSH_BASE + i))"
|
|
103
|
+
done)
|
|
104
|
+
|
|
105
|
+
Login: test / test (your ~/.ssh/id_*.pub is also injected if found)
|
|
106
|
+
|
|
107
|
+
Environment overrides:
|
|
108
|
+
VM_MEM=2048 VM_CPUS=2 VM_DISK_SIZE=12G
|
|
109
|
+
CONFIG: $CONFIG_FILE
|
|
110
|
+
EOF
|
|
111
|
+
exit 1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
check_deps() {
|
|
115
|
+
local missing=()
|
|
116
|
+
for cmd in qemu-system-x86_64 qemu-img wget; do
|
|
117
|
+
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
|
118
|
+
done
|
|
119
|
+
if ! command -v cloud-localds &>/dev/null \
|
|
120
|
+
&& ! command -v genisoimage &>/dev/null \
|
|
121
|
+
&& ! command -v mkisofs &>/dev/null; then
|
|
122
|
+
missing+=("cloud-localds (cloud-image-utils) OR genisoimage")
|
|
123
|
+
fi
|
|
124
|
+
[[ ${#missing[@]} -eq 0 ]] || {
|
|
125
|
+
echo "Missing dependencies:"
|
|
126
|
+
printf ' %s\n' "${missing[@]}"
|
|
127
|
+
echo ""
|
|
128
|
+
echo "Install with: sudo pacman -S qemu-full cloud-image-utils wget"
|
|
129
|
+
exit 1
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
vm_index() {
|
|
134
|
+
local vm="$1"
|
|
135
|
+
for i in "${!ALL_VMS[@]}"; do
|
|
136
|
+
[[ "${ALL_VMS[$i]}" == "$vm" ]] && { echo "$i"; return; }
|
|
137
|
+
done
|
|
138
|
+
die "Unknown VM '$vm'. Valid: ${ALL_VMS[*]}"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ssh_port() { echo $((SSH_BASE + $(vm_index "$1"))); }
|
|
142
|
+
cockpit_port() { echo $((COCKPIT_BASE + $(vm_index "$1"))); }
|
|
143
|
+
|
|
144
|
+
vm_distro() {
|
|
145
|
+
local vm="$1"
|
|
146
|
+
# Distro is the first component before any '-' (arch, debian, fedora)
|
|
147
|
+
echo "${vm%%-*}"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pid_file() { echo "$VM_DIR/$1/qemu.pid"; }
|
|
151
|
+
disk_img() { echo "$VM_DIR/$1/disk.qcow2"; }
|
|
152
|
+
base_img() { local d; d="$(vm_distro "$1")"; echo "$VM_DIR/$d/base.qcow2"; }
|
|
153
|
+
seed_iso() { echo "$VM_DIR/$1/seed.iso"; }
|
|
154
|
+
console_log() { echo "$VM_DIR/$1/console.log"; }
|
|
155
|
+
|
|
156
|
+
is_running() {
|
|
157
|
+
local pf; pf="$(pid_file "$1")"
|
|
158
|
+
[[ -f "$pf" ]] && kill -0 "$(cat "$pf")" 2>/dev/null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resolve_vms() {
|
|
162
|
+
[[ $# -eq 0 ]] && { echo "${ALL_VMS[@]}"; return; }
|
|
163
|
+
local result=() seen=() out=()
|
|
164
|
+
for arg in "$@"; do
|
|
165
|
+
if [[ "$arg" == "all" ]]; then
|
|
166
|
+
result+=("${ALL_VMS[@]}")
|
|
167
|
+
else
|
|
168
|
+
# Accept exact VM names or distro names (which expand to all VMs for that distro)
|
|
169
|
+
local matched=0
|
|
170
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
171
|
+
if [[ "$vm" == "$arg" || "$(vm_distro "$vm")" == "$arg" ]]; then
|
|
172
|
+
result+=("$vm"); matched=1
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
[[ $matched -eq 0 ]] && die "Unknown VM or shortcut: '$arg'. Valid: ${ALL_VMS[*]} all"
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
for v in "${result[@]}"; do
|
|
179
|
+
[[ " ${seen[*]} " == *" $v "* ]] && continue
|
|
180
|
+
seen+=("$v"); out+=("$v")
|
|
181
|
+
done
|
|
182
|
+
echo "${out[@]}"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resolve_distros() {
|
|
186
|
+
[[ $# -eq 0 ]] && {
|
|
187
|
+
local seen=() out=()
|
|
188
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
189
|
+
local d; d="$(vm_distro "$vm")"
|
|
190
|
+
[[ " ${seen[*]} " == *" $d "* ]] && continue
|
|
191
|
+
seen+=("$d"); out+=("$d")
|
|
192
|
+
done
|
|
193
|
+
echo "${out[@]}"
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
local result=() seen=() out=()
|
|
197
|
+
for arg in "$@"; do
|
|
198
|
+
case "$arg" in
|
|
199
|
+
all) for vm in "${ALL_VMS[@]}"; do result+=("$(vm_distro "$vm")"); done ;;
|
|
200
|
+
*) result+=("$(vm_distro "$arg")") ;;
|
|
201
|
+
esac
|
|
202
|
+
done
|
|
203
|
+
for v in "${result[@]}"; do
|
|
204
|
+
[[ " ${seen[*]} " == *" $v "* ]] && continue
|
|
205
|
+
seen+=("$v"); out+=("$v")
|
|
206
|
+
done
|
|
207
|
+
echo "${out[@]}"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
find_ssh_pubkey() {
|
|
211
|
+
for key in ~/.ssh/id_ed25519.pub ~/.ssh/id_rsa.pub ~/.ssh/id_ecdsa.pub; do
|
|
212
|
+
[[ -f "$key" ]] && { cat "$key"; return; }
|
|
213
|
+
done
|
|
214
|
+
echo ""
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
make_seed_iso() {
|
|
218
|
+
local iso="$1" userdata="$2" metadata="$3"
|
|
219
|
+
if command -v cloud-localds &>/dev/null; then
|
|
220
|
+
cloud-localds "$iso" "$userdata" "$metadata"
|
|
221
|
+
elif command -v genisoimage &>/dev/null; then
|
|
222
|
+
genisoimage -output "$iso" -volid cidata -joliet -rock "$userdata" "$metadata" 2>/dev/null
|
|
223
|
+
else
|
|
224
|
+
mkisofs -output "$iso" -volid cidata -joliet -rock "$userdata" "$metadata" 2>/dev/null
|
|
225
|
+
fi
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
qemu_accel_args() {
|
|
229
|
+
if [[ -r /dev/kvm ]]; then
|
|
230
|
+
echo "-machine type=q35,accel=kvm -cpu host"
|
|
231
|
+
else
|
|
232
|
+
info "WARNING: /dev/kvm not accessible — running without KVM (will be slow)"
|
|
233
|
+
echo "-machine type=q35"
|
|
234
|
+
fi
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ── cloud-init user-data ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
generate_userdata() {
|
|
240
|
+
local vm="$1" ssh_pubkey="$2" outfile="$3"
|
|
241
|
+
local distro; distro="$(vm_distro "$vm")"
|
|
242
|
+
|
|
243
|
+
local group
|
|
244
|
+
case "$distro" in
|
|
245
|
+
arch|fedora) group="wheel" ;;
|
|
246
|
+
debian) group="sudo" ;;
|
|
247
|
+
*) group="sudo" ;;
|
|
248
|
+
esac
|
|
249
|
+
|
|
250
|
+
local ssh_keys_block=" ssh_authorized_keys: []"
|
|
251
|
+
[[ -n "$ssh_pubkey" ]] && ssh_keys_block=" ssh_authorized_keys:
|
|
252
|
+
- ${ssh_pubkey}"
|
|
253
|
+
|
|
254
|
+
# ── header ──────────────────────────────────────────────────────────────────
|
|
255
|
+
cat > "$outfile" <<YAML
|
|
256
|
+
#cloud-config
|
|
257
|
+
hostname: ${vm}-test
|
|
258
|
+
|
|
259
|
+
users:
|
|
260
|
+
- name: test
|
|
261
|
+
groups: ${group}
|
|
262
|
+
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
263
|
+
lock_passwd: false
|
|
264
|
+
${ssh_keys_block}
|
|
265
|
+
|
|
266
|
+
chpasswd:
|
|
267
|
+
list: |
|
|
268
|
+
test:test
|
|
269
|
+
expire: false
|
|
270
|
+
|
|
271
|
+
package_update: true
|
|
272
|
+
package_upgrade: false
|
|
273
|
+
packages:
|
|
274
|
+
- cockpit
|
|
275
|
+
YAML
|
|
276
|
+
|
|
277
|
+
# Plugin-specific packages ($vm passed as 2nd arg so scenario-aware plugins can use it)
|
|
278
|
+
local pkg
|
|
279
|
+
pkg="$(extra_packages "$distro" "$vm")"
|
|
280
|
+
if [[ -n "$pkg" ]]; then
|
|
281
|
+
while IFS= read -r line; do
|
|
282
|
+
[[ -n "$line" ]] && printf ' - %s\n' "$line" >> "$outfile"
|
|
283
|
+
done <<< "$pkg"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# ── write_files ──────────────────────────────────────────────────────────────
|
|
287
|
+
cat >> "$outfile" <<YAML
|
|
288
|
+
|
|
289
|
+
write_files:
|
|
290
|
+
- path: /etc/modules-load.d/9p.conf
|
|
291
|
+
content: |
|
|
292
|
+
9p
|
|
293
|
+
9pnet
|
|
294
|
+
9pnet_virtio
|
|
295
|
+
YAML
|
|
296
|
+
|
|
297
|
+
# Plugin-specific pre-staged files
|
|
298
|
+
pre_staged_files "$vm" >> "$outfile" || true
|
|
299
|
+
|
|
300
|
+
# ── runcmd ───────────────────────────────────────────────────────────────────
|
|
301
|
+
cat >> "$outfile" <<YAML
|
|
302
|
+
|
|
303
|
+
runcmd:
|
|
304
|
+
- modprobe 9p 9pnet 9pnet_virtio || true
|
|
305
|
+
- mkdir -p ${INSTALL_PATH}
|
|
306
|
+
- echo "${MOUNT_TAG} ${INSTALL_PATH} 9p trans=virtio,version=9p2000.L,ro,_netdev 0 0" >> /etc/fstab
|
|
307
|
+
- mount ${INSTALL_PATH} || true
|
|
308
|
+
- systemctl enable --now cockpit.socket
|
|
309
|
+
YAML
|
|
310
|
+
|
|
311
|
+
# Plugin-specific runcmd
|
|
312
|
+
extra_runcmd "$vm" >> "$outfile" || true
|
|
313
|
+
|
|
314
|
+
# ── footer ───────────────────────────────────────────────────────────────────
|
|
315
|
+
cat >> "$outfile" <<YAML
|
|
316
|
+
|
|
317
|
+
final_message: |
|
|
318
|
+
${vm} VM ready.
|
|
319
|
+
Cockpit : https://localhost:$(cockpit_port "$vm")
|
|
320
|
+
SSH : ssh -p $(ssh_port "$vm") -o StrictHostKeyChecking=no test@localhost
|
|
321
|
+
Login : test / test
|
|
322
|
+
YAML
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
cmd_download() {
|
|
328
|
+
local distros
|
|
329
|
+
read -ra distros <<< "$(resolve_distros "$@")"
|
|
330
|
+
|
|
331
|
+
for distro in "${distros[@]}"; do
|
|
332
|
+
local url img
|
|
333
|
+
case "$distro" in
|
|
334
|
+
arch) url="$ARCH_IMAGE_URL" ;;
|
|
335
|
+
debian) url="$DEBIAN_IMAGE_URL" ;;
|
|
336
|
+
fedora) url="$FEDORA_IMAGE_URL" ;;
|
|
337
|
+
*) die "No image URL configured for distro '$distro'" ;;
|
|
338
|
+
esac
|
|
339
|
+
img="$VM_DIR/$distro/base.qcow2"
|
|
340
|
+
mkdir -p "$VM_DIR/$distro"
|
|
341
|
+
|
|
342
|
+
if [[ -f "$img" ]]; then
|
|
343
|
+
info "$distro: base image already exists ($(du -sh "$img" | cut -f1)) — skipping"
|
|
344
|
+
continue
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
info "$distro: downloading from $url"
|
|
348
|
+
wget --progress=bar:force -O "${img}.tmp" "$url"
|
|
349
|
+
mv "${img}.tmp" "$img"
|
|
350
|
+
ok "$distro: saved to $img"
|
|
351
|
+
done
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
cmd_build() {
|
|
355
|
+
info "Building $PLUGIN_NAME plugin..."
|
|
356
|
+
cd "$PROJECT_DIR"
|
|
357
|
+
npm run build
|
|
358
|
+
ok "Build complete → $DIST_DIR/main.js"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
cmd_start() {
|
|
362
|
+
local vms
|
|
363
|
+
read -ra vms <<< "$(resolve_vms "$@")"
|
|
364
|
+
|
|
365
|
+
for vm in "${vms[@]}"; do
|
|
366
|
+
local distro sp cp vm_path bimg dimg siso udata mdata
|
|
367
|
+
distro="$(vm_distro "$vm")"
|
|
368
|
+
sp="$(ssh_port "$vm")"
|
|
369
|
+
cp="$(cockpit_port "$vm")"
|
|
370
|
+
vm_path="$VM_DIR/$vm"
|
|
371
|
+
bimg="$(base_img "$vm")"
|
|
372
|
+
dimg="$(disk_img "$vm")"
|
|
373
|
+
siso="$(seed_iso "$vm")"
|
|
374
|
+
udata="$vm_path/user-data"
|
|
375
|
+
mdata="$vm_path/meta-data"
|
|
376
|
+
|
|
377
|
+
[[ -f "$bimg" ]] || die "$vm: base image missing — run: $0 download $distro"
|
|
378
|
+
[[ -f "$DIST_DIR/main.js" ]] || die "src/main.js not found — run: $0 build (or npm run build)"
|
|
379
|
+
|
|
380
|
+
if is_running "$vm"; then
|
|
381
|
+
info "$vm: already running (PID $(cat "$(pid_file "$vm")"))"
|
|
382
|
+
continue
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
mkdir -p "$vm_path"
|
|
386
|
+
|
|
387
|
+
if [[ ! -f "$dimg" || "$bimg" -nt "$dimg" ]]; then
|
|
388
|
+
info "$vm: creating overlay disk (${VM_DISK_SIZE}) from $distro base..."
|
|
389
|
+
qemu-img create -f qcow2 -b "$bimg" -F qcow2 "$dimg"
|
|
390
|
+
qemu-img resize "$dimg" "$VM_DISK_SIZE"
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
if [[ ! -f "$siso" ]]; then
|
|
394
|
+
local ssh_pubkey
|
|
395
|
+
ssh_pubkey="$(find_ssh_pubkey)"
|
|
396
|
+
info "$vm: generating cloud-init seed..."
|
|
397
|
+
[[ -n "$ssh_pubkey" ]] && ok "Found SSH public key — injecting into VM"
|
|
398
|
+
generate_userdata "$vm" "$ssh_pubkey" "$udata"
|
|
399
|
+
printf 'instance-id: %s-01\nlocal-hostname: %s-test\n' "$vm" "$vm" > "$mdata"
|
|
400
|
+
make_seed_iso "$siso" "$udata" "$mdata"
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
info "$vm: starting VM (mem=${VM_MEM}M cpus=${VM_CPUS})..."
|
|
404
|
+
|
|
405
|
+
local accel_str; accel_str="$(qemu_accel_args)"
|
|
406
|
+
# shellcheck disable=SC2206
|
|
407
|
+
local accel_args=($accel_str)
|
|
408
|
+
|
|
409
|
+
qemu-system-x86_64 \
|
|
410
|
+
-name "${PLUGIN_NAME}-${vm}" \
|
|
411
|
+
"${accel_args[@]}" \
|
|
412
|
+
-smp "$VM_CPUS" \
|
|
413
|
+
-m "$VM_MEM" \
|
|
414
|
+
-drive "file=${dimg},format=qcow2,if=virtio,cache=writeback" \
|
|
415
|
+
-drive "file=${siso},format=raw,if=virtio,readonly=on" \
|
|
416
|
+
-virtfs "local,path=${DIST_DIR},mount_tag=${MOUNT_TAG},security_model=none,readonly=on" \
|
|
417
|
+
-netdev "user,id=net0,hostfwd=tcp:127.0.0.1:${sp}-:22,hostfwd=tcp:127.0.0.1:${cp}-:9090" \
|
|
418
|
+
-device virtio-net-pci,netdev=net0 \
|
|
419
|
+
-display none \
|
|
420
|
+
-serial "file:$(console_log "$vm")" \
|
|
421
|
+
-pidfile "$(pid_file "$vm")" \
|
|
422
|
+
-daemonize
|
|
423
|
+
|
|
424
|
+
ok "$vm: started (PID $(cat "$(pid_file "$vm")"))"
|
|
425
|
+
echo ""
|
|
426
|
+
echo " Cockpit → https://localhost:${cp} (accept self-signed cert)"
|
|
427
|
+
echo " SSH → ssh -p ${sp} -o StrictHostKeyChecking=no test@localhost"
|
|
428
|
+
echo " Ready? → $0 wait $vm"
|
|
429
|
+
echo " Logs → $0 logs $vm"
|
|
430
|
+
echo ""
|
|
431
|
+
done
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
cmd_wait() {
|
|
435
|
+
local vm="${1:-}"
|
|
436
|
+
[[ -n "$vm" ]] || die "Usage: $0 wait <vm>"
|
|
437
|
+
vm_index "$vm" > /dev/null
|
|
438
|
+
local sp; sp="$(ssh_port "$vm")"
|
|
439
|
+
|
|
440
|
+
is_running "$vm" || die "$vm is not running — start it first: $0 start $vm"
|
|
441
|
+
|
|
442
|
+
info "$vm: waiting for SSH on port $sp..."
|
|
443
|
+
local elapsed=0 timeout=300
|
|
444
|
+
while ! ssh -p "$sp" \
|
|
445
|
+
-o StrictHostKeyChecking=no \
|
|
446
|
+
-o UserKnownHostsFile=/dev/null \
|
|
447
|
+
-o ConnectTimeout=2 \
|
|
448
|
+
-o BatchMode=yes \
|
|
449
|
+
test@localhost true 2>/dev/null; do
|
|
450
|
+
sleep 5; elapsed=$((elapsed + 5))
|
|
451
|
+
[[ $elapsed -ge $timeout ]] && die "Timed out after ${timeout}s waiting for SSH"
|
|
452
|
+
printf "."
|
|
453
|
+
done
|
|
454
|
+
echo ""
|
|
455
|
+
info "$vm: SSH ready — waiting for cloud-init to complete..."
|
|
456
|
+
local ci_out
|
|
457
|
+
ci_out=$(ssh -p "$sp" \
|
|
458
|
+
-o StrictHostKeyChecking=no \
|
|
459
|
+
-o UserKnownHostsFile=/dev/null \
|
|
460
|
+
-o ConnectTimeout=5 \
|
|
461
|
+
-o BatchMode=yes \
|
|
462
|
+
test@localhost 'sudo cloud-init status --wait' 2>/dev/null || true)
|
|
463
|
+
if ! echo "$ci_out" | grep -q "status: done"; then
|
|
464
|
+
echo "WARNING: cloud-init did not reach 'done' (got: $ci_out)"
|
|
465
|
+
echo " Check: $0 logs $vm"
|
|
466
|
+
return 1
|
|
467
|
+
fi
|
|
468
|
+
echo ""
|
|
469
|
+
ok "$vm: VM is ready!"
|
|
470
|
+
echo ""
|
|
471
|
+
echo " Open → https://localhost:$(cockpit_port "$vm")"
|
|
472
|
+
echo " Login → test / test"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
cmd_stop() {
|
|
476
|
+
local vms
|
|
477
|
+
read -ra vms <<< "$(resolve_vms "$@")"
|
|
478
|
+
|
|
479
|
+
for vm in "${vms[@]}"; do
|
|
480
|
+
local pf; pf="$(pid_file "$vm")"
|
|
481
|
+
if is_running "$vm"; then
|
|
482
|
+
info "$vm: stopping (PID $(cat "$pf"))..."
|
|
483
|
+
kill "$(cat "$pf")"
|
|
484
|
+
local i=0
|
|
485
|
+
while kill -0 "$(cat "$pf")" 2>/dev/null && [[ $i -lt 20 ]]; do
|
|
486
|
+
sleep 0.5; i=$((i+1))
|
|
487
|
+
done
|
|
488
|
+
rm -f "$pf"
|
|
489
|
+
ok "$vm: stopped"
|
|
490
|
+
else
|
|
491
|
+
info "$vm: not running"
|
|
492
|
+
fi
|
|
493
|
+
done
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
cmd_status() {
|
|
497
|
+
local distros=()
|
|
498
|
+
local seen=()
|
|
499
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
500
|
+
local d; d="$(vm_distro "$vm")"
|
|
501
|
+
[[ " ${seen[*]} " == *" $d "* ]] && continue
|
|
502
|
+
seen+=("$d"); distros+=("$d")
|
|
503
|
+
done
|
|
504
|
+
|
|
505
|
+
echo ""
|
|
506
|
+
echo "Base images:"
|
|
507
|
+
for d in "${distros[@]}"; do
|
|
508
|
+
local img="$VM_DIR/$d/base.qcow2"
|
|
509
|
+
if [[ -f "$img" ]]; then
|
|
510
|
+
printf " %-8s ✓ %s\n" "$d" "$(du -sh "$img" | cut -f1)"
|
|
511
|
+
else
|
|
512
|
+
printf " %-8s ✗ not downloaded\n" "$d"
|
|
513
|
+
fi
|
|
514
|
+
done
|
|
515
|
+
echo ""
|
|
516
|
+
printf " %-18s %-8s %-8s %s\n" "VM" "STATE" "COCKPIT" "SSH"
|
|
517
|
+
printf " %-18s %-8s %-8s %s\n" "--" "-----" "-------" "---"
|
|
518
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
519
|
+
local state cp sp
|
|
520
|
+
cp="$(cockpit_port "$vm")"
|
|
521
|
+
sp="$(ssh_port "$vm")"
|
|
522
|
+
if is_running "$vm"; then
|
|
523
|
+
state="running"
|
|
524
|
+
elif [[ -f "$(disk_img "$vm")" ]]; then
|
|
525
|
+
state="stopped"
|
|
526
|
+
else
|
|
527
|
+
state="not created"
|
|
528
|
+
fi
|
|
529
|
+
printf " %-18s %-8s :%-7s :%s\n" "$vm" "$state" "$cp" "$sp"
|
|
530
|
+
done
|
|
531
|
+
echo ""
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
cmd_ssh() {
|
|
535
|
+
local vm="${1:-}"
|
|
536
|
+
[[ -n "$vm" ]] || die "Usage: $0 ssh <vm>"
|
|
537
|
+
vm_index "$vm" > /dev/null
|
|
538
|
+
is_running "$vm" || die "$vm is not running — start it: $0 start $vm"
|
|
539
|
+
exec ssh \
|
|
540
|
+
-p "$(ssh_port "$vm")" \
|
|
541
|
+
-o StrictHostKeyChecking=no \
|
|
542
|
+
-o UserKnownHostsFile=/dev/null \
|
|
543
|
+
test@localhost
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
cmd_logs() {
|
|
547
|
+
local vm="${1:-}"
|
|
548
|
+
[[ -n "$vm" ]] || die "Usage: $0 logs <vm>"
|
|
549
|
+
vm_index "$vm" > /dev/null
|
|
550
|
+
local log; log="$(console_log "$vm")"
|
|
551
|
+
[[ -f "$log" ]] || die "No console log yet for $vm (start it first)"
|
|
552
|
+
exec tail -f "$log"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
cmd_clean() {
|
|
556
|
+
local vms
|
|
557
|
+
read -ra vms <<< "$(resolve_vms "$@")"
|
|
558
|
+
for vm in "${vms[@]}"; do
|
|
559
|
+
is_running "$vm" && { info "$vm: stopping first"; cmd_stop "$vm"; }
|
|
560
|
+
info "$vm: removing disk and cloud-init state (base image kept)..."
|
|
561
|
+
rm -f "$(disk_img "$vm")" "$(seed_iso "$vm")" \
|
|
562
|
+
"$VM_DIR/$vm/user-data" "$VM_DIR/$vm/meta-data" \
|
|
563
|
+
"$(console_log "$vm")" "$(pid_file "$vm")"
|
|
564
|
+
ok "$vm: cleaned — next 'start' will reprovision from the base image"
|
|
565
|
+
done
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
cmd_rebuild() {
|
|
569
|
+
local vms
|
|
570
|
+
read -ra vms <<< "$(resolve_vms "$@")"
|
|
571
|
+
cmd_clean "${vms[@]}"
|
|
572
|
+
cmd_start "${vms[@]}"
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
cmd_reset() {
|
|
576
|
+
local distros
|
|
577
|
+
read -ra distros <<< "$(resolve_distros "$@")"
|
|
578
|
+
for distro in "${distros[@]}"; do
|
|
579
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
580
|
+
[[ "$(vm_distro "$vm")" == "$distro" ]] && is_running "$vm" && cmd_stop "$vm"
|
|
581
|
+
done
|
|
582
|
+
info "$distro: removing all VM files including base image..."
|
|
583
|
+
rm -rf "$VM_DIR/$distro"
|
|
584
|
+
for vm in "${ALL_VMS[@]}"; do
|
|
585
|
+
[[ "$(vm_distro "$vm")" == "$distro" ]] && rm -rf "$VM_DIR/$vm"
|
|
586
|
+
done
|
|
587
|
+
ok "$distro: reset — run 'download $distro' to start fresh"
|
|
588
|
+
done
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
check_deps
|
|
594
|
+
|
|
595
|
+
case "${1:-}" in
|
|
596
|
+
download) shift; cmd_download "$@" ;;
|
|
597
|
+
build) shift; cmd_build ;;
|
|
598
|
+
start) shift; cmd_start "$@" ;;
|
|
599
|
+
wait) shift; cmd_wait "$@" ;;
|
|
600
|
+
stop) shift; cmd_stop "$@" ;;
|
|
601
|
+
status) cmd_status ;;
|
|
602
|
+
ssh) shift; cmd_ssh "$@" ;;
|
|
603
|
+
logs) shift; cmd_logs "$@" ;;
|
|
604
|
+
clean) shift; cmd_clean "$@" ;;
|
|
605
|
+
rebuild) shift; cmd_rebuild "$@" ;;
|
|
606
|
+
reset) shift; cmd_reset "$@" ;;
|
|
607
|
+
*) usage ;;
|
|
608
|
+
esac
|
package/src/cockpit.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Ambient type declarations for the Cockpit browser global.
|
|
2
|
+
// Superset covering cockpit-caddy and cockpit-compose usage patterns.
|
|
3
|
+
/// <reference path="./css.d.ts" />
|
|
4
|
+
|
|
5
|
+
declare interface CockpitProcess extends Promise<string> {
|
|
6
|
+
stream(callback: (data: string) => void): CockpitProcess;
|
|
7
|
+
close(problem?: string): void;
|
|
8
|
+
input(data?: string, stream?: boolean): void;
|
|
9
|
+
wait?(): Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare interface CockpitChannel {
|
|
13
|
+
close(): void;
|
|
14
|
+
send(data: string): void;
|
|
15
|
+
addEventListener(event: "message", callback: (event: Event, payload: string) => void): void;
|
|
16
|
+
addEventListener(event: "close", callback: (event: Event, options: { problem?: string; message?: string }) => void): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare interface CockpitHttpRequestOptions {
|
|
20
|
+
method: string;
|
|
21
|
+
path: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
body?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare interface CockpitHttpClient {
|
|
27
|
+
get(path: string, params?: Record<string, string>): Promise<string>;
|
|
28
|
+
post(path: string, body: string, headers?: Record<string, string>): Promise<string>;
|
|
29
|
+
request(options: CockpitHttpRequestOptions): Promise<{ status: number; headers: Record<string, string>; data: string }>;
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare interface CockpitUser {
|
|
34
|
+
id: number;
|
|
35
|
+
name: string;
|
|
36
|
+
home: string;
|
|
37
|
+
shell: string;
|
|
38
|
+
groups: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare const cockpit: {
|
|
42
|
+
user(): Promise<CockpitUser>;
|
|
43
|
+
spawn(
|
|
44
|
+
args: string[],
|
|
45
|
+
options?: { superuser?: "try" | "require"; err?: string; environ?: string[] }
|
|
46
|
+
): CockpitProcess;
|
|
47
|
+
http(options: { port?: number; address?: string }): CockpitHttpClient;
|
|
48
|
+
file(
|
|
49
|
+
path: string,
|
|
50
|
+
options?: { superuser?: "try" | "require"; syntax?: unknown }
|
|
51
|
+
): {
|
|
52
|
+
read(): Promise<string>;
|
|
53
|
+
replace(content: string): Promise<void>;
|
|
54
|
+
watch(callback: (content: string | null) => void): CockpitChannel;
|
|
55
|
+
};
|
|
56
|
+
channel(options: {
|
|
57
|
+
payload: string;
|
|
58
|
+
spawn?: string[];
|
|
59
|
+
pty?: boolean;
|
|
60
|
+
superuser?: "try" | "require";
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
}): CockpitChannel;
|
|
63
|
+
};
|