From 88917d3083bc3c85ccea6e1e76df2a700847df47 Mon Sep 17 00:00:00 2001 From: Norm Brandinger Date: Tue, 30 Dec 2025 14:43:10 -0500 Subject: [PATCH] feat: replace Vagrant with Multipass for ARM64 VM management Vagrant on Apple Silicon proved unreliable: - vagrant-qemu: VMs boot but SSH never connects - VirtualBox: No ARM64 boxes available - VMware: Requires paid software New approach uses Multipass which works natively on ARM64: - Added scripts/multipass-vms.sh for VM lifecycle management - Archived failed vagrant/ to archive/vagrant-failed/ - Ubuntu 22.04 ARM64 VMs with proper networking - Simple: create, destroy, start, stop, status, ssh commands - Auto-generates Ansible inventory with VM IPs Usage: brew install --cask multipass ./scripts/multipass-vms.sh create ./scripts/multipass-vms.sh inventory ansible-playbook -i ansible/inventory/multipass.yml ... --- ansible/inventory/vagrant.yml | 18 +- archive/README.md | 18 +- {vagrant => archive/vagrant-failed}/README.md | 0 .../vagrant-failed}/Vagrantfile | 67 ++-- scripts/multipass-vms.sh | 302 ++++++++++++++++++ 5 files changed, 364 insertions(+), 41 deletions(-) rename {vagrant => archive/vagrant-failed}/README.md (100%) rename {vagrant => archive/vagrant-failed}/Vagrantfile (75%) create mode 100755 scripts/multipass-vms.sh diff --git a/ansible/inventory/vagrant.yml b/ansible/inventory/vagrant.yml index 69fba53..bb720c0 100644 --- a/ansible/inventory/vagrant.yml +++ b/ansible/inventory/vagrant.yml @@ -1,4 +1,4 @@ -# Ansible inventory for vagrant-qemu VMs +# Ansible inventory for VirtualBox VMs # This inventory is used when provisioning via 'vagrant provision' # or when running Ansible directly against Vagrant VMs # @@ -14,7 +14,7 @@ all: ansible_python_interpreter: /usr/bin/python3 # DevStack Core integration - devstack_host: 192.168.64.1 + devstack_host: 192.168.56.1 vault_addr: "http://{{ devstack_host }}:8200" postgres_host: "{{ devstack_host }}" redis_host: "{{ devstack_host }}" @@ -22,7 +22,7 @@ all: # Domain configuration domain: sip.local - internal_gateway: 192.168.64.1 + internal_gateway: 192.168.56.1 children: voip: @@ -30,8 +30,8 @@ all: sip_proxies: hosts: sip-1: - ansible_host: 192.168.64.10 - internal_ip: 192.168.64.10 + ansible_host: 192.168.56.10 + internal_ip: 192.168.56.10 has_external_interface: true opensips_enabled: true kamailio_enabled: false @@ -39,16 +39,16 @@ all: media_servers: hosts: media-1: - ansible_host: 192.168.64.20 - internal_ip: 192.168.64.20 + ansible_host: 192.168.56.20 + internal_ip: 192.168.56.20 has_external_interface: true rtpengine_enabled: true pbx: hosts: pbx-1: - ansible_host: 192.168.64.30 - internal_ip: 192.168.64.30 + ansible_host: 192.168.56.30 + internal_ip: 192.168.56.30 has_external_interface: false asterisk_enabled: true freeswitch_enabled: false diff --git a/archive/README.md b/archive/README.md index a37e2ab..deac331 100644 --- a/archive/README.md +++ b/archive/README.md @@ -2,10 +2,26 @@ This directory contains legacy implementations that have been superseded by newer approaches. +## vagrant-failed/ + +**Archived**: 2025-12-30 +**Replaced by**: Multipass (`scripts/multipass-vms.sh`) + +The Vagrant approach failed on Apple Silicon due to: +1. **vagrant-qemu**: VMs boot but SSH never connects (cloud-init hangs) +2. **VirtualBox**: No ARM64 boxes available (x86 only) +3. **VMware**: Requires paid software + +### Lesson Learned + +Vagrant's ARM64 ecosystem is immature. Use native tools like Multipass instead. + +--- + ## libvirt-legacy/ **Archived**: 2025-12-30 -**Replaced by**: `vagrant/` directory with vagrant-qemu +**Replaced by**: Multipass (`scripts/multipass-vms.sh`) The libvirt implementation was the original VM infrastructure using: - libvirt/QEMU with socket_vmnet for networking diff --git a/vagrant/README.md b/archive/vagrant-failed/README.md similarity index 100% rename from vagrant/README.md rename to archive/vagrant-failed/README.md diff --git a/vagrant/Vagrantfile b/archive/vagrant-failed/Vagrantfile similarity index 75% rename from vagrant/Vagrantfile rename to archive/vagrant-failed/Vagrantfile index 8b7b1f1..c45d45b 100644 --- a/vagrant/Vagrantfile +++ b/archive/vagrant-failed/Vagrantfile @@ -3,15 +3,14 @@ # # voip-stack Vagrantfile # -# Creates 3 VMs for VoIP infrastructure using vagrant-qemu on Apple Silicon: -# - sip-1: OpenSIPS SIP Proxy (192.168.64.10) -# - pbx-1: Asterisk PBX (192.168.64.30) -# - media-1: RTPEngine Media Proxy (192.168.64.20) +# Creates 3 VMs for VoIP infrastructure using VirtualBox on Apple Silicon: +# - sip-1: OpenSIPS SIP Proxy (192.168.56.10) +# - pbx-1: Asterisk PBX (192.168.56.30) +# - media-1: RTPEngine Media Proxy (192.168.56.20) # # Prerequisites: -# brew install qemu +# brew install --cask virtualbox # brew install --cask vagrant -# vagrant plugin install vagrant-qemu # # Usage: # vagrant up # Start all VMs @@ -27,14 +26,13 @@ Vagrant.require_version ">= 2.3.0" # VM Configuration +# Using generic boxes which support multiple providers including VirtualBox VM_BOX = "generic/debian12" -VM_BOX_VERSION = "4.3.12" +VM_BOX_VERSION = ">= 0" # Network Configuration -# Note: vagrant-qemu uses QEMU's user-mode networking by default -# We configure private_network for VM-to-VM and host-to-VM communication -NETWORK_PREFIX = "192.168.64" -GATEWAY = "192.168.64.1" +# VirtualBox uses 192.168.56.0/24 for host-only networking by default +NETWORK_PREFIX = "192.168.56" # VM Definitions VMS = { @@ -64,14 +62,18 @@ VMS = { } } -# DevStack Core host IP (Mac host running Docker services) -DEVSTACK_HOST = "192.168.64.1" +# DevStack Core host IP (Mac host - accessible via host-only network) +# VirtualBox host-only adapter makes host available at 192.168.56.1 +DEVSTACK_HOST = "192.168.56.1" Vagrant.configure("2") do |config| # Common box configuration config.vm.box = VM_BOX config.vm.box_version = VM_BOX_VERSION + # Default to VirtualBox provider + config.vm.provider "virtualbox" + # SSH configuration config.ssh.insert_key = true config.ssh.forward_agent = true @@ -84,28 +86,31 @@ Vagrant.configure("2") do |config| config.vm.define vm_name do |vm| vm.vm.hostname = vm_name - # Private network for VM communication + # Private network for VM communication (host-only) vm.vm.network "private_network", ip: vm_config[:ip], netmask: "255.255.255.0" - # QEMU provider configuration - vm.vm.provider "qemu" do |qe| - qe.memory = vm_config[:memory].to_s - qe.smp = vm_config[:cpus].to_s - qe.arch = "aarch64" - qe.machine = "virt,accel=hvf,highmem=on" - qe.cpu = "host" - qe.net_device = "virtio-net-pci" - - # Extra QEMU arguments for Apple Silicon optimization - qe.extra_qemu_args = %w( - -cpu host - -smp 2,sockets=1,cores=2,threads=1 - ) - - # Enable serial console for debugging - qe.extra_qemu_args += %w(-serial mon:stdio) if ENV['VAGRANT_DEBUG'] + # VirtualBox provider configuration + vm.vm.provider "virtualbox" do |vb| + vb.name = "voip-#{vm_name}" + vb.memory = vm_config[:memory] + vb.cpus = vm_config[:cpus] + + # Performance optimizations + vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] + vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"] + vb.customize ["modifyvm", :id, "--ioapic", "on"] + + # Disable audio and USB for server VMs + vb.customize ["modifyvm", :id, "--audio", "none"] + vb.customize ["modifyvm", :id, "--usb", "off"] + + # Don't display VirtualBox GUI + vb.gui = false + + # Link clone for faster VM creation + vb.linked_clone = true end # VM-specific provisioning message diff --git a/scripts/multipass-vms.sh b/scripts/multipass-vms.sh new file mode 100755 index 0000000..4588623 --- /dev/null +++ b/scripts/multipass-vms.sh @@ -0,0 +1,302 @@ +#!/bin/bash +####################################### +# voip-stack VM Manager using Multipass +# +# Creates and manages VoIP VMs on Apple Silicon +# +# Usage: +# ./scripts/multipass-vms.sh create # Create all VMs +# ./scripts/multipass-vms.sh destroy # Destroy all VMs +# ./scripts/multipass-vms.sh start # Start all VMs +# ./scripts/multipass-vms.sh stop # Stop all VMs +# ./scripts/multipass-vms.sh status # Show VM status +# ./scripts/multipass-vms.sh ssh # SSH into VM +####################################### + +set -euo pipefail + +# VM Definitions +declare -A VMS=( + ["sip-1"]="cpus=2,mem=2G,disk=20G" + ["media-1"]="cpus=4,mem=4G,disk=20G" + ["pbx-1"]="cpus=2,mem=4G,disk=20G" +) + +# Ubuntu version (22.04 LTS has good ARM64 support) +UBUNTU_VERSION="22.04" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +check_multipass() { + if ! command -v multipass &> /dev/null; then + error "Multipass not installed. Run: brew install --cask multipass" + fi +} + +cmd_create() { + check_multipass + info "Creating VoIP VMs..." + + for vm_name in "${!VMS[@]}"; do + local config="${VMS[$vm_name]}" + local cpus=$(echo "$config" | grep -o 'cpus=[^,]*' | cut -d= -f2) + local mem=$(echo "$config" | grep -o 'mem=[^,]*' | cut -d= -f2) + local disk=$(echo "$config" | grep -o 'disk=[^,]*' | cut -d= -f2) + + if multipass info "$vm_name" &>/dev/null; then + warn "VM $vm_name already exists, skipping" + else + info "Creating $vm_name (cpus=$cpus, mem=$mem, disk=$disk)..." + multipass launch "$UBUNTU_VERSION" \ + --name "$vm_name" \ + --cpus "$cpus" \ + --memory "$mem" \ + --disk "$disk" + success "Created $vm_name" + fi + done + + # Show status and IPs + echo "" + cmd_status +} + +cmd_destroy() { + check_multipass + warn "This will destroy all VoIP VMs!" + read -p "Are you sure? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + for vm_name in "${!VMS[@]}"; do + if multipass info "$vm_name" &>/dev/null; then + info "Destroying $vm_name..." + multipass delete "$vm_name" --purge + success "Destroyed $vm_name" + fi + done + else + info "Cancelled" + fi +} + +cmd_start() { + check_multipass + info "Starting VMs..." + for vm_name in "${!VMS[@]}"; do + if multipass info "$vm_name" &>/dev/null; then + multipass start "$vm_name" + success "Started $vm_name" + else + warn "VM $vm_name doesn't exist" + fi + done +} + +cmd_stop() { + check_multipass + info "Stopping VMs..." + for vm_name in "${!VMS[@]}"; do + if multipass info "$vm_name" &>/dev/null; then + multipass stop "$vm_name" + success "Stopped $vm_name" + fi + done +} + +cmd_status() { + check_multipass + echo -e "${BLUE}═══ voip-stack VM Status ═══${NC}" + echo "" + + # Table header + printf "%-12s %-10s %-16s %-6s %-6s\n" "NAME" "STATE" "IP" "CPUS" "MEM" + printf "%-12s %-10s %-16s %-6s %-6s\n" "----" "-----" "--" "----" "---" + + for vm_name in sip-1 media-1 pbx-1; do + if multipass info "$vm_name" &>/dev/null; then + local state=$(multipass info "$vm_name" --format csv | tail -1 | cut -d, -f2) + local ip=$(multipass info "$vm_name" --format csv | tail -1 | cut -d, -f3) + local cpus=$(multipass info "$vm_name" --format csv | tail -1 | cut -d, -f4) + local mem=$(multipass info "$vm_name" --format csv | tail -1 | cut -d, -f6) + printf "%-12s %-10s %-16s %-6s %-6s\n" "$vm_name" "$state" "${ip:-N/A}" "$cpus" "$mem" + else + printf "%-12s %-10s %-16s %-6s %-6s\n" "$vm_name" "not found" "-" "-" "-" + fi + done + + echo "" + echo -e "${BLUE}SSH Access:${NC}" + echo " multipass shell sip-1" + echo " multipass shell media-1" + echo " multipass shell pbx-1" +} + +cmd_ssh() { + check_multipass + local vm_name="${1:-}" + if [[ -z "$vm_name" ]]; then + echo "Usage: $0 ssh " + echo "Available VMs: sip-1, media-1, pbx-1" + exit 1 + fi + multipass shell "$vm_name" +} + +cmd_inventory() { + check_multipass + info "Generating Ansible inventory..." + + local inventory_file="${SCRIPT_DIR}/../ansible/inventory/multipass.yml" + + cat > "$inventory_file" << 'HEADER' +# Ansible inventory for Multipass VMs +# Auto-generated by multipass-vms.sh +# +# Usage: +# ansible-playbook -i inventory/multipass.yml playbooks/provision-vms.yml + +all: + vars: + ansible_user: ubuntu + ansible_python_interpreter: /usr/bin/python3 + + # DevStack Core integration (host machine) + # Multipass VMs can reach host via the bridge IP + devstack_host: "{{ ansible_default_ipv4.gateway }}" + vault_addr: "http://{{ devstack_host }}:8200" + postgres_host: "{{ devstack_host }}" + redis_host: "{{ devstack_host }}" + rabbitmq_host: "{{ devstack_host }}" + + # Domain configuration + domain: sip.local + + children: + voip: + children: + sip_proxies: + hosts: +HEADER + + # Add sip-1 + if multipass info sip-1 &>/dev/null; then + local sip_ip=$(multipass info sip-1 --format csv | tail -1 | cut -d, -f3) + cat >> "$inventory_file" << EOF + sip-1: + ansible_host: $sip_ip + internal_ip: $sip_ip + has_external_interface: true + opensips_enabled: true + kamailio_enabled: false + +EOF + fi + + cat >> "$inventory_file" << 'MEDIA_HEADER' + media_servers: + hosts: +MEDIA_HEADER + + # Add media-1 + if multipass info media-1 &>/dev/null; then + local media_ip=$(multipass info media-1 --format csv | tail -1 | cut -d, -f3) + cat >> "$inventory_file" << EOF + media-1: + ansible_host: $media_ip + internal_ip: $media_ip + has_external_interface: true + rtpengine_enabled: true + +EOF + fi + + cat >> "$inventory_file" << 'PBX_HEADER' + pbx: + hosts: +PBX_HEADER + + # Add pbx-1 + if multipass info pbx-1 &>/dev/null; then + local pbx_ip=$(multipass info pbx-1 --format csv | tail -1 | cut -d, -f3) + cat >> "$inventory_file" << EOF + pbx-1: + ansible_host: $pbx_ip + internal_ip: $pbx_ip + has_external_interface: false + asterisk_enabled: true + freeswitch_enabled: false +EOF + fi + + success "Generated $inventory_file" + cat "$inventory_file" +} + +cmd_help() { + cat << 'EOF' +voip-stack VM Manager (Multipass) + +Usage: ./scripts/multipass-vms.sh [options] + +Commands: + create Create all VMs (sip-1, media-1, pbx-1) + destroy Destroy all VMs (with confirmation) + start Start all VMs + stop Stop all VMs + status Show VM status and IP addresses + ssh SSH into specific VM + inventory Generate Ansible inventory file + help Show this help message + +Examples: + ./scripts/multipass-vms.sh create + ./scripts/multipass-vms.sh status + ./scripts/multipass-vms.sh ssh sip-1 + ./scripts/multipass-vms.sh inventory + +VM Specifications: + sip-1: 2 CPUs, 2GB RAM, 20GB disk (OpenSIPS) + media-1: 4 CPUs, 4GB RAM, 20GB disk (RTPEngine) + pbx-1: 2 CPUs, 4GB RAM, 20GB disk (Asterisk) + +After creating VMs, generate inventory and run Ansible: + ./scripts/multipass-vms.sh inventory + ansible-playbook -i ansible/inventory/multipass.yml ansible/playbooks/provision-vms.yml + +EOF +} + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Main +main() { + local command="${1:-help}" + shift || true + + case "$command" in + create) cmd_create ;; + destroy) cmd_destroy ;; + start) cmd_start ;; + stop) cmd_stop ;; + status) cmd_status ;; + ssh) cmd_ssh "$@" ;; + inventory) cmd_inventory ;; + help|--help|-h) cmd_help ;; + *) + error "Unknown command: $command. Use 'help' for usage." + ;; + esac +} + +main "$@"