stable vm run and container spin up, not reachable via ssh

This commit is contained in:
2025-08-14 18:45:18 +02:00
parent adaa7b29d2
commit c49eb614d5
4 changed files with 379 additions and 191 deletions

View File

@@ -5,20 +5,51 @@ This file provides guidelines for AI coding agents operating within this reposit
## Build, Lint, and Test Commands
- **Build**: `make build-usb` (Builds the NixOS workshop ISO)
- **Lint**: No specific linting command found. Follow general code style guidelines.
- **Test**: No specific testing command found. Use `make status-cloud` for health checks.
- **Single Test**: No specific command for running a single test.
- **Local VM**: `make local-vm-run` (Starts local development environment with 15 containers)
- **Lint**: `make lint` (Runs markdownlint, JSON validation, and nixpkgs-fmt)
- **Test**: `make status-cloud` (Health checks for cloud infrastructure)
- **Deploy**: `make deploy-cloud` (Deploys 15 VMs to Hetzner Cloud)
## Code Style Guidelines
- **Imports**: Organize imports alphabetically. Avoid unused imports.
- **Formatting**: Adhere to Nixpkgs formatting conventions. Use `nixpkgs-fmt` if available.
- **Formatting**: Adhere to Nixpkgs formatting conventions. Use `nixpkgs-fmt` for consistency.
- **Types**: Use Nix's type system rigorously. Define types explicitly where possible.
- **Naming Conventions**:
- Variables and functions: `camelCase` or `snake_case` (be consistent).
- Package names: `lowercase-with-hyphens`.
- Variables and functions: `camelCase` for Nix expressions
- Container/server names: `lowercase` (hopper, curie, lovelace, etc.)
- Script names: `kebab-case` for executables
- **SSH Keys**: Always use Ed25519 keys (`~/.ssh/id_ed25519.pub`)
- **Domain**: Use `codecrispi.es` consistently across all environments
- **Password Policy**: Minimize password usage; prefer key-based authentication
- **Error Handling**: Handle errors explicitly. Use Nix's error reporting mechanisms.
- **General**:
- Keep code concise and readable.
- Prefer declarative over imperative approaches.
- Document complex logic.
## Container Architecture
- **Local VM**: Creates 15 containers (192.168.100.11-25) matching production count
- **Container Names**: hopper, curie, lovelace, noether, hamilton, franklin, johnson, clarke, goldberg, liskov, wing, rosen, shaw, karp, rich
- **Networking**: Private networking with NAT for local development
- **DNS**: Local `.local` domain resolution for testing
## Available Scripts
- `connect <name>` - SSH into specific container
- `containers` - List all containers with IPs
- `logs` - Show container setup logs
- `recipes` - Display available Co-op Cloud recipes
- `help` - Show command help
## Development Workflow
1. Use `make local-vm-run` for local development
2. Test with all 15 containers to match production
3. Use `make build-usb` for workshop USB drives
4. Deploy to cloud with `make deploy-cloud`
## General Guidelines
- Keep code concise and readable
- Prefer declarative over imperative approaches
- Document complex logic with comments
- Test locally before cloud deployment
- Maintain feature parity between USB/VM environments where possible

View File

