Compare commits

...

4 Commits

10 changed files with 474 additions and 342 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Ignore everything in .vm directory
.vm/*
# But keep .gitkeep
!.vm/.gitkeep
# Ignore result in iso directory
iso/result

157
.roo/rules/rules.md Normal file
View File

@@ -0,0 +1,157 @@
# Cloonar Assistant LLM Rules
This document defines the rules and guidelines for an LLM working with the Cloonar Assistant NixOS module project.
## 1. Project Understanding
### 1.1 Core Components
- Network Infrastructure (VLANs, DHCP, DNS, Firewall)
- Security Services (WireGuard VPN, SSL/ACME)
- Home Automation (Home Assistant)
- System Services (Dynamic DNS, Container Management)
- Development Tools (ISO Builder, VM Testing)
### 1.2 Module Architecture
```mermaid
graph TD
A[Cloonar Assistant] --> B[Network Management]
A --> C[Security]
A --> D[Services]
A --> E[Development Tools]
B --> B1[VLANs]
B --> B2[DHCP/Kea]
B --> B3[DNS/Unbound]
B --> B4[Firewall/nftables]
C --> C1[WireGuard VPN]
C --> C2[SSL/ACME]
C --> C3[SOPS Integration]
D --> D1[Home Assistant]
D --> D2[Dynamic DNS]
D --> D3[Container Services]
E --> E1[ISO Builder]
E --> E2[VM Testing]
```
## 2. NixOS Integration (REQUIRED)
### 2.1 Package and Option Verification
- ALWAYS use the NixOS MCP server to verify packages and options before suggesting them
- Query format: `use_mcp_tool` with server "nixos" for all NixOS-related lookups
- Verify package availability in the project's current NixOS version
- Validate option compatibility and deprecation status
### 2.2 Configuration Guidelines
- All NixOS configurations must be validated through MCP before suggestion
- Use proper module imports and option declarations
- Follow NixOS naming conventions and type declarations
- Consider module dependencies and conflicts
## 3. Development Guidelines
### 3.1 Code Structure
- Maintain modular organization in `modules/cloonar-assistant/`
- Follow existing patterns for option declarations
- Use descriptive names for options and properties
- Keep related functionality grouped in appropriate submodules
### 3.2 Testing Protocol
- Utilize the VM testing scripts in `scripts/`
- Test configuration changes with `test-configuration`
- Verify VLAN and network functionality in VM environment
- Ensure proper service container operation
## 4. Configuration Support
### 4.1 Network Configuration
- Define appropriate VLANs based on network requirements
- Configure firewall rules using nftables syntax
- Set up proper DHCP and DNS services
- Implement correct routing between VLANs
### 4.2 Service Configuration
- Configure Home Assistant container with proper isolation
- Set up SSL certificates via ACME
- Manage WireGuard VPN peers and configurations
- Configure dynamic DNS updates
## 5. Security Best Practices
### 5.1 General Security
- Never expose sensitive information in configurations
- Use SOPS for secrets management
- Implement proper network segmentation
- Follow principle of least privilege
### 5.2 Network Security
- Verify firewall rule correctness
- Implement proper VLAN isolation
- Secure VPN configurations
- Validate SSL certificate management
## 6. Troubleshooting Framework
### 6.1 Diagnostic Approach
```mermaid
flowchart LR
A[Issue Reported] --> B{Category?}
B --> C[Network]
B --> D[Service]
B --> E[Build]
C --> C1[Check VLANs]
C --> C2[Verify Firewall]
C --> C3[Test DNS]
D --> D1[Container Status]
D --> D2[Service Logs]
D --> D3[Dependencies]
E --> E1[Nix Errors]
E --> E2[Option Issues]
E --> E3[Build Logs]
```
### 6.2 Common Issues
- VLAN connectivity problems
- Container networking issues
- SSL certificate renewal failures
- Build and configuration errors
## 7. Self-Maintenance Rules
### 7.1 Rule Update Triggers
Monitor and update rules.md when:
- Major NixOS version changes affect module functionality
- New security considerations emerge
- Core module features are added/modified
- Breaking changes in dependencies occur
### 7.2 Update Protocol
1. Identify breaking changes or important updates
2. Document impact on existing configurations
3. Update relevant rule sections
4. Add new guidelines if needed
5. Update version compatibility information
### 7.3 Documentation Sync
- Keep rules aligned with current codebase
- Update mermaid diagrams for architectural changes
- Maintain accurate NixOS version compatibility info
- Document new features and deprecations
### 7.4 Change Validation
Before updating rules:
- Verify changes against current codebase
- Test impact on existing configurations
- Check NixOS MCP for option/package changes
- Validate security implications
## 8. Version Information
- Last Updated: 2025-06-06
- Compatible NixOS Versions: 23.05, 23.11, 24.05
- Project Version: Current Master

12
candle-sensor Normal file
View File

@@ -0,0 +1,12 @@
it does not work if the sensor is underneath
i could build a device which you can put on the table and looks to the candles
it has 80cm of recognizing a flame.
it would use the following components
Adafruit ESP32 Feather V2
ANGEEK KY-026 Flame Sensor Module
2000 - 3000 mah LiPo
battery life would be 20 Days, cost would be around 35€ just plain devices cost without work time
3d printed housing would be needed

1
example/channel Normal file
View File

@@ -0,0 +1 @@
https://channels.nixos.org/nixos-25.05

View File

@@ -1,9 +1,8 @@
{ ... }: {
imports = [
./interfaces.nix
./dhcp.nix
./firewall.nix
./unbound.nix
./dnsmasq.nix # New dnsmasq configuration
./wireguard.nix
];
}

View File

@@ -1,187 +0,0 @@
{ config, lib, ... }:
{
services.kea.dhcp4 = lib.mkIf config.cloonar-assistant.firewall.enable {
enable = true;
settings = {
interfaces-config = {
interfaces = [
"lan"
"server"
"infrastructure"
"multimedia"
"smart"
"guest"
];
};
lease-database = {
name = "/var/lib/kea/dhcp4.leases";
persist = true;
type = "memfile";
};
rebind-timer = 2000;
renew-timer = 1000;
subnet4 = [
{
id = 96;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.96.100 - ${config.cloonar-assistant.networkPrefix}.96.240";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.96.0/24";
interface = "lan";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.96.1";
}
{
name = "domain-name";
data = config.cloonar-assistant.domain;
}
{
name = "domain-search";
data = config.cloonar-assistant.domain;
}
{
name = "domain-name-servers";
data = "${config.cloonar-assistant.networkPrefix}.96.1";
}
];
reservations = [
];
}
{
id = 97;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.97.100 - ${config.cloonar-assistant.networkPrefix}.97.240";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.97.0/24";
interface = "server";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.97.1";
}
{
name = "domain-name";
data = config.cloonar-assistant.domain;
}
{
name = "domain-name-servers";
data = "${config.cloonar-assistant.networkPrefix}.97.1";
}
];
reservations = [
];
}
{
id = 101;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.101.100 - ${config.cloonar-assistant.networkPrefix}.101.240";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.101.0/24";
interface = "infrastructure";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.101.1";
}
{
name = "domain-name";
data = config.cloonar-assistant.domain;
}
{
name = "domain-name-servers";
data = "${config.cloonar-assistant.networkPrefix}.101.1";
}
{
name = "capwap-ac-v4";
code = 138;
data = "${config.cloonar-assistant.networkPrefix}.97.2";
}
];
reservations = [
];
}
{
id = 99;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.99.100 - ${config.cloonar-assistant.networkPrefix}.99.240";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.99.0/24";
interface = "multimedia";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.99.1";
}
{
name = "domain-name";
data = config.cloonar-assistant.domain;
}
{
name = "domain-name-servers";
data = "${config.cloonar-assistant.networkPrefix}.99.1";
}
];
reservations = [
];
}
{
id = 254;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.254.10 - ${config.cloonar-assistant.networkPrefix}.254.254";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.254.0/24";
interface = "guest";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.254.1";
}
{
name = "domain-name-servers";
data = "9.9.9.9";
}
];
}
{
id = 100;
pools = [
{
pool = "${config.cloonar-assistant.networkPrefix}.100.100 - ${config.cloonar-assistant.networkPrefix}.100.240";
}
];
subnet = "${config.cloonar-assistant.networkPrefix}.100.0/24";
interface = "smart";
option-data = [
{
name = "routers";
data = "${config.cloonar-assistant.networkPrefix}.100.1";
}
{
name = "domain-name";
data = config.cloonar-assistant.domain;
}
{
name = "domain-name-servers";
data = "${config.cloonar-assistant.networkPrefix}.100.1";
}
];
reservations = [
];
}
];
valid-lifetime = 4000;
};
};
}

View File

@@ -0,0 +1,98 @@
{ config, lib, ... }:
{
# Disable systemd-resolved (same as current unbound.nix)
services.resolved.enable = false;
# Main dnsmasq service with preserved conditional enablement
services.dnsmasq = lib.mkIf config.cloonar-assistant.firewall.enable {
enable = true;
resolveLocalQueries = false; # We handle DNS manually
settings = {
# Interface binding
interface = [
"lan"
"server"
"infrastructure"
"multimedia"
"smart"
"guest"
];
# DHCP ranges per VLAN
dhcp-range = [
"${config.cloonar-assistant.networkPrefix}.96.100,${config.cloonar-assistant.networkPrefix}.96.240,24h"
"${config.cloonar-assistant.networkPrefix}.97.100,${config.cloonar-assistant.networkPrefix}.97.240,24h"
"${config.cloonar-assistant.networkPrefix}.101.100,${config.cloonar-assistant.networkPrefix}.101.240,24h"
"${config.cloonar-assistant.networkPrefix}.99.100,${config.cloonar-assistant.networkPrefix}.99.240,24h"
"${config.cloonar-assistant.networkPrefix}.100.100,${config.cloonar-assistant.networkPrefix}.100.240,24h"
"${config.cloonar-assistant.networkPrefix}.254.10,${config.cloonar-assistant.networkPrefix}.254.254,24h"
];
# DHCP options with VLAN tagging
dhcp-option = [
# LAN VLAN (.96)
"tag:lan,option:router,${config.cloonar-assistant.networkPrefix}.96.1"
"tag:lan,option:dns-server,${config.cloonar-assistant.networkPrefix}.96.1"
"tag:lan,option:domain-name,${config.cloonar-assistant.domain}"
# Server VLAN (.97)
"tag:server,option:router,${config.cloonar-assistant.networkPrefix}.97.1"
"tag:server,option:dns-server,${config.cloonar-assistant.networkPrefix}.97.1"
"tag:server,option:domain-name,${config.cloonar-assistant.domain}"
# Infrastructure VLAN (.101) with CAPWAP option
"tag:infrastructure,option:router,${config.cloonar-assistant.networkPrefix}.101.1"
"tag:infrastructure,option:dns-server,${config.cloonar-assistant.networkPrefix}.101.1"
"tag:infrastructure,option:domain-name,${config.cloonar-assistant.domain}"
"tag:infrastructure,138,${config.cloonar-assistant.networkPrefix}.97.2" # CAPWAP
# Multimedia VLAN (.99)
"tag:multimedia,option:router,${config.cloonar-assistant.networkPrefix}.99.1"
"tag:multimedia,option:dns-server,${config.cloonar-assistant.networkPrefix}.99.1"
"tag:multimedia,option:domain-name,${config.cloonar-assistant.domain}"
# Smart VLAN (.100)
"tag:smart,option:router,${config.cloonar-assistant.networkPrefix}.100.1"
"tag:smart,option:dns-server,${config.cloonar-assistant.networkPrefix}.100.1"
"tag:smart,option:domain-name,${config.cloonar-assistant.domain}"
# Guest VLAN (.254) - DNS isolation
"tag:guest,option:router,${config.cloonar-assistant.networkPrefix}.254.1"
"tag:guest,option:dns-server,9.9.9.9" # External DNS only
];
# Static DNS records
address = [
"/fw.${config.cloonar-assistant.domain}/${config.cloonar-assistant.networkPrefix}.97.1"
"/fw/${config.cloonar-assistant.networkPrefix}.97.1"
"/home-assistant.${config.cloonar-assistant.domain}/${config.cloonar-assistant.networkPrefix}.97.20"
"/mopidy.${config.cloonar-assistant.domain}/${config.cloonar-assistant.networkPrefix}.97.21"
"/snapcast.${config.cloonar-assistant.domain}/${config.cloonar-assistant.networkPrefix}.97.21"
"/localhost/127.0.0.1"
"/localhost.${config.cloonar-assistant.domain}/127.0.0.1"
];
# Domain configuration
domain = "${config.cloonar-assistant.domain}";
expand-hosts = true;
# Upstream DNS servers (plain DNS, no DoT support in dnsmasq)
server = [
"9.9.9.9"
"149.112.112.11"
];
# Performance and security
cache-size = 1000;
neg-ttl = 60;
domain-needed = true; # Don't forward plain names
bogus-priv = true; # Don't forward RFC1918 reverse lookups
bind-interfaces = true; # Only bind to specified interfaces
};
};
# Firewall configuration (preserve existing)
networking.firewall.allowedUDPPorts = [ 53 5353 ];
}

View File

@@ -1,138 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = {
remote-control.control-enable = true;
server = {
interface = [ "0.0.0.0" "::0" ];
interface-automatic = "yes";
access-control = [
"127.0.0.0/8 allow"
"${config.cloonar-assistant.networkPrefix}.96.0/24 allow"
"${config.cloonar-assistant.networkPrefix}.97.0/24 allow"
"${config.cloonar-assistant.networkPrefix}.98.0/24 allow"
"${config.cloonar-assistant.networkPrefix}.99.0/24 allow"
"${config.cloonar-assistant.networkPrefix}.101.0/24 allow"
"0.0.0.0/0 allow"
];
tls-cert-bundle = "/etc/ssl/certs/ca-certificates.crt";
local-zone = "\"${config.cloonar-assistant.domain}\" transparent";
local-data = [
"\"localhost A 127.0.0.1\""
"\"localhost.${config.cloonar-assistant.domain} A 127.0.0.1\""
"\"localhost AAAA ::1\""
"\"localhost.${config.cloonar-assistant.domain} AAAA ::1\""
"\"fw.${config.cloonar-assistant.domain} A ${config.cloonar-assistant.networkPrefix}.97.1\""
"\"fw A ${config.cloonar-assistant.networkPrefix}.97.1\""
"\"mopidy.${config.cloonar-assistant.domain} IN A ${config.cloonar-assistant.networkPrefix}.97.21\""
"\"snapcast.${config.cloonar-assistant.domain} IN A ${config.cloonar-assistant.networkPrefix}.97.21\""
"\"home-assistant.${config.cloonar-assistant.domain} IN A ${config.cloonar-assistant.networkPrefix}.97.20\""
];
local-data-ptr = [
"\"127.0.0.1 localhost\""
"\"::1 localhost\""
"\"${config.cloonar-assistant.networkPrefix}.97.1 fw.${config.cloonar-assistant.domain}\""
"\"${config.cloonar-assistant.networkPrefix}.97.20 home-assistant.${config.cloonar-assistant.domain}\""
"\"${config.cloonar-assistant.networkPrefix}.97.21 snapcast.${config.cloonar-assistant.domain}\""
];
# ssl-upstream = "yes";
};
forward-zone = [
{
name = ".";
forward-tls-upstream = "yes";
forward-first = "no";
forward-addr = [
"9.9.9.9@853#dns9.quad9.net"
"149.112.112.11@853#dns11.quad9.net"
];
}
];
};
in {
users.users.unbound = {
group = "unbound";
isSystemUser = true;
extraGroups = [ "ssl-users" ];
};
users.groups.unbound = { };
services.resolved.enable = false;
services.unbound = {
enable = true;
settings = cfg;
};
systemd.services.unbound-sync = lib.mkIf config.cloonar-assistant.firewall.enable {
enable = true;
path = with pkgs; [ unbound inotify-tools ];
script = ''
function readFile() {
if [[ "''\$2" == "A" ]] ; then
cat "''\$1" | tail -n +2 | while IFS=, read -r address hwaddr client_id valid_lifetime expire subnet_id fqdn_fwd fqdn_rev hostname state user_context
do
echo "''\${address},''\${hostname}"
done
else
cat "''\$1" | tail -n +2 | while IFS=, read -r address duid valid_lifetime expire subnet_id pref_lifetime lease_type iaid prefix_len fqdn_fwd fqdn_rev hostname hwaddr state user_context hwtype hwaddr_source
do
echo "''\${address},''\${hostname}"
done
fi
}
function readFileUnique() {
readFile "''\$1" ''\$2 | uniq | while IFS=, read -r address hostname
do
if echo "''\${1}" | grep -Eq '.*\.(${config.cloonar-assistant.domain})'; then
echo ''\${hostname} ''\$2 ''\${address}
unbound-control local_data ''\${hostname} ''\$2 ''\${address} > /dev/null 2>&1
if [[ "''\$2" == "A" ]] ; then
echo ''\${address} | while IFS=. read -r ip0 ip1 ip2 ip3
do
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.ip4.arpa. PTR ''\${hostname} > /dev/null 2>&1
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.in-addr.arpa. PTR ''\${hostname} > /dev/null 2>&1
done
fi
else
if [[ "''\$2" == "A" ]] ; then
echo ''\${address} | while IFS=. read -r ip0 ip1 ip2 ip3
do
if [[ "''\${hostname}" != "" ]]; then
domain=${config.cloonar-assistant.domain}
if [[ "''\${hostname}" != *. ]]; then
unbound-control local_data ''\${hostname}.''\${domain} ''\$2 ''\${address} > /dev/null 2>&1
else
unbound-control local_data ''\${hostname}''\${domain} ''\$2 ''\${address} > /dev/null 2>&1
fi
fi
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.ip4.arpa. PTR ''\${hostname} > /dev/null 2>&1
unbound-control local_data ''\${ip3}.''\${ip2}.''\${ip1}.''\${ip0}.in-addr.arpa. PTR ''\${hostname} > /dev/null 2>&1
done
fi
fi
done
}
function syncFile() {
# readFileUnique "''\$1" "''\$2"
while true; do
readFileUnique "''\$1" "''\$2"
sleep 10
done
}
syncFile "/var/lib/kea/dhcp4.leases" A &
# syncFile "/var/lib/kea/dhcp6.leases" AAAA &
wait
'';
wants = [ "network-online.target" "unbound.service" ];
after = [ "network-online.target" "unbound.service" ];
partOf = [ "unbound.service" ];
wantedBy = [ "multi-user.target" ];
};
networking.firewall.allowedUDPPorts = [ 53 5353 ];
}

154
scripts/deploy-to-vm Executable file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
set -Euo pipefail
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
PROJECT_ROOT=$(readlink -f "$SCRIPT_DIR/..")
TARGET_DIR="/tmp/cloonar-config"
SSH_PORT=2222
SSH_HOST="localhost"
SSH_USER="root"
SSH_PASS="linux"
SSH_OPTIONS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ARCHIVE_PATH="/tmp/cloonar-config.tar.gz"
# Parse command line options
START_VM=0
KEEP_FILES=0
VERBOSE=0
NO_REBOOT=0
while [[ $# -gt 0 ]]; do
case $1 in
--start-vm)
START_VM=1
shift
;;
--keep-files)
KEEP_FILES=1
shift
;;
--verbose)
VERBOSE=1
shift
;;
--no-reboot)
NO_REBOOT=1
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--start-vm] [--keep-files] [--verbose] [--no-reboot]"
exit 1
;;
esac
done
log() {
echo "==> $1"
}
error() {
echo "ERROR: $1" >&2
exit 1
}
verbose() {
if [[ $VERBOSE -eq 1 ]]; then
echo "$1"
fi
}
cleanup() {
if [[ $KEEP_FILES -eq 0 ]]; then
log "Cleaning up temporary files..."
rm -f "$ARCHIVE_PATH"
sshpass -p "$SSH_PASS" ssh $SSH_OPTIONS -p $SSH_PORT $SSH_USER@$SSH_HOST "rm -rf $TARGET_DIR" || true
fi
}
trap cleanup EXIT
# Check if VM is running by testing SSH connection
check_vm_running() {
sshpass -p "$SSH_PASS" ssh $SSH_OPTIONS -p $SSH_PORT $SSH_USER@$SSH_HOST "echo 2>/dev/null" >/dev/null
}
# Wait for SSH to become available
wait_for_ssh() {
local retries=60
local wait_time=2
log "Waiting for SSH connection..."
while [[ $retries -gt 0 ]]; do
if check_vm_running; then
log "SSH connection established"
return 0
fi
verbose "Waiting... ($retries attempts remaining)"
retries=$((retries - 1))
sleep $wait_time
done
return 1
}
# Check if sshpass is installed
if ! command -v sshpass >/dev/null; then
error "sshpass is required but not installed. Please install it first."
fi
# Start VM if requested or not running
if [[ $START_VM -eq 1 ]] || ! check_vm_running; then
log "Starting VM..."
"$SCRIPT_DIR/run-vm" >/dev/null 2>&1 || error "Failed to start VM"
log "VM started, waiting 10 seconds for initial boot..."
sleep 10
wait_for_ssh || error "Failed to establish SSH connection"
fi
# Create archive of project files
log "Creating project archive..."
cd "$PROJECT_ROOT"
tar czf "$ARCHIVE_PATH" \
example/ \
modules/ \
.sops.yaml \
2>/dev/null || true
if [[ ! -f "$ARCHIVE_PATH" ]]; then
error "Failed to create archive at $ARCHIVE_PATH"
fi
# Copy files to VM
log "Copying files to VM..."
sshpass -p "$SSH_PASS" ssh $SSH_OPTIONS -p $SSH_PORT $SSH_USER@$SSH_HOST "rm -rf $TARGET_DIR; mkdir -p $TARGET_DIR" || error "Failed to create target directory"
sshpass -p "$SSH_PASS" scp $SSH_OPTIONS -P $SSH_PORT "$ARCHIVE_PATH" "$SSH_USER@$SSH_HOST:$TARGET_DIR/cloonar-config.tar.gz" || error "Failed to copy archive"
# Extract files and build configuration
log "Extracting files and building configuration..."
sshpass -p "$SSH_PASS" ssh $SSH_OPTIONS -p $SSH_PORT $SSH_USER@$SSH_HOST "cd $TARGET_DIR && \
tar xzf cloonar-config.tar.gz && \
nixos-rebuild switch \
-I nixpkgs=\$(cat example/channel)/nixexprs.tar.xz \
-I nixos-config=$TARGET_DIR/example/configuration.nix" || error "Build failed"
BUILD_EXIT=$?
if [[ $BUILD_EXIT -eq 0 ]]; then
log "Configuration built and activated successfully!"
if [[ $NO_REBOOT -eq 0 ]]; then
log "Rebooting VM..."
sshpass -p "$SSH_PASS" ssh $SSH_OPTIONS -p $SSH_PORT $SSH_USER@$SSH_HOST "systemctl reboot"
log "Waiting for VM to reboot..."
sleep 30
wait_for_ssh || error "Failed to reconnect after reboot"
log "VM is back online"
fi
else
error "Build failed with exit code $BUILD_EXIT"
fi
if [[ $KEEP_FILES -eq 1 ]]; then
log "Files kept in $TARGET_DIR on VM"
fi

View File

@@ -2,12 +2,11 @@
set -Euo pipefail
# -----------------------------------------------------------------------------
# This script sets up and launches a QEMU virtual machine with OVMF (UEFI).
# It checks for the necessary files, creates directories/images as needed,
# and provides clear, user-friendly output along the way.
# This script sets up and launches (or stops) a QEMU virtual machine with OVMF.
#
# Usage:
# ./run-vm.sh [install]
# - Pass "install" to attach the ISO as a CD-ROM for installation.
# ./run-vm.sh [install] # starts (and backgrounds) the VM; use "install" to attach the ISO
# ./run-vm.sh stop # kills the running QEMU VM (reads PID from .vm/qemu.pid)
# -----------------------------------------------------------------------------
# Paths to OVMF firmware (pflash)
@@ -17,9 +16,33 @@ OVMF_VARS_DEFAULT="/run/libvirt/nix-ovmf/OVMF_VARS.fd"
# Determine where this script lives and compute related paths
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
TARGET_DIR=$(readlink -f "$SCRIPT_DIR/../.vm")
OVMF_VARS_PATH=$(readlink -f "$SCRIPT_DIR/../.vm/OVMF_VARS-myvm.fd")
IMG_PATH=$(readlink -f "$SCRIPT_DIR/../.vm/disk.img")
OVMF_VARS_PATH="$TARGET_DIR/OVMF_VARS-myvm.fd"
IMG_PATH="$TARGET_DIR/disk.img"
ISO_DIR=$(readlink -f "$SCRIPT_DIR/../iso/result/iso")
PID_FILE="$TARGET_DIR/qemu.pid"
# If first argument is "stop", then kill the running VM and exit:
if [ "${1-}" = "stop" ]; then
if [ -f "$PID_FILE" ]; then
VM_PID=$(<"$PID_FILE")
if kill -0 "$VM_PID" 2>/dev/null; then
echo "Killing QEMU (PID $VM_PID)..."
kill "$VM_PID"
# Optionally wait for it to die:
wait "$VM_PID" 2>/dev/null || true
echo "✅ VM stopped."
rm -f "$PID_FILE"
exit 0
else
echo "⚠️ No running QEMU process with PID $VM_PID. Removing stale PID file."
rm -f "$PID_FILE"
exit 1
fi
else
echo "⚠️ No PID file found at $PID_FILE. Is the VM running?"
exit 1
fi
fi
echo
echo "============================================================"
@@ -92,7 +115,8 @@ fi
if [ "$INSTALL_MODE" -eq 1 ]; then
echo "[5/6] Install mode enabled: CD-ROM will be attached"
CDROM_OPTS="-drive file=\"$ISO_FILE\",format=raw,if=none,media=cdrom,id=cd1,readonly=on -device ahci,id=ahci0 -device ide-cd,bus=ahci0.0,drive=cd1,bootindex=1"
CDROM_OPTS="-drive file=\"$ISO_FILE\",format=raw,if=none,media=cdrom,id=cd1,readonly=on \
-device ahci,id=ahci0 -device ide-cd,bus=ahci0.0,drive=cd1,bootindex=1"
else
echo "[5/6] Normal boot mode: No CD-ROM attached"
CDROM_OPTS=""
@@ -100,9 +124,9 @@ fi
echo
# -----------------------------------------------------------------------------
# 6. Launch QEMU
# 6. Launch QEMU (in the background)
# -----------------------------------------------------------------------------
echo "[6/6] Launching QEMU VM now..."
echo "[6/6] Launching QEMU VM now (in background)..."
echo "------------------------------------------------------------"
echo " • Machine: q35, KVM acceleration"
echo " • Memory: 4096 MB"
@@ -122,7 +146,7 @@ echo
# Construct network options
NET_OPTS="-netdev user,id=net0,hostfwd=tcp::2222-:22 -device e1000,netdev=net0"
# Run QEMU using eval to allow variable expansion in CDROM_OPTS
# Run QEMU in the background and store its PID
eval qemu-system-x86_64 \
-machine type=q35,accel=kvm \
-m 4096 \
@@ -137,9 +161,14 @@ eval qemu-system-x86_64 \
$CDROM_OPTS \
\
$NET_OPTS \
-vga virtio
-vga virtio \
&
VM_PID=$!
echo "$VM_PID" > "$PID_FILE"
echo "✅ QEMU started with PID $VM_PID. PID file: $PID_FILE"
echo
echo "============================================================"
echo " QEMU VM has exited"
echo "============================================================"
echo "To stop the VM at any time, run:"
echo " $0 stop"
echo
exit 0