|
#!/bin/bash |
|
|
|
# Multipass VM Manager for Testing |
|
# Creates Ubuntu VMs that mimic real cloud servers (EC2, DigitalOcean, etc.) |
|
# Usage: ./multipass-manager.sh [command] |
|
|
|
set -e |
|
|
|
# Configuration |
|
VM_PREFIX="deploy" |
|
NUM_VMS=3 |
|
MEMORY="1G" |
|
CPUS=1 |
|
DISK="20G" |
|
UBUNTU_VERSION="24.04" |
|
SNAPSHOT_NAME="clean" |
|
SSH_USER="ubuntu" # Mimics EC2/cloud providers (non-root with sudo) |
|
|
|
# Paths for cleanup tracking |
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
STATE_DIR="$SCRIPT_DIR/.multipass-state" |
|
VM_LIST_FILE="$STATE_DIR/vms.txt" |
|
SSH_CONFIG_BACKUP="$STATE_DIR/ssh_config.backup" |
|
SSH_CONFIG_MARKER="# MULTIPASS-MANAGER VMs - START" |
|
SSH_CONFIG_MARKER_END="# MULTIPASS-MANAGER VMs - END" |
|
|
|
# Colors for output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' # No Color |
|
|
|
function print_success() { |
|
echo -e "${GREEN}✓ $1${NC}" |
|
} |
|
|
|
function print_error() { |
|
echo -e "${RED}✗ $1${NC}" |
|
} |
|
|
|
function print_info() { |
|
echo -e "${YELLOW}ℹ $1${NC}" |
|
} |
|
|
|
function print_warning() { |
|
echo -e "${BLUE}⚠ $1${NC}" |
|
} |
|
|
|
function init_state_dir() { |
|
if [ ! -d "$STATE_DIR" ]; then |
|
mkdir -p "$STATE_DIR" |
|
print_info "Created state directory: $STATE_DIR" |
|
fi |
|
} |
|
|
|
function track_vm() { |
|
local vm_name=$1 |
|
init_state_dir |
|
echo "$vm_name" >> "$VM_LIST_FILE" |
|
} |
|
|
|
function get_tracked_vms() { |
|
if [ -f "$VM_LIST_FILE" ]; then |
|
sort -u "$VM_LIST_FILE" |
|
fi |
|
} |
|
|
|
function clear_tracked_vms() { |
|
if [ -f "$VM_LIST_FILE" ]; then |
|
rm "$VM_LIST_FILE" |
|
fi |
|
} |
|
|
|
function backup_ssh_config() { |
|
if [ -f ~/.ssh/config ] && [ ! -f "$SSH_CONFIG_BACKUP" ]; then |
|
init_state_dir |
|
cp ~/.ssh/config "$SSH_CONFIG_BACKUP" |
|
print_info "Backed up SSH config" |
|
fi |
|
} |
|
|
|
function remove_ssh_config_entries() { |
|
if [ -f ~/.ssh/config ]; then |
|
if grep -q "$SSH_CONFIG_MARKER" ~/.ssh/config; then |
|
print_info "Removing SSH config entries..." |
|
|
|
# Create temp file without our entries |
|
awk "/$SSH_CONFIG_MARKER/,/$SSH_CONFIG_MARKER_END/ {next} {print}" ~/.ssh/config > ~/.ssh/config.tmp |
|
mv ~/.ssh/config.tmp ~/.ssh/config |
|
|
|
print_success "SSH config entries removed" |
|
fi |
|
fi |
|
} |
|
|
|
# SSH key paths (stored in state directory, not user's ~/.ssh) |
|
SSH_KEY_PATH="$STATE_DIR/vms_key" |
|
SSH_PUBKEY_PATH="$STATE_DIR/vms_key.pub" |
|
|
|
# Generate SSH keypair for VMs (if not exists) |
|
function generate_ssh_keypair() { |
|
if [ -f "$SSH_KEY_PATH" ]; then |
|
print_info "Using existing SSH keypair: $SSH_KEY_PATH" |
|
return |
|
fi |
|
|
|
print_info "Generating SSH keypair for VMs..." |
|
ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "vms-manager-key" -q |
|
chmod 600 "$SSH_KEY_PATH" |
|
chmod 644 "$SSH_PUBKEY_PATH" |
|
print_success "SSH keypair generated: $SSH_KEY_PATH" |
|
} |
|
|
|
# Get the VMS public key |
|
function get_ssh_pubkey() { |
|
if [ -f "$SSH_PUBKEY_PATH" ]; then |
|
cat "$SSH_PUBKEY_PATH" |
|
else |
|
echo "" |
|
fi |
|
} |
|
|
|
# Show the private key for easy copy/paste |
|
function show_private_key() { |
|
if [ -f "$SSH_KEY_PATH" ]; then |
|
echo "" |
|
print_info "═══════════════════════════════════════════════════════════════" |
|
print_info "SSH PRIVATE KEY (copy this 'Add Server' form):" |
|
print_info "═══════════════════════════════════════════════════════════════" |
|
echo "" |
|
cat "$SSH_KEY_PATH" |
|
echo "" |
|
print_info "═══════════════════════════════════════════════════════════════" |
|
print_info "Key file: $SSH_KEY_PATH" |
|
print_info "═══════════════════════════════════════════════════════════════" |
|
else |
|
print_error "No SSH key found. Run 'create' first." |
|
fi |
|
} |
|
|
|
# Inject SSH key into a VM for the ubuntu user |
|
function inject_ssh_key() { |
|
local vm_name=$1 |
|
local pubkey=$(get_ssh_pubkey) |
|
|
|
if [ -z "$pubkey" ]; then |
|
print_error "No SSH public key found!" |
|
return 1 |
|
fi |
|
|
|
# Inject into ubuntu user's authorized_keys (multipass exec runs as ubuntu by default) |
|
multipass exec "$vm_name" -- bash -c "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$pubkey' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" |
|
print_success "SSH key injected for $SSH_USER@$vm_name" |
|
} |
|
|
|
function create_vms() { |
|
print_info "Creating $NUM_VMS VMs..." |
|
init_state_dir |
|
|
|
# Generate keypair (or use existing) |
|
generate_ssh_keypair |
|
|
|
local pubkey=$(get_ssh_pubkey) |
|
if [ -z "$pubkey" ]; then |
|
print_error "Failed to get SSH public key!" |
|
exit 1 |
|
fi |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
print_info "VM $VM_NAME already exists, skipping..." |
|
else |
|
print_info "Creating $VM_NAME..." |
|
multipass launch $UBUNTU_VERSION \ |
|
--name "$VM_NAME" \ |
|
--memory "$MEMORY" \ |
|
--cpus "$CPUS" \ |
|
--disk "$DISK" |
|
track_vm "$VM_NAME" |
|
print_success "Created $VM_NAME" |
|
|
|
# Inject SSH key for external access (manual SSH) |
|
inject_ssh_key "$VM_NAME" |
|
fi |
|
done |
|
|
|
print_success "All VMs created!" |
|
echo "" |
|
show_ips |
|
echo "" |
|
print_info "To add these servers:" |
|
echo " Enter: IP, User='$SSH_USER', paste key" |
|
show_private_key |
|
} |
|
|
|
function snapshot_vms() { |
|
print_info "Creating snapshots for all VMs..." |
|
print_info "Stopping VMs first (required for snapshots)..." |
|
|
|
# Stop all VMs first |
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
multipass stop "$VM_NAME" 2>/dev/null || true |
|
fi |
|
done |
|
|
|
# Take snapshots |
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
SNAPSHOT="${VM_NAME}.${SNAPSHOT_NAME}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
# Delete existing snapshot if present (makes this idempotent) |
|
multipass delete --purge "$SNAPSHOT" 2>/dev/null || true |
|
|
|
print_info "Snapshotting $VM_NAME..." |
|
if multipass snapshot "$VM_NAME" --name "$SNAPSHOT_NAME" 2>/dev/null; then |
|
print_success "Snapshot created for $VM_NAME" |
|
else |
|
print_warning "Failed to create snapshot for $VM_NAME" |
|
fi |
|
else |
|
print_error "VM $VM_NAME doesn't exist" |
|
fi |
|
done |
|
|
|
# Restart all VMs |
|
print_info "Restarting VMs..." |
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
multipass start "$VM_NAME" 2>/dev/null || true |
|
fi |
|
done |
|
|
|
print_success "All snapshots created! VMs are running again." |
|
} |
|
|
|
function restore_vms() { |
|
print_info "Restoring all VMs to clean state..." |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
SNAPSHOT="${VM_NAME}.${SNAPSHOT_NAME}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
print_info "Restoring $VM_NAME..." |
|
multipass restore "$SNAPSHOT" 2>/dev/null || print_warning "No snapshot found for $VM_NAME" |
|
print_success "Restored $VM_NAME" |
|
else |
|
print_error "VM $VM_NAME doesn't exist" |
|
fi |
|
done |
|
|
|
print_success "All VMs restored to clean state!" |
|
} |
|
|
|
function start_vms() { |
|
print_info "Starting all VMs..." |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
print_info "Starting $VM_NAME..." |
|
multipass start "$VM_NAME" 2>/dev/null || print_warning "Could not start $VM_NAME" |
|
print_success "Started $VM_NAME" |
|
else |
|
print_warning "VM $VM_NAME doesn't exist" |
|
fi |
|
done |
|
|
|
print_success "All VMs started!" |
|
echo "" |
|
print_info "Note: IPs may have changed. Run 'ssh-update' to refresh SSH config." |
|
show_ips |
|
} |
|
|
|
function stop_vms() { |
|
print_info "Stopping all VMs..." |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
print_info "Stopping $VM_NAME..." |
|
multipass stop "$VM_NAME" 2>/dev/null || print_warning "Could not stop $VM_NAME" |
|
print_success "Stopped $VM_NAME" |
|
fi |
|
done |
|
|
|
print_success "All VMs stopped!" |
|
} |
|
|
|
function list_vms() { |
|
print_info "Current VMs:" |
|
multipass list 2>/dev/null || print_error "Multipass not available" |
|
} |
|
|
|
function show_ips() { |
|
print_info "VM IP Addresses:" |
|
echo "" |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
IP=$(multipass info "$VM_NAME" 2>/dev/null | grep IPv4 | awk '{print $2}') |
|
echo "$VM_NAME: $IP" |
|
fi |
|
done |
|
} |
|
|
|
function ssh_vm() { |
|
if [ -z "$1" ]; then |
|
print_error "Please specify VM number (e.g., ./multipass-manager.sh ssh 1)" |
|
exit 1 |
|
fi |
|
|
|
VM_NAME="${VM_PREFIX}${1}" |
|
print_info "Connecting to $VM_NAME..." |
|
multipass shell "$VM_NAME" |
|
} |
|
|
|
function destroy_vms() { |
|
print_warning "This will destroy all VMs and clean up everything!" |
|
echo "" |
|
|
|
# Show what will be cleaned |
|
print_info "The following will be removed:" |
|
echo " - All VMs matching prefix: $VM_PREFIX" |
|
echo " - All snapshots" |
|
echo " - SSH config entries (if any)" |
|
echo " - State directory: $STATE_DIR" |
|
echo "" |
|
|
|
read -p "Are you sure you want to destroy everything? (y/N) " -n 1 -r |
|
echo |
|
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then |
|
# Get tracked VMs first (in case list file exists) |
|
local tracked_vms=$(get_tracked_vms) |
|
|
|
# Remove VMs by prefix pattern |
|
print_info "Destroying VMs..." |
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
print_info "Destroying $VM_NAME..." |
|
multipass delete "$VM_NAME" 2>/dev/null || true |
|
fi |
|
done |
|
|
|
# Also destroy any tracked VMs that might have different names |
|
if [ -n "$tracked_vms" ]; then |
|
while IFS= read -r vm; do |
|
if multipass list 2>/dev/null | grep -q "$vm"; then |
|
print_info "Destroying tracked VM: $vm..." |
|
multipass delete "$vm" 2>/dev/null || true |
|
fi |
|
done <<< "$tracked_vms" |
|
fi |
|
|
|
# Purge deleted VMs |
|
multipass purge 2>/dev/null || true |
|
print_success "All VMs destroyed!" |
|
|
|
# Clean up SSH config |
|
remove_ssh_config_entries |
|
|
|
# Remove state directory |
|
if [ -d "$STATE_DIR" ]; then |
|
rm -rf "$STATE_DIR" |
|
print_success "State directory removed" |
|
fi |
|
|
|
print_success "System is clean! All traces removed." |
|
else |
|
print_info "Cancelled" |
|
fi |
|
} |
|
|
|
function update_ssh_config() { |
|
print_info "Updating SSH configuration..." |
|
backup_ssh_config |
|
|
|
# Remove old entries if they exist |
|
remove_ssh_config_entries |
|
|
|
# Create SSH config directory if it doesn't exist |
|
mkdir -p ~/.ssh |
|
touch ~/.ssh/config |
|
|
|
# Add new entries |
|
{ |
|
echo "" |
|
echo "$SSH_CONFIG_MARKER" |
|
echo "# Auto-generated by multipass-manager.sh" |
|
echo "# Run './multipass-manager.sh destroy' to remove these entries" |
|
echo "" |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
IP=$(multipass info "$VM_NAME" 2>/dev/null | grep IPv4 | awk '{print $2}') |
|
echo "Host $VM_NAME" |
|
echo " HostName $IP" |
|
echo " User $SSH_USER" |
|
echo " IdentityFile $SSH_KEY_PATH" |
|
echo " StrictHostKeyChecking no" |
|
echo " UserKnownHostsFile /dev/null" |
|
echo "" |
|
fi |
|
done |
|
|
|
echo "$SSH_CONFIG_MARKER_END" |
|
} >> ~/.ssh/config |
|
|
|
print_success "SSH config updated! You can now use: ssh ${VM_PREFIX}1" |
|
print_info "Entries will be removed when you run './multipass-manager.sh destroy'" |
|
} |
|
|
|
function get_ssh_config() { |
|
print_info "SSH configuration for all VMs:" |
|
echo "" |
|
|
|
for i in $(seq 1 $NUM_VMS); do |
|
VM_NAME="${VM_PREFIX}${i}" |
|
|
|
if multipass list 2>/dev/null | grep -q "$VM_NAME"; then |
|
IP=$(multipass info "$VM_NAME" 2>/dev/null | grep IPv4 | awk '{print $2}') |
|
echo "Host $VM_NAME" |
|
echo " HostName $IP" |
|
echo " User $SSH_USER" |
|
echo " IdentityFile $SSH_KEY_PATH" |
|
echo " StrictHostKeyChecking no" |
|
echo " UserKnownHostsFile /dev/null" |
|
echo "" |
|
fi |
|
done |
|
|
|
echo "" |
|
print_info "To add these to your SSH config, run:" |
|
echo " ./multipass-manager.sh ssh-update" |
|
} |
|
|
|
function show_status() { |
|
echo "" |
|
print_info "=== Multipass Manager Status ===" |
|
echo "" |
|
|
|
# Check if state directory exists |
|
if [ -d "$STATE_DIR" ]; then |
|
print_success "State directory: $STATE_DIR" |
|
|
|
if [ -f "$VM_LIST_FILE" ]; then |
|
local count=$(wc -l < "$VM_LIST_FILE" | tr -d ' ') |
|
echo " Tracked VMs: $count" |
|
fi |
|
|
|
if [ -f "$SSH_CONFIG_BACKUP" ]; then |
|
echo " SSH config backup: exists" |
|
fi |
|
else |
|
print_info "State directory: not created (no VMs yet)" |
|
fi |
|
|
|
echo "" |
|
print_info "Active VMs:" |
|
multipass list 2>/dev/null | grep "$VM_PREFIX" || echo " None" |
|
|
|
echo "" |
|
print_info "SSH Config entries:" |
|
if [ -f ~/.ssh/config ] && grep -q "$SSH_CONFIG_MARKER" ~/.ssh/config; then |
|
print_success "Present (will be cleaned on destroy)" |
|
else |
|
print_info "Not configured" |
|
fi |
|
|
|
echo "" |
|
} |
|
|
|
function show_help() { |
|
cat << EOF |
|
Multipass VM Manager for Testing |
|
|
|
Creates Ubuntu VMs that mimic real cloud servers (EC2, DigitalOcean, etc.) |
|
for testing. |
|
|
|
Usage: ./multipass-manager.sh [command] |
|
|
|
Commands: |
|
create Create VMs + generate SSH key (auto-displayed) |
|
key Show SSH private key |
|
start Start all VMs (after reboot) |
|
stop Stop all VMs |
|
snapshot Save clean state (before testing) |
|
restore Restore all VMs to clean state |
|
list List all VMs |
|
ips Show IP addresses |
|
ssh [num] SSH into a VM (e.g., ssh 1) |
|
ssh-config Show SSH config for all VMs |
|
ssh-update Add VMs to ~/.ssh/config |
|
status Show current state |
|
destroy Nuke everything (VMs, keys, SSH config) |
|
help Show this help |
|
|
|
After Restarting Mac: |
|
VMs persist but don't auto-start. IPs may change. |
|
./multipass-manager.sh start # Start VMs |
|
./multipass-manager.sh ssh-update # Refresh SSH config |
|
|
|
Typical Workflow: |
|
1. ./multipass-manager.sh create # Create VMs + show key |
|
2. ./multipass-manager.sh snapshot # Save clean state |
|
3. Add VMs, commission them |
|
4. Test deployments |
|
5. ./multipass-manager.sh restore # Reset to clean |
|
6. Repeat 3-5 as needed |
|
7. ./multipass-manager.sh destroy # Clean up |
|
|
|
EOF |
|
} |
|
|
|
# Main script logic |
|
case "${1:-help}" in |
|
create) |
|
create_vms |
|
;; |
|
key) |
|
show_private_key |
|
;; |
|
start) |
|
start_vms |
|
;; |
|
stop) |
|
stop_vms |
|
;; |
|
snapshot) |
|
snapshot_vms |
|
;; |
|
restore) |
|
restore_vms |
|
;; |
|
list) |
|
list_vms |
|
;; |
|
ips) |
|
show_ips |
|
;; |
|
ssh) |
|
ssh_vm "$2" |
|
;; |
|
ssh-config) |
|
get_ssh_config |
|
;; |
|
ssh-update) |
|
update_ssh_config |
|
;; |
|
destroy) |
|
destroy_vms |
|
;; |
|
status) |
|
show_status |
|
;; |
|
help|--help|-h) |
|
show_help |
|
;; |
|
*) |
|
print_error "Unknown command: $1" |
|
echo "" |
|
show_help |
|
exit 1 |
|
;; |
|
esac |