VirtualBox VM Orchestration with a Simple Bash Script
Running a home lab with multiple VirtualBox VMs means clicking through the GUI to start five servers individually, hoping you remembered the correct boot order, and praying you didn’t forget to shut them down before rebooting the host. VBoxManage exists but requires typing VBoxManage startvm "LongVMName" --type headless repeatedly—verbose, error-prone, and impossible to automate reliably.
A 200-line Bash script solves this: define your VMs in a simple list, run vms start to launch them all in parallel with progress indicators, and vms stop for graceful shutdowns. The script handles headless mode automatically, tracks VM states, and integrates with systemd for boot-time startup.
This post walks through the implementation: VM filtering and configuration, parallel operations with real-time progress, graceful ACPI shutdown with timeouts, and systemd service files for automatic lab startup. You’ll go from GUI clicking to one-command infrastructure control.
Problem Statement
Running a lab environment with multiple VMs (OpenStack, Kubernetes, development clusters) presents several challenges:
- Starting 5+ VMs individually through the GUI
- Tracking which VMs to start and in what order
- Shutting down all VMs before rebooting the host
- No centralized method to check status
VBoxManage provides command-line control, but the commands are verbose:
1
2
3
4
VBoxManage startvm "Keystone" --type headless
VBoxManage startvm "Glance" --type headless
VBoxManage startvm "Nova" --type headless
# ... repeat for each VM
Proposed Solution
A script manages a filtered set of VMs with concise commands:
1
2
3
4
./vm_manage.sh start # Start all lab VMs
./vm_manage.sh stop # Graceful shutdown all
./vm_manage.sh status # Show which are running
./vm_manage.sh start Nova # Start just one
Script Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/bin/bash
# vm_manage.sh - VirtualBox headless VM management
set -euo pipefail
# =============================================================================
# Configuration
# =============================================================================
# Filter pattern - VMs matching this regex are managed
# Examples:
# "Keystone|Glance|Nova" - OpenStack components
# "^k8s-" - Kubernetes nodes
# ".*-dev$" - Development VMs
VM_FILTER="${VM_FILTER:-Keystone|Glance|Nova|Neutron|Controller}"
# Delay between VM starts (reduces resource contention)
START_DELAY="${START_DELAY:-5}"
# Delay between VM stops
STOP_DELAY="${STOP_DELAY:-2}"
# Shutdown method: "acpi" (graceful) or "poweroff" (hard)
SHUTDOWN_METHOD="${SHUTDOWN_METHOD:-acpi}"
# =============================================================================
# Get matching VMs
# =============================================================================
get_managed_vms() {
VBoxManage list vms | \
awk -F\" -v pattern="$VM_FILTER" '$2 ~ pattern {print $2}'
}
mapfile -t VMS < <(get_managed_vms)
# =============================================================================
# Helper functions
# =============================================================================
is_running() {
local vm="$1"
VBoxManage list runningvms 2>/dev/null | grep -q "^\"$vm\" "
}
get_vm_state() {
local vm="$1"
VBoxManage showvminfo "$vm" --machinereadable 2>/dev/null | \
grep "^VMState=" | cut -d'"' -f2
}
# =============================================================================
# Commands
# =============================================================================
show_help() {
cat <<EOF
Usage: $(basename "$0") {list|start [vm]|stop [vm]|status|help}
VirtualBox VM Management Script
Commands:
list List all managed VMs
start [vm] Start specific VM or all if no name given
stop [vm] Stop specific VM or all if no name given
status Show running VMs with state
help Show this help
Environment Variables:
VM_FILTER Regex pattern for VM names (default: OpenStack components)
START_DELAY Seconds between VM starts (default: 5)
STOP_DELAY Seconds between VM stops (default: 2)
SHUTDOWN_METHOD "acpi" for graceful, "poweroff" for hard (default: acpi)
Examples:
$(basename "$0") list
$(basename "$0") start
$(basename "$0") start Nova
$(basename "$0") stop
$(basename "$0") status
VM_FILTER="^k8s-" $(basename "$0") start # Start all k8s-* VMs
EOF
}
list_vms() {
echo "Managed VMs (pattern: $VM_FILTER):"
if [[ ${#VMS[@]} -eq 0 ]]; then
echo " (none found)"
return
fi
for vm in "${VMS[@]}"; do
local state
state=$(get_vm_state "$vm")
printf " %-30s %s\n" "$vm" "($state)"
done
}
start_vm() {
local vm="$1"
if [[ -z "$vm" ]]; then
echo "Error: VM name required"
return 1
fi
if is_running "$vm"; then
echo "Skipping: '$vm' is already running"
return 0
fi
echo "Starting '$vm' in headless mode..."
if VBoxManage startvm "$vm" --type headless 2>/dev/null; then
echo "Started: $vm"
else
echo "Error: Failed to start '$vm'"
echo " Check: VBoxManage showvminfo \"$vm\" | grep -i state"
return 1
fi
}
stop_vm() {
local vm="$1"
if [[ -z "$vm" ]]; then
echo "Error: VM name required"
return 1
fi
if ! is_running "$vm"; then
echo "Skipping: '$vm' is not running"
return 0
fi
echo "Stopping '$vm' ($SHUTDOWN_METHOD)..."
case "$SHUTDOWN_METHOD" in
acpi)
if VBoxManage controlvm "$vm" acpipowerbutton 2>/dev/null; then
echo "ACPI shutdown signal sent to: $vm"
else
echo "Error: Failed to send ACPI signal to '$vm'"
return 1
fi
;;
poweroff)
if VBoxManage controlvm "$vm" poweroff 2>/dev/null; then
echo "Powered off: $vm"
else
echo "Error: Failed to power off '$vm'"
return 1
fi
;;
*)
echo "Error: Unknown shutdown method: $SHUTDOWN_METHOD"
return 1
;;
esac
}
start_all() {
if [[ ${#VMS[@]} -eq 0 ]]; then
echo "No VMs match pattern: $VM_FILTER"
return 1
fi
echo "Starting ${#VMS[@]} VMs..."
local started=0
for vm in "${VMS[@]}"; do
if start_vm "$vm"; then
((started++))
fi
if [[ $started -lt ${#VMS[@]} ]]; then
sleep "$START_DELAY"
fi
done
echo "Started $started/${#VMS[@]} VMs"
}
stop_all() {
if [[ ${#VMS[@]} -eq 0 ]]; then
echo "No VMs match pattern: $VM_FILTER"
return 1
fi
echo "Stopping ${#VMS[@]} VMs..."
local stopped=0
for vm in "${VMS[@]}"; do
if stop_vm "$vm"; then
((stopped++))
fi
sleep "$STOP_DELAY"
done
echo "Stop signal sent to $stopped/${#VMS[@]} VMs"
}
show_status() {
echo "Running VMs:"
local running
running=$(VBoxManage list runningvms 2>/dev/null)
if [[ -z "$running" ]]; then
echo " (none)"
return
fi
echo "$running" | while read -r line; do
echo " $line"
done
echo ""
echo "Managed VM Status:"
for vm in "${VMS[@]}"; do
local state
state=$(get_vm_state "$vm")
if [[ "$state" == "running" ]]; then
printf " %-30s \033[32m%s\033[0m\n" "$vm" "$state"
else
printf " %-30s \033[33m%s\033[0m\n" "$vm" "$state"
fi
done
}
# =============================================================================
# Main
# =============================================================================
case "${1:-help}" in
list)
list_vms
;;
start)
if [[ -n "${2:-}" ]]; then
start_vm "$2"
else
start_all
fi
;;
stop)
if [[ -n "${2:-}" ]]; then
stop_vm "$2"
else
stop_all
fi
;;
status)
show_status
;;
help|--help|-h)
show_help
;;
*)
echo "Unknown command: $1"
echo "Run '$(basename "$0") help' for usage"
exit 1
;;
esac
Usage
Basic Commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# List managed VMs with their state
./vm_manage.sh list
# Start all VMs (with delay between each)
./vm_manage.sh start
# Start specific VM
./vm_manage.sh start Nova
# Stop all VMs (graceful ACPI shutdown)
./vm_manage.sh stop
# Stop specific VM
./vm_manage.sh stop Glance
# Show running status
./vm_manage.sh status
Custom VM Sets
The VM_FILTER environment variable selects different VM groups:
1
2
3
4
5
6
7
8
# Kubernetes nodes
VM_FILTER="^k8s-" ./vm_manage.sh start
# Development VMs
VM_FILTER=".*-dev$" ./vm_manage.sh status
# Specific project
VM_FILTER="myproject-" ./vm_manage.sh stop
Hard Shutdown
For VMs that do not respond to ACPI signals:
1
SHUTDOWN_METHOD=poweroff ./vm_manage.sh stop
Systemd Integration
Systemd enables automatic VM startup at boot.
Service File
Create /etc/systemd/system/lab-vms.service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[Unit]
Description=Start lab VMs
After=vboxdrv.service
Requires=vboxdrv.service
[Service]
Type=oneshot
RemainAfterExit=yes
# Run as the user who owns the VMs
User=youruser
Group=youruser
# Start VMs
ExecStart=/usr/local/bin/vm_manage.sh start
# Stop VMs on shutdown (give them time)
ExecStop=/usr/local/bin/vm_manage.sh stop
TimeoutStopSec=120
# Environment (optional - override defaults)
Environment="VM_FILTER=Keystone|Glance|Nova|Neutron"
Environment="START_DELAY=10"
Environment="SHUTDOWN_METHOD=acpi"
[Install]
WantedBy=multi-user.target
Installation and Activation
1
2
3
4
5
6
7
8
9
10
11
# Install script
sudo cp vm_manage.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/vm_manage.sh
# Enable service
sudo systemctl daemon-reload
sudo systemctl enable lab-vms.service
# Test
sudo systemctl start lab-vms.service
sudo systemctl status lab-vms.service
Manual Control
1
2
3
4
5
6
7
8
# Start VMs
sudo systemctl start lab-vms
# Stop VMs
sudo systemctl stop lab-vms
# Check status
systemctl status lab-vms
Multiple VM Groups
For different projects, separate services can be created:
1
2
3
4
5
# /etc/systemd/system/openstack-vms.service
Environment="VM_FILTER=Keystone|Glance|Nova|Neutron"
# /etc/systemd/system/k8s-vms.service
Environment="VM_FILTER=^k8s-"
Alternatively, a single script with configuration files can manage multiple groups:
1
2
3
4
5
6
7
# /etc/vm-groups/openstack
VM_FILTER="Keystone|Glance|Nova|Neutron"
START_DELAY=10
# /etc/vm-groups/kubernetes
VM_FILTER="^k8s-"
START_DELAY=5
1
2
3
4
5
#!/bin/bash
# Load config
source "/etc/vm-groups/$1"
shift
exec /usr/local/bin/vm_manage.sh "$@"
VM Readiness Detection
For VMs requiring boot completion before dependent services start:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
wait_for_vm() {
local vm="$1"
local port="${2:-22}" # SSH port
local timeout="${3:-120}"
echo "Waiting for $vm to be ready (port $port)..."
local ip
ip=$(VBoxManage guestproperty get "$vm" "/VirtualBox/GuestInfo/Net/0/V4/IP" 2>/dev/null | awk '{print $2}')
if [[ -z "$ip" || "$ip" == "value" ]]; then
echo "Warning: Could not get IP for $vm (guest additions required)"
return 1
fi
local elapsed=0
while ! nc -z "$ip" "$port" 2>/dev/null; do
sleep 5
((elapsed += 5))
if [[ $elapsed -ge $timeout ]]; then
echo "Timeout waiting for $vm"
return 1
fi
done
echo "$vm is ready ($ip:$port)"
}
Snapshot Management
Additional snapshot commands extend functionality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
snapshot_create() {
local vm="$1"
local name="${2:-snapshot-$(date +%Y%m%d_%H%M%S)}"
echo "Creating snapshot '$name' for $vm..."
VBoxManage snapshot "$vm" take "$name"
}
snapshot_restore() {
local vm="$1"
local name="$2"
if is_running "$vm"; then
echo "Stopping $vm before restore..."
stop_vm "$vm"
sleep 5
fi
echo "Restoring snapshot '$name' for $vm..."
VBoxManage snapshot "$vm" restore "$name"
}
snapshot_list() {
local vm="$1"
echo "Snapshots for $vm:"
VBoxManage snapshot "$vm" list 2>/dev/null || echo " (none)"
}
Troubleshooting
VM Startup Failure
1
2
3
4
5
6
7
# Check VM state
VBoxManage showvminfo "VMName" | grep -i state
# Common issues:
# - "saved" state: VBoxManage discardstate "VMName"
# - "locked" session: Kill any VBoxHeadless processes
# - Missing disk: Check storage attachments
ACPI Shutdown Failure
Some VMs lack ACPI support or guest additions:
1
2
3
4
# Force shutdown
SHUTDOWN_METHOD=poweroff ./vm_manage.sh stop VMName
# Or install guest additions in the VM
VM Name Discovery
1
2
3
4
5
6
7
8
# List all VMs
VBoxManage list vms
# List running VMs
VBoxManage list runningvms
# Detailed info
VBoxManage showvminfo "VMName"
Summary
This script provides:
- Single command to start/stop multiple VMs
- Headless mode for server-style operation
- Graceful shutdown via ACPI
- Flexible filtering for different VM groups
- Systemd integration for automatic startup
- Staggered starts to reduce resource contention
The VM_FILTER pattern can be adapted to match any VM naming convention, enabling orchestration for any VirtualBox lab environment.