Skip to content

Instantly share code, notes, and snippets.

@CkCreative
Created January 19, 2026 05:47
Show Gist options
  • Select an option

  • Save CkCreative/0371e8295fe07fe0ce10187dde8fb600 to your computer and use it in GitHub Desktop.

Select an option

Save CkCreative/0371e8295fe07fe0ce10187dde8fb600 to your computer and use it in GitHub Desktop.
Easily Manage Multipass VMs on Mac

VMS - Multipass VM Manager for Testing

Creates Ubuntu VMs easily.

Features

  • Auto-generates SSH keypair - Keys stored in .multipass-state/, displayed for easy copy/paste
  • Mimics real cloud servers - Ubuntu with ubuntu user + passwordless sudo (like EC2)

Prerequisites

Quick Start

./multipass-manager.sh create    # Creates VMs + displays SSH key

Commands

Command Description
create Create VMs + generate SSH key
key Show SSH private key
start Start VMs (after reboot)
stop Stop VMs
snapshot Save clean state
restore Reset to clean state
ips Show VM IPs
ssh [n] SSH into VM n
ssh-update Add VMs to ~/.ssh/config
status Show current state
destroy Delete everything

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 (IPs may have changed)
./multipass-manager.sh ips        # Get new IPs

Workflow

./multipass-manager.sh create      # 1. Create VMs + show key
./multipass-manager.sh snapshot    # 2. Save clean state

# Copy key

./multipass-manager.sh restore     # 3. Reset to clean
# Repeat testing...

./multipass-manager.sh destroy     # 4. Clean up everything

Where Keys Are Stored

vms/
├── multipass-manager.sh
└── .multipass-state/
    ├── vms_key           # Private key
    ├── vms_key.pub       # Public key (injected into VMs)
    ├── vms.txt           # Tracked VM list
    └── ssh_config.backup # SSH config backup

All of this is deleted on destroy.

#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment