added script to launch alpine vm with cloud hypervisor + update readme. Thanks @jan for script

This commit is contained in:
Maxime Van Hees 2025-07-31 15:53:19 +02:00
parent 798c2d3d32
commit 59583124a8
2 changed files with 455 additions and 19 deletions

View File

@ -1,29 +1,66 @@
# Hetzner Robot API Rhai Client
# Hetzner Robot API Rhai Client & Local VM Orchestrator
This project provides a Rhai scripting client for interacting with the Hetzner Robot API. It allows users to manage their Hetzner servers, SSH keys, boot configurations, and order new servers and addons directly from Rhai scripts.
This project provides two core functionalities:
1. A **Rhai scripting client** to programmatically manage Hetzner dedicated servers via the Robot API.
2. A **local VM orchestrator** (`alpine-boot.sh`) to quickly set up and run a lightweight Alpine Linux virtual machine on any host, ideal for development and testing.
## Goal
This allows you to use the Rhai scripts to order a new server from Hetzner, and then use the `alpine-boot.sh` script on that new server to stand up a local virtualized environment.
The primary goal of this project is to offer a flexible and scriptable interface to the Hetzner Robot API, enabling automation of server management tasks. By leveraging Rhai, users can write simple yet powerful scripts to interact with their Hetzner infrastructure.
## 1. Hetzner Rhai Client
## Examples
The primary goal of this component is to offer a flexible and scriptable interface to the Hetzner Robot API, enabling automation of server management tasks. By leveraging Rhai, users can write simple yet powerful scripts to interact with their Hetzner infrastructure.
### Rhai Examples
The `examples/` directory contains several Rhai scripts demonstrating the capabilities of this client:
- [`examples/server_management.rhai`](examples/server_management.rhai): Showcases basic server management operations, such as fetching server details and updating server names.
- [`examples/ssh_key_management.rhai`](examples/ssh_key_management.rhai): Demonstrates how to list, add, update, and delete SSH keys.
- [`examples/boot_management.rhai`](examples/boot_management.rhai): Provides examples for managing server boot configurations, including rescue mode.
- [`examples/server_ordering.rhai`](examples/server_ordering.rhai): Contains comprehensive examples for ordering new servers and managing server addons, including:
- Getting available server products.
- Ordering new servers.
- Listing server order transactions.
- Fetching specific transaction details.
- Listing and ordering auction servers.
- Getting available server addon products.
- Listing server addon transactions.
- Ordering server addons.
- Querying specific server addon transactions.
- [`examples/server_management.rhai`](examples/server_management.rhai): Showcases basic server management operations.
- [`examples/ssh_key_management.rhai`](examples/ssh_key_management.rhai): Demonstrates how to manage SSH keys.
- [`examples/boot_management.rhai`](examples/boot_management.rhai): Provides examples for managing server boot configurations.
- [`examples/server_ordering.rhai`](examples/server_ordering.rhai): Contains comprehensive examples for ordering new servers and addons.
## 2. Local VM Setup with `alpine-boot.sh`
For local development and testing, this project includes a script to quickly boot a lightweight Alpine Linux virtual machine using `cloud-hypervisor` and `virtiofs`. The [`alpine-boot.sh`](alpine-boot.sh) script automates the entire process, from downloading the OS to configuring and launching the VM.
### Prerequisites
On a fresh Debian-based system (like an Ubuntu server ordered from Hetzner), you must first install the required dependencies. These commands only need to be run once.
```bash
# Install dependencies from apt
sudo apt update && sudo apt install -y curl tar virtiofsd
# Download and install a static cloud-hypervisor binary
wget https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v47.0/cloud-hypervisor-static
chmod +x cloud-hypervisor-static
sudo ln -s "$(pwd)/cloud-hypervisor-static" /usr/local/bin/cloud-hypervisor
```
*Note: The `cloud-hypervisor` version is pinned for stability.*
### Script Usage
#### Basic Boot
To download all assets and boot the VM with default settings (hostname `alpine-3.22`, root password `root`):
```bash
./alpine-boot.sh
```
#### Customizing the VM
You can customize the VM using command-line options:
- `--hostname <name>`: Set a custom hostname.
- `--root-password <password>`: Set a custom root password.
**Example:**
```bash
./alpine-boot.sh --hostname my-alpine --root-password mysecretpassword
```
#### Other Commands
- `download-only`: Downloads assets without booting.
- `clean`: Removes the workspace and all downloaded files.
- `help`: Displays the help message.
## Important Note on IPv6 Addresses
When ordering a server without an IPv4 addon, the server might be assigned an IPv6 network like `2a01:4f8:221:1fe3::/64`. It's important to note that you cannot directly SSH to this network address. Hetzner does not use SLAAC for server assignments. The actual address to SSH to will typically be the network address with `2` appended (e.g., `2a01:4f8:221:1fe3::2`).
When ordering a Hetzner server without an IPv4 addon, it might be assigned an IPv6 network like `2a01:4f8:221:1fe3::/64`. You cannot directly SSH to this network address. Hetzner does not use SLAAC for server assignments. The actual address to SSH to will typically be the network address with `2` appended (e.g., `2a01:4f8:221:1fe3::2`).