@@ -4,6 +4,7 @@ export
.PHONY: help deploy-cloud build-usb flash-usb local-vm-run clean status destroy-cloud opencode lint
DOMAIN := $(or $(WORKSHOP_DOMAIN),codecrispi.es)
PARTICIPANTS := $(or $(WORKSHOP_PARTICIPANTS),3)
USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX)
help:
@@ -19,17 +20,21 @@ help:
@echo " make flash-usb - Flash ISO to USB drive"
@echo ""
@echo "Local Development:"
@echo " make local-vm-run - Start local VM with containers"
@echo " make local-vm-run - Start local VM with 15 containers"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Development:"
@echo " make opencode - Start opencode in dev shell"
@echo " make lint - Run linting checks"
@echo ""
@echo "Config: Domain=$(DOMAIN), USB=$(USB_DEVICE)"
@echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_rsa.pub"
@echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub"
build-usb:
@echo "Building NixOS workshop ISO for $(DOMAIN)..."
@if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \
echo "Generate with: ssh-keygen -t rsa -b 4096"; \
echo "Generate with: ssh-keygen -t ed25519"; \
exit 1; \
fi
nix build .#live-iso --show-trace
@@ -54,8 +59,9 @@ deploy-cloud:
echo "Get token from: https://console.hetzner.cloud/"; \
exit 1; \
fi
@if [ ! -f ~/.ssh/id_rsa.pub ]; then \
echo "SSH key not found at ~/.ssh/id_rsa.pub"; \
@if [ ! -f ~/.ssh/id_ed25519.pub ]; then \
echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \
echo "Generate with: ssh-keygen -t ed25519"; \
exit 1; \
fi
@echo "Deploying 15 workshop servers to Hetzner Cloud..."
@@ -66,7 +72,7 @@ deploy-cloud:
-var="hetzner_dns_token=$(HETZNER_DNS_TOKEN)" \
-var="dns_zone_id=$(DNS_ZONE_ID)" \
-var="domain=$(DOMAIN)" \
-var="ssh_public_key=$$(cat ~/.ssh/id_rsa.pub)"
-var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)"
@echo "Running health checks..."
@sleep 60
$(MAKE) status-cloud
@@ -91,8 +97,8 @@ destroy-cloud:
cd terraform && terraform destroy -auto-approve
local-vm-run:
@echo "Starting local workshop VM..."
@echo "VM will open with desktop showing 2 participant containers"
@echo "Starting local workshop VM with $(PARTICIPANTS) containers..."
@echo "VM will open with desktop showing all participant containers"
nix run --impure .#local-vm
clean:

160
README.md
View File

