added script to launch alpine vm with cloud hypervisor + update readme. Thanks @jan for script
This commit is contained in:
parent
798c2d3d32
commit
59583124a8
75
README.md
75
README.md
@ -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
399
alpine-boot.sh
Executable 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 "$@"
|
Loading…
Reference in New Issue
Block a user