399
alpine-boot.sh Executable file
View File

@ -0,0 +1,399 @@
#!/bin/bash
# Alpine Linux Boot Script with Cloud-Hypervisor and VirtioFS
# This script downloads Alpine Linux miniroot, kernel, and initrd,
# then boots using cloud-hypervisor with virtiofs
set -euo pipefail
# Configuration
ALPINE_VERSION="3.22"
ALPINE_ARCH="x86_64"
ALPINE_MIRROR="https://dl-cdn.alpinelinux.org/alpine"
WORK_DIR="$(pwd)/alpine-boot"
MINIROOT_DIR="$WORK_DIR/miniroot"
KERNEL_DIR="$WORK_DIR/kernel"
# Default hostname
DEFAULT_HOSTNAME="alpine-$ALPINE_VERSION"
HOSTNAME="$DEFAULT_HOSTNAME"
# Root password (use --root-password to override)
ROOT_PASSWORD="root"
# URLs
ALPINE_BASE_URL="$ALPINE_MIRROR/v$ALPINE_VERSION/releases/$ALPINE_ARCH"
MINIROOT_URL="$ALPINE_BASE_URL/alpine-minirootfs-$ALPINE_VERSION.0-$ALPINE_ARCH.tar.gz"
NETBOOT_URL="$ALPINE_BASE_URL/netboot-$ALPINE_VERSION.0"
VMLINUZ_URL="$NETBOOT_URL/vmlinuz-virt"
INITRAMFS_URL="$NETBOOT_URL/initramfs-virt"
# Cloud-hypervisor configuration
VM_MEMORY="1024M"
VM_CPUS="2"
KERNEL_CMDLINE="console=ttyS0 rootfstype=virtiofs root=/dev/root debug rw init=/sbin/init debug_init"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
# Find virtiofsd location
find_virtiofsd() {
# Check common locations for virtiofsd
local virtiofsd_paths=(
"virtiofsd" # In PATH
"/usr/lib/virtiofsd" # Arch Linux
"/usr/libexec/virtiofsd" # Some distributions
"/usr/bin/virtiofsd" # Standard location
)
for path in "${virtiofsd_paths[@]}"; do
if [[ "$path" == "virtiofsd" ]]; then
# Check if it's in PATH
if command -v "$path" >/dev/null 2>&1; then
echo "$path"
return 0
fi
else
# Check if file exists for absolute paths
if [[ -f "$path" && -x "$path" ]]; then
echo "$path"
return 0
fi
fi
done
return 1
}
# Check dependencies
check_dependencies() {
log "Checking dependencies..."
local missing_deps=()
command -v curl >/dev/null 2>&1 || missing_deps+=("curl")
command -v tar >/dev/null 2>&1 || missing_deps+=("tar")
command -v cloud-hypervisor >/dev/null 2>&1 || missing_deps+=("cloud-hypervisor")
# Check for virtiofsd
if ! VIRTIOFSD_CMD=$(find_virtiofsd); then
missing_deps+=("virtiofsd")
else
log "Found virtiofsd at: $VIRTIOFSD_CMD"
fi
if [ ${#missing_deps[@]} -ne 0 ]; then
error "Missing dependencies: ${missing_deps[*]}"
fi
log "All dependencies found."
}
# Create working directories
setup_directories() {
log "Setting up directories..."
mkdir -p "$WORK_DIR" "$MINIROOT_DIR" "$KERNEL_DIR"
}
# Download and extract miniroot
download_miniroot() {
log "Downloading Alpine Linux miniroot..."
local miniroot_file="$WORK_DIR/alpine-minirootfs.tar.gz"
if [ ! -f "$miniroot_file" ]; then
curl -L -o "$miniroot_file" "$MINIROOT_URL" || error "Failed to download miniroot"
else
log "Miniroot already downloaded, skipping..."
fi
log "Extracting miniroot..."
if [ ! -d "$MINIROOT_DIR/bin" ]; then
tar -xzf "$miniroot_file" -C "$MINIROOT_DIR" || error "Failed to extract miniroot"
else
log "Miniroot already extracted, skipping..."
fi
}
# Download kernel and initramfs
download_kernel() {
log "Downloading Alpine Linux kernel and initramfs..."
local vmlinuz_file="$KERNEL_DIR/vmlinuz"
local initramfs_file="$KERNEL_DIR/initramfs"
# Download vmlinuz
if [ ! -f "$vmlinuz_file" ]; then
log "Downloading vmlinuz..."
curl -L -o "$vmlinuz_file" "$VMLINUZ_URL" || error "Failed to download vmlinuz"
else
log "vmlinuz already downloaded, skipping..."
fi
# Download initramfs
if [ ! -f "$initramfs_file" ]; then
log "Downloading initramfs..."
curl -L -o "$initramfs_file" "$INITRAMFS_URL" || error "Failed to download initramfs"
else
log "initramfs already downloaded, skipping..."
fi
# Verify files
if [ ! -f "$vmlinuz_file" ] || [ ! -s "$vmlinuz_file" ]; then
error "vmlinuz file is missing or empty"
fi
if [ ! -f "$initramfs_file" ] || [ ! -s "$initramfs_file" ]; then
error "initramfs file is missing or empty"
fi
}
# Customize miniroot with chroot
customize_miniroot() {
log "Customizing miniroot with additional packages and configuration..."
# Set up chroot environment
log "Setting up chroot environment..."
sudo mount --bind /proc "$MINIROOT_DIR/proc" || error "Failed to bind mount /proc"
sudo mount --bind /sys "$MINIROOT_DIR/sys" || error "Failed to bind mount /sys"
sudo mount --bind /dev "$MINIROOT_DIR/dev" || error "Failed to bind mount /dev"
# Cleanup function for chroot environment
cleanup_chroot() {
log "Cleaning up chroot environment..."
sudo umount "$MINIROOT_DIR/proc" 2>/dev/null || true
sudo umount "$MINIROOT_DIR/sys" 2>/dev/null || true
sudo umount "$MINIROOT_DIR/dev" 2>/dev/null || true
}
trap cleanup_chroot EXIT
# Add hvc0 getty to /etc/inittab
log "Adding hvc0 getty to /etc/inittab..."
if ! grep -q "hvc0::respawn" "$MINIROOT_DIR/etc/inittab" 2>/dev/null; then
sudo chroot "$MINIROOT_DIR" /bin/sh -c "echo 'hvc0::respawn:/sbin/getty -L hvc0 115200 vt100' >> /etc/inittab" || error "Failed to add hvc0 getty"
else
log "hvc0 getty already configured, skipping..."
fi
# Configure nameserver
log "Configuring nameserver..."
sudo chroot "$MINIROOT_DIR" /bin/sh -c "echo 'nameserver 1.1.1.1' > /etc/resolv.conf" || error "Failed to configure nameserver"
# Configure hostname (removed from here, will be set separately)
# Update package database and install packages
log "Updating package database..."
sudo chroot "$MINIROOT_DIR" /bin/sh -c "apk update" || error "Failed to update package database"
log "Installing openrc, bash, openssh..."
sudo chroot "$MINIROOT_DIR" /bin/sh -c "apk add openrc bash openssh" || error "Failed to install packages"
# Enable services
log "Enabling services (sshd, networking)..."
sudo chroot "$MINIROOT_DIR" /bin/sh -c "rc-update add sshd default" || warn "Failed to enable sshd service"
sudo chroot "$MINIROOT_DIR" /bin/sh -c "rc-update add networking boot" || warn "Failed to enable networking service"
# Set root password
log "Setting root password..."
warn "Setting default root password. Use --root-password to override. For production, use SSH keys."
sudo chroot "$MINIROOT_DIR" /bin/sh -c "echo 'root:$ROOT_PASSWORD' | chpasswd" || error "Failed to set root password"
# Enable root SSH login
log "Enabling root SSH login with password..."
if [ -f "$MINIROOT_DIR/etc/ssh/sshd_config" ]; then
sudo sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' "$MINIROOT_DIR/etc/ssh/sshd_config" || warn "Could not enable root SSH login"
else
warn "sshd_config not found, skipping SSH configuration."
fi
# Clean up chroot environment
cleanup_chroot
trap - EXIT
log "Miniroot customization complete."
}
# Set hostname in miniroot
set_hostname() {
log "Setting hostname to: $HOSTNAME"
sudo chroot "$MINIROOT_DIR" /bin/sh -c "echo '$HOSTNAME' > /etc/hostname" || error "Failed to set hostname"
sudo chroot "$MINIROOT_DIR" /bin/sh -c "echo '127.0.0.1 localhost $HOSTNAME' > /etc/hosts" || error "Failed to configure hosts file"
}
# Prepare miniroot for virtiofs
prepare_miniroot() {
log "Preparing miniroot for virtiofs..."
# Ensure necessary directories exist
mkdir -p "$MINIROOT_DIR/dev" "$MINIROOT_DIR/proc" "$MINIROOT_DIR/sys" "$MINIROOT_DIR/tmp"
# Customize miniroot if not already done
# Use a versioned marker file to ensure re-customization when script logic changes.
if [ ! -f "$MINIROOT_DIR/.customized_v2" ]; then
customize_miniroot
# Mark as customized
rm -f "$MINIROOT_DIR/.customized" # remove old marker
touch "$MINIROOT_DIR/.customized_v2"
else
log "Miniroot already customized, skipping..."
fi
# Always set hostname (even if already customized)
set_hostname
}
# Boot with cloud-hypervisor
boot_vm() {
log "Booting Alpine Linux with cloud-hypervisor..."
# Clean up stale sockets from previous runs to prevent "Address in use" errors
log "Cleaning up any stale sockets..."
rm -f /tmp/ch-virtiofs.sock /tmp/ch-api.sock
local kernel_file="$KERNEL_DIR/vmlinuz"
local initramfs_file="$KERNEL_DIR/initramfs"
if [ ! -f "$kernel_file" ]; then
error "Kernel file not found: $kernel_file"
fi
# Build cloud-hypervisor command
local ch_cmd=(
"cloud-hypervisor"
"--memory" "size=$VM_MEMORY,shared=on"
"--cpus" "boot=$VM_CPUS"
"--kernel" "$kernel_file"
"--cmdline" "$KERNEL_CMDLINE"
"--fs" "tag=/dev/root,socket=/tmp/ch-virtiofs.sock,num_queues=1"
"--serial" "tty"
"--api-socket" "/tmp/ch-api.sock"
)
# Add initramfs if available
if [ -f "$initramfs_file" ]; then
ch_cmd+=("--initramfs" "$initramfs_file")
fi
log "Starting virtiofsd..."
# Start virtiofsd in background
"$VIRTIOFSD_CMD" --socket-path=/tmp/ch-virtiofs.sock --shared-dir="$MINIROOT_DIR" --announce-submounts &
local virtiofsd_pid=$!
# Give virtiofsd time to start
sleep 2
log "Starting cloud-hypervisor..."
log "Command: ${ch_cmd[*]}"
# Cleanup function
cleanup() {
log "Cleaning up..."
kill $virtiofsd_pid 2>/dev/null || true
rm -f /tmp/ch-virtiofs.sock /tmp/ch-api.sock
}
trap cleanup EXIT
# Execute cloud-hypervisor
"${ch_cmd[@]}" || error "Failed to start cloud-hypervisor"
}
# Main execution
main() {
log "Alpine Linux Boot Script Starting..."
check_dependencies
setup_directories
download_miniroot
download_kernel
prepare_miniroot
boot_vm
}
# Main entry point with argument parsing
handle_args() {
# Default action
local action="boot"
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--hostname)
[[ -z "$2" ]] && error "Missing value for --hostname"
HOSTNAME="$2"
shift 2
;;
--root-password)
[[ -z "$2" ]] && error "Missing value for --root-password"
ROOT_PASSWORD="$2"
shift 2
;;
clean|download-only)
action="$1"
shift
;;
help|--help|-h)
action="help"
shift
;;
*)
error "Unknown option or command: $1"
;;
esac
done
# Execute action
case "$action" in
boot)
log "Using hostname: $HOSTNAME"
main
;;
clean)
log "Cleaning up work directory..."
sudo rm -rf "$WORK_DIR"
log "Cleanup complete."
;;
download-only)
log "Download-only mode..."
check_dependencies
setup_directories
download_miniroot
download_kernel
log "Downloads complete."
;;
help)
echo "Usage: $0 [OPTIONS] [COMMAND]"
echo ""
echo "OPTIONS:"
echo " --hostname HOSTNAME Set VM hostname (default: $DEFAULT_HOSTNAME)"
echo " --root-password PWD Set root password for the VM (default: 'root')"
echo ""
echo "COMMANDS:"
echo " clean Remove downloaded files and work directory"
echo " download-only Only download files, don't boot"
echo " help Show this help message"
echo ""
echo "Examples:"
echo " $0 Boot with default hostname"
echo " $0 --hostname myserver Boot with hostname 'myserver'"
echo " $0 --root-password secret boot with custom password"
;;
esac
}
handle_args "$@"