@@ -6,7 +6,7 @@ This repository contains the infrastructure for the Co-op Cloud workshop, provid
## 🚀 Quick Start
```bash
# 1. Start the local development virtual machine
# 1. Start the local development virtual machine (15 containers)
make local-vm-run
# 2. Build & flash USB drives for participants
@@ -16,9 +16,9 @@ make flash-usb USB_DEVICE=/dev/sdX
# 3. Deploy the production cloud infrastructure
export HCLOUD_TOKEN="your_token_here"
make deploy-cloud
````
```
-----
---
## 📁 Project Structure
@@ -30,60 +30,119 @@ make deploy-cloud
└── Makefile # Build & deploy commands
```
-----
---
## 🌍 Three Environments
### 1\. Cloud (Production)
### 1. Cloud (Production)
- [cite\_start]**What:** Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc. [cite: 52]
- **Purpose:** The live environment for workshop participants.
- **What:** 15 Hetzner VMs named `hopper.codecrispi.es`, `curie.codecrispi.es`, etc.
- **Purpose:** The live environment for workshop participants.
- **Participants:** hopper, curie, lovelace, noether, hamilton, franklin, johnson, clarke, goldberg, liskov, wing, rosen, shaw, karp, rich
### 2\. USB Boot (Workshop)
### 2. USB Boot (Workshop)
- [cite\_start]**What:** A bootable NixOS live environment. [cite: 4]
- **Purpose:** Used by participants to connect to their cloud servers. [cite\_start]It includes helper functions like `connect hopper`. [cite: 12]
- **What:** A bootable NixOS live environment with SSH client tools.
- **Purpose:** Used by participants to connect to their cloud servers.
- **Features:** Pre-configured with helper functions like `connect hopper`, `recipes` command, and workshop-specific tooling.
### 3\. Local (Development)
### 3. Local (Development)
- **What:** A self-contained Virtual Machine (VM) that runs on your local computer.
- **Purpose:** The VM hosts simulated participant containers (e.g., `hopper.local`) and includes a lightweight desktop with a web browser, providing a perfect, isolated environment to test the entire workshop flow without needing cloud servers.
- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with all 15 containers.
- **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers.
- **Resources:** Creates 15 containers (heavy resource usage - ensure adequate RAM/CPU)
-----
---
## 🔧 Local Development Workflow
1. **Start the VM**
Run the following command. A new window will open and automatically boot into a lightweight desktop.
1. **Start the VM**
Run the following command. A new window will open and automatically boot into a lightweight desktop.
```bash
make local-vm-run
```
```bash
make local-vm-run
```
2. **Work Inside the VM**
All testing is now done inside the VM's graphical desktop.
2. **Work Inside the VM**
All testing is now done inside the VM's graphical desktop.
* Open the **Terminal** to run commands.
* Open **Firefox** to view the deployed web applications.
* Open the **Terminal** to run commands.
* Open **Firefox** to view the deployed web applications.
3. **Example: Deploying WordPress**
3. **Example: Deploying WordPress**
* **In the VM's Terminal**, get a root shell and SSH into the first participant's container:
```bash
# Become root (no password needed)
sudo -i
* **In the VM's Terminal**, get a root shell and SSH into a participant's container:
```bash
# Become root (no password needed)
sudo -i
# Connect to participant 1 (hopper.local)
ssh root@192.168.100.11
```
* **Inside the container**, deploy a WordPress site with `abra`:
```bash
abra app new wordpress -S --domain=blog.hopper.local
abra app deploy blog.hopper.local
```
* **In the VM's Firefox**, navigate to the address `http://blog.hopper.local`. You will see the WordPress installation screen.
# Connect to participant 1 (hopper)
connect hopper
-----
# Or direct SSH
ssh root@192.168.100.11
```
* **Inside the container**, deploy a WordPress site with `abra`:
```bash
abra app new wordpress -S --domain=blog.hopper.local
abra app deploy blog.hopper.local
```
* **In the VM's Firefox**, navigate to `http://blog.hopper.local`. You will see the WordPress installation screen.
4. **Available Helper Commands**
```bash
sudo containers # List all 15 containers with IPs
sudo logs # Show setup logs for all containers
sudo recipes # Display available Co-op Cloud recipes
sudo help # Show all available commands
```
---
## 🌐 Cloud Deployment
The cloud environment creates 15 production servers:
```bash
# Set required environment variables
export HCLOUD_TOKEN="your_hetzner_token"
export HETZNER_DNS_TOKEN="your_dns_token"
export DNS_ZONE_ID="your_zone_id"
# Deploy all 15 servers
make deploy-cloud
# Check server status
make status-cloud
```
Each server is accessible at:
- `hopper.codecrispi.es`
- `curie.codecrispi.es`
- `lovelace.codecrispi.es`
- ... (15 total)
---
## 💾 USB Workshop Environment
Build bootable USB drives for participants:
```bash
# Build the ISO
make build-usb
# Flash to USB drive (replace /dev/sdX with your device)
make flash-usb USB_DEVICE=/dev/sdb
```
The USB environment includes:
- Pre-configured SSH client
- `connect <name>` command to SSH into assigned servers
- `recipes` command showing available Co-op Cloud applications
- Workshop-specific networking and WiFi helpers
---
## 🧹 Cleanup
@@ -94,5 +153,28 @@ make clean
# Destroy Hetzner cloud infrastructure
make destroy-cloud
# To stop the local VM, simply close its window.
# To stop the local VM, simply close its window
```
---
## 🔑 Prerequisites
- **SSH Key:** Ed25519 key at `~/.ssh/id_ed25519.pub`
```bash
ssh-keygen -t ed25519
```
- **Nix:** NixOS or Nix package manager with flakes enabled
- **Cloud Tokens:** Hetzner Cloud API token for deployment
- **Resources:** For local VM: 8GB+ RAM recommended (runs 15 containers)
---
## 🎯 Workshop Flow
1. **Preparation:** Deploy cloud infrastructure with `make deploy-cloud`
2. **Distribution:** Flash USB drives for participants with `make build-usb && make flash-usb`
3. **Workshop:** Participants boot from USB and connect to their assigned cloud servers
4. **Development:** Use local VM (`make local-vm-run`) for testing and development
The architecture ensures participants get identical environments whether connecting from USB boot drives to cloud servers, or testing locally in the development VM.

319
flake.nix
View File

@@ -13,8 +13,7 @@
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
participantNames = [ "hopper" "curie" ];
fullParticipantNames = [
allParticipantNames = [
"hopper"
"curie"
"lovelace"
@@ -31,6 +30,15 @@
"karp"
"rich"
];
numLookup = {
"0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4; "5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9;
"10" = 10; "11" = 11; "12" = 12; "13" = 13; "14" = 14; "15" = 15;
};
participantsEnv = builtins.getEnv "PARTICIPANTS";
numParticipants = if builtins.hasAttr participantsEnv numLookup
then builtins.getAttr participantsEnv numLookup
else 3;
participantNames = builtins.genList (i: builtins.elemAt allParticipantNames i) numParticipants;
in
{
packages.${system} = {
@@ -73,62 +81,62 @@
programs.zsh = {
enable = true;
interactiveShellInit = ''
echo "CODE CRISPIES Workshop Environment"
echo "Available servers:"
${builtins.concatStringsSep "\n" (map (name:
"echo \" - ${name}.codecrispi.es\""
) fullParticipantNames)}
echo ""
echo "Commands: connect <name> | recipes | help"
echo "CODE CRISPIES Workshop Environment"
echo "Available servers:"
${builtins.concatStringsSep "\n" (map (name:
"echo \" - ${name}.codecrispi.es\""
) allParticipantNames)}
echo ""
echo "Commands: connect <name> | recipes | help"
connect() {
[ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
echo "Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
}
connect() {
[ -z "$1" ] && { echo "Usage: connect <name>"; return 1; }
echo "Connecting to $1.codecrispi.es..."
ssh -o StrictHostKeyChecking=no workshop@$1.codecrispi.es
}
recipes() {
echo "Available Co-op Cloud Recipes:"
echo ""
echo "Content Management:"
echo " wordpress ghost hedgedoc dokuwiki mediawiki"
echo ""
echo "File & Collaboration:"
echo " nextcloud seafile collabora onlyoffice"
echo ""
echo "Communication:"
echo " jitsi-meet matrix-synapse rocketchat mattermost"
echo ""
echo "E-commerce & Business:"
echo " prestashop invoiceninja kimai pretix"
echo ""
echo "Development & Tools:"
echo " gitea drone n8n gitlab jupyter-lab"
echo ""
echo "Analytics & Monitoring:"
echo " plausible matomo uptime-kuma grafana"
echo ""
echo "Media & Social:"
echo " peertube funkwhale mastodon pixelfed jellyfin"
echo ""
echo "Deploy: abra app new <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
recipes() {
echo "Available Co-op Cloud Recipes:"
echo ""
echo "Content Management:"
echo " wordpress ghost hedgedoc dokuwiki mediawiki"
echo ""
echo "File & Collaboration:"
echo " nextcloud seafile collabora onlyoffice"
echo ""
echo "Communication:"
echo " jitsi-meet matrix-synapse rocketchat mattermost"
echo ""
echo "E-commerce & Business:"
echo " prestashop invoiceninja kimai pretix"
echo ""
echo "Development & Tools:"
echo " gitea drone n8n gitlab jupyter-lab"
echo ""
echo "Analytics & Monitoring:"
echo " plausible matomo uptime-kuma grafana"
echo ""
echo "Media & Social:"
echo " peertube funkwhale mastodon pixelfed jellyfin"
echo ""
echo "Deploy: abra app new <recipe> -S --domain=myapp.<name>.codecrispi.es"
echo "Browse all: https://recipes.coopcloud.tech"
}
help() {
echo "CODE CRISPIES Workshop Commands:"
echo ""
echo "connect <name> - SSH to your assigned server"
echo "recipes - Show available app recipes"
echo "sudo nmcli dev wifi connect SSID password PASSWORD"
echo ""
echo "Examples:"
echo " connect hopper"
echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024"
}
help() {
echo "CODE CRISPIES Workshop Commands:"
echo ""
echo "connect <name> - SSH to your assigned server"
echo "recipes - Show available app recipes"
echo "sudo nmcli dev wifi connect SSID password PASSWORD"
echo ""
echo "Examples:"
echo " connect hopper"
echo " sudo nmcli dev wifi connect CODE_CRISPIES_GUEST password workshop2024"
}
export -f connect recipes help
'';
export -f connect recipes help
'';
};
services.xserver = {
@@ -191,27 +199,32 @@
autoLogin.user = "workshop";
};
services.xserver.displayManager.sessionCommands = ''
${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \
--command="bash -c '
echo \"Workshop VM Ready!\";
echo \"\";
echo \"SSH into containers:\";
echo \" sudo connect hopper # Container login\";
echo \" sudo connect curie # Container login\";
echo \" ssh root@192.168.100.11 # Direct SSH to hopper\";
echo \" ssh root@192.168.100.12 # Direct SSH to curie\";
echo \"\";
echo \"Container management:\";
echo \" sudo containers # List all containers\";
echo \" sudo logs # Show setup logs\";
echo \"\";
echo \"Abra is pre-installed in containers!\";
echo \"\";
bash
'" &
'';
services.xserver.displayManager.sessionCommands = ''
${pkgs.xfce.xfce4-terminal}/bin/xfce4-terminal --title="Workshop Terminal" \
--command="bash -c '
echo "Workshop VM Ready!";
echo "";
echo "SSH into containers:";
${builtins.concatStringsSep "
" (map (name:
let ip = "192.168.100.${toString (11 + (builtins.elemAt (builtins.genList (x: x) (builtins.length participantNames))
(builtins.elemAt
(builtins.filter (i: builtins.elemAt participantNames i == name)
(builtins.genList (x: x) (builtins.length participantNames))) 0)))}";
in "echo \" sudo connect ${name} # Container login to ${name}\""
) participantNames)}
echo " (Total: ${toString numParticipants} containers)";
echo "";
echo "Container management:";
echo " sudo containers # List all containers";
echo " sudo logs # Show setup logs";
echo " sudo recipes # Show available recipes";
echo "";
echo "Abra is pre-installed in containers!";
echo "";
bash
'" &
'';
environment.systemPackages = with pkgs; [
firefox
curl
@@ -220,25 +233,73 @@
nano
tree
nixos-container
(pkgs.writeScriptBin "connect" ''
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: connect <container-name>"
echo "Available: hopper curie"
exit 1
fi
exec nixos-container root-login "$1"
'')
(pkgs.writeScriptBin "containers" ''
#!/bin/bash
exec nixos-container list
'')
(pkgs.writeScriptBin "logs" ''
#!/bin/bash
exec journalctl -u container@hopper -u container@curie -f
'')
];
(pkgs.writeScriptBin "connect" ''
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: connect <container-name>"
echo "Available: ${builtins.concatStringsSep " " participantNames}"
exit 1
fi
exec nixos-container root-login "$1"
'')
(pkgs.writeScriptBin "containers" ''
#!/bin/bash
echo "Active containers:"
nixos-container list
'')
(pkgs.writeScriptBin "logs" ''
#!/bin/bash
echo "Showing logs for all containers (Ctrl+C to exit)"
exec journalctl -u container@* -f
'')
(pkgs.writeScriptBin "recipes" ''
#!/bin/bash
echo "Available Co-op Cloud Recipes"
echo "Content Management:"
echo " wordpress ghost hedgedoc dokuwiki mediawiki"
echo "File & Collaboration:"
echo " nextcloud seafile collabora onlyoffice"
echo "Communication:"
echo " jitsi-meet matrix-synapse rocketchat mattermost"
echo "E-commerce & Business:"
echo " prestashop invoiceninja kimai pretix"
echo "Development & Tools:"
echo " gitea drone n8n gitlab jupyter-lab"
echo "Analytics & Monitoring:"
echo " plausible matomo uptime-kuma grafana"
echo "Media & Social:"
echo " peertube funkwhale mastodon pixelfed jellyfin"
echo "Usage in container:"
echo " abra app new <recipe> -S --domain=myapp.<container-name>.local"
echo " abra app deploy myapp.<container-name>.local"
echo "Browse all: https://recipes.coopcloud.tech"
'')
(pkgs.writeScriptBin "help" ''
#!/bin/bash
echo "CODE CRISPIES Workshop VM Commands:"
echo ""
echo "Container Management:"
echo " connect <name> - SSH into specific container"
echo " containers - List all containers with IPs"
echo " logs - Show container setup logs"
echo ""
echo "Workshop Tools:"
echo " recipes - Show available Co-op Cloud recipes"
echo " help - Show this help"
echo ""
echo "Examples:"
echo " sudo connect hopper"
echo " ssh root@192.168.100.11"
echo ""
echo "Available containers: ${builtins.concatStringsSep " " participantNames}"
'')
];
# Add local DNS resolution for .local domains
networking = {
hostName = "workshop-vm";
firewall.enable = false;
@@ -247,6 +308,12 @@
internalInterfaces = [ "ve-+" ];
externalInterface = "eth0";
};
extraHosts = builtins.concatStringsSep "\n" (builtins.genList (i:
let
name = builtins.elemAt participantNames i;
ip = "192.168.100.${toString (11 + i)}";
in "${ip} ${name}.local"
) (builtins.length participantNames));
};
containers = builtins.listToAttrs (builtins.genList
@@ -266,10 +333,10 @@
config = {
system.stateVersion = "25.05";
users.users.root.password = "root";
users.users.root.password = "";
users.users.workshop = {
isNormalUser = true;
password = "workshop";
password = "";
extraGroups = [ "wheel" "docker" ];
};
@@ -278,6 +345,7 @@
settings = {
PasswordAuthentication = true;
PermitRootLogin = "yes";
PermitEmptyPasswords = true;
};
};
@@ -288,6 +356,7 @@
};
security.sudo.wheelNeedsPassword = false;
security.pam.services.login.allowNullPassword = true;
virtualisation.docker.enable = true;
environment.systemPackages = with pkgs; [
@@ -304,43 +373,43 @@
after = [ "network-online.target" "docker.service" ];
wants = [ "network-online.target" ];
script = ''
echo "Setting up ${name} container..."
echo "Setting up ${name} container..."
for i in {1..10}; do
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
echo "Network ready"
break
fi
echo "Waiting for network... ($i/10)"
sleep 2
done
for i in {1..10}; do
if ${pkgs.curl}/bin/curl -s --max-time 5 google.com >/dev/null 2>&1; then
echo "Network ready"
break
fi
echo "Waiting for network... ($i/10)"
sleep 2
done
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true
${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || true
export HOME=/root
if [ ! -f /root/.local/bin/abra ]; then
echo "Installing abra..."
${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash
echo "Abra installed"
fi
export HOME=/root
if [ ! -f /root/.local/bin/abra ]; then
echo "Installing abra..."
${pkgs.curl}/bin/curl -fsSL https://install.abra.coopcloud.tech | ${pkgs.bash}/bin/bash
echo "Abra installed"
fi
if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
fi
if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
fi
if [ -f /root/.local/bin/abra ]; then
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true
fi
if [ -f /root/.local/bin/abra ]; then
ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true
fi
if [ -f /root/.local/bin/abra ]; then
export PATH="/root/.local/bin:$PATH"
/root/.local/bin/abra server add ${name}.local 2>/dev/null || true
fi
if [ -f /root/.local/bin/abra ]; then
export PATH="/root/.local/bin:$PATH"
/root/.local/bin/abra server add ${name}.local 2>/dev/null || true
fi
echo "${name} container ready!"
echo "SSH: ssh root@${ip} (password: root)"
echo "Abra: Available via 'abra' command"
'';
echo "${name} container ready!"
echo "SSH: ssh root@${ip} (no password)"
echo "Abra: Available via 'abra' command"
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;