diff --git a/Makefile b/Makefile index afe6d54..5321d28 100644 --- a/Makefile +++ b/Makefile @@ -4,67 +4,74 @@ 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) +PARTICIPANTS := $(or $(PARTICIPANTS),3) USB_DEVICE := $(or $(USB_DEVICE),/dev/sdX) help: - @echo "CODE CRISPIES Workshop" + @echo "CODE CRISPIES Workshop Infrastructure" @echo "" - @echo "Cloud Infrastructure (Hetzner):" + @echo "๐ŸŒ Cloud Infrastructure (Hetzner):" @echo " make deploy-cloud - Deploy 15 VMs to Hetzner Cloud" @echo " make status-cloud - Check server health" @echo " make destroy-cloud - Destroy cloud infrastructure" @echo "" - @echo "USB Boot Drive:" + @echo "๐Ÿ’พ USB Boot Drive:" @echo " make build-usb - Build NixOS workshop ISO" @echo " make flash-usb - Flash ISO to USB drive" @echo "" - @echo "Local Development:" - @echo " make local-vm-run - Start local VM with 15 containers" + @echo "๐Ÿ–ฅ๏ธ Local Development:" + @echo " make local-vm-run - Start local VM with containers" + @echo " make local-vm-test - Test with 2 containers only" + @echo " make local-vm-full - Test with all 15 containers" @echo " make clean - Clean build artifacts" @echo "" - @echo "Development:" + @echo "โš™๏ธ Development:" @echo " make opencode - Start opencode in dev shell" @echo " make lint - Run linting checks" + @echo " make check-vm - Verify VM builds correctly" + @echo "" + @echo "Current Config:" + @echo " Domain: $(DOMAIN)" + @echo " Participants: $(PARTICIPANTS)" + @echo " USB Device: $(USB_DEVICE)" @echo "" - @echo "Config: Domain=$(DOMAIN), USB=$(USB_DEVICE)" @echo "Required: HCLOUD_TOKEN, SSH key at ~/.ssh/id_ed25519.pub" build-usb: - @echo "Building NixOS workshop ISO for $(DOMAIN)..." + @echo "๐Ÿ”จ Building NixOS workshop ISO..." @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ - echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ + echo "โŒ SSH key not found at ~/.ssh/id_ed25519.pub"; \ echo "Generate with: ssh-keygen -t ed25519"; \ exit 1; \ fi nix build .#live-iso --show-trace - @echo "ISO built: result/iso/nixos.iso" - @echo "Size: $$(du -h result/iso/nixos.iso | cut -f1)" + @echo "โœ… ISO built: result/iso/nixos.iso" + @echo "๐Ÿ“ฆ Size: $$(du -h result/iso/nixos.iso | cut -f1)" flash-usb: build-usb @if [ "$(USB_DEVICE)" = "/dev/sdX" ]; then \ - echo "Set USB_DEVICE=/dev/sdX (find with 'lsblk')"; \ + echo "โŒ Set USB_DEVICE=/dev/sdX (find with 'lsblk')"; \ exit 1; \ fi - @echo "About to flash $(USB_DEVICE) - THIS WILL ERASE ALL DATA!" - @echo "Verify device: $$(lsblk $(USB_DEVICE) 2>/dev/null || echo 'DEVICE NOT FOUND')" + @echo "โš ๏ธ About to flash $(USB_DEVICE) - THIS WILL ERASE ALL DATA!" + @echo "Device info: $$(lsblk $(USB_DEVICE) 2>/dev/null || echo 'DEVICE NOT FOUND')" @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] sudo dd if=result/iso/nixos.iso of=$(USB_DEVICE) bs=4M status=progress oflag=sync sync - @echo "USB drive ready for workshop!" + @echo "โœ… USB drive ready for workshop!" deploy-cloud: @if [ -z "$(HCLOUD_TOKEN)" ]; then \ - echo "HCLOUD_TOKEN not set"; \ + echo "โŒ HCLOUD_TOKEN not set"; \ echo "Get token from: https://console.hetzner.cloud/"; \ exit 1; \ fi @if [ ! -f ~/.ssh/id_ed25519.pub ]; then \ - echo "SSH key not found at ~/.ssh/id_ed25519.pub"; \ + 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..." + @echo "๐Ÿš€ Deploying 15 workshop servers to Hetzner Cloud..." @echo "Domain: $(DOMAIN)" cd terraform && terraform init cd terraform && terraform apply -auto-approve \ @@ -73,47 +80,62 @@ deploy-cloud: -var="dns_zone_id=$(DNS_ZONE_ID)" \ -var="domain=$(DOMAIN)" \ -var="ssh_public_key=$$(cat ~/.ssh/id_ed25519.pub)" - @echo "Running health checks..." + @echo "โณ Running health checks..." @sleep 60 $(MAKE) status-cloud - @echo "Cloud deployment complete!" + @echo "โœ… Cloud deployment complete!" status-cloud: - @echo "Checking server health..." + @echo "๐Ÿ” Checking server health..." @for name in hopper curie lovelace noether hamilton franklin johnson clarke goldberg liskov wing rosen shaw karp rich; do \ printf "%-10s " "$$name:"; \ if timeout 10 curl -s -f https://traefik.$$name.$(DOMAIN)/ping >/dev/null 2>&1; then \ - echo "Ready"; \ + echo "โœ… Ready"; \ elif timeout 5 ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no workshop@$$name.$(DOMAIN) "echo ok" >/dev/null 2>&1; then \ - echo "SSH OK, Traefik starting..."; \ + echo "โณ SSH OK, Traefik starting..."; \ else \ - echo "Not ready"; \ + echo "โŒ Not ready"; \ fi; \ done destroy-cloud: - @echo "This will destroy ALL workshop servers!" + @echo "โš ๏ธ This will destroy ALL workshop servers!" @read -p "Continue? [y/N]: " confirm && [ "$$confirm" = "y" ] cd terraform && terraform destroy -auto-approve + @echo "โœ… Cloud infrastructure destroyed" local-vm-run: - @echo "Starting local workshop VM with $(PARTICIPANTS) containers..." + @echo "๐Ÿ–ฅ๏ธ Starting local workshop VM with $(PARTICIPANTS) containers..." @echo "VM will open with desktop showing all participant containers" - nix run --impure .#local-vm + PARTICIPANTS=$(PARTICIPANTS) nix run --impure .#local-vm + +local-vm-test: + @echo "๐Ÿงช Testing with 2 containers only..." + PARTICIPANTS=2 nix run --impure .#local-vm + +local-vm-full: + @echo "๐Ÿš€ Testing with all 15 containers (heavy resource usage!)..." + PARTICIPANTS=15 nix run --impure .#local-vm + +check-vm: + @echo "โœ… Verifying VM builds correctly..." + PARTICIPANTS=2 nix build --impure .#local-vm + @echo "โœ… VM build successful" clean: rm -rf result .direnv terraform/.terraform terraform/terraform.tfstate* - @echo "Cleaned up build artifacts" + @echo "๐Ÿงน Cleaned up build artifacts" opencode: - @echo "Starting opencode in Nix dev shell..." + @echo "๐Ÿ’ป Starting opencode in Nix dev shell..." nix develop --command opencode lint: - @echo "Linting Markdown files..." + @echo "๐Ÿ” Linting project files..." + @echo "Markdown files..." @markdownlint-cli . || true - @echo "Linting JSON files..." + @echo "JSON files..." @find . -type f -name "*.json" -print0 | xargs -0 -I {} bash -c 'jq . "{}" >/dev/null || (echo "JSON lint error in {}" && exit 1)' - @echo "Linting Nix files..." - @nixpkgs-fmt . || true - @echo "Linting complete." + @echo "Nix files..." + @nixpkgs-fmt --check . || true + @echo "โœ… Linting complete" diff --git a/README.md b/README.md index 10e4114..994f2eb 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@ # ๐Ÿช CODE CRISPIES Workshop Infrastructure -This repository contains the infrastructure for the Co-op Cloud workshop, providing three distinct deployment environments. +This repository contains the infrastructure for the Co-op Cloud workshop, providing three distinct deployment environments with dynamic scaling support. --- ## ๐Ÿš€ Quick Start ```bash -# 1. Start the local development virtual machine (15 containers) +# 1. Start the local development virtual machine (default: 3 containers) make local-vm-run -# 2. Build & flash USB drives for participants +# 2. Test with different container counts +PARTICIPANTS=2 make local-vm-test # Lightweight testing +PARTICIPANTS=15 make local-vm-full # Full workshop simulation + +# 3. Build & flash USB drives for participants make build-usb make flash-usb USB_DEVICE=/dev/sdX -# 3. Deploy the production cloud infrastructure +# 4. Deploy the production cloud infrastructure export HCLOUD_TOKEN="your_token_here" make deploy-cloud ``` @@ -23,7 +27,7 @@ make deploy-cloud ## ๐Ÿ“ Project Structure ``` -โ”œโ”€โ”€ flake.nix # All Nix configurations (USB, VM) +โ”œโ”€โ”€ flake.nix # All Nix configurations (USB, VM, containers) โ”œโ”€โ”€ terraform/ # Hetzner Cloud infrastructure โ”œโ”€โ”€ scripts/deploy.sh # Cloud setup automation โ”œโ”€โ”€ docs/USB_BOOT_INSTRUCTIONS.md @@ -48,50 +52,56 @@ make deploy-cloud ### 3. Local (Development) -- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with all 15 containers. +- **What:** A self-contained Virtual Machine (VM) that runs on your local computer with configurable container count. - **Purpose:** Complete local testing environment that mirrors production setup without needing cloud servers. -- **Resources:** Creates 15 containers (heavy resource usage - ensure adequate RAM/CPU) +- **Scalability:** Supports 1-15 containers via `PARTICIPANTS` environment variable. --- ## ๐Ÿ”ง Local Development Workflow -1. **Start the VM** - Run the following command. A new window will open and automatically boot into a lightweight desktop. - +1. **Choose Your Scale** ```bash + # Lightweight development (2 containers) + PARTICIPANTS=2 make local-vm-run + + # Production simulation (15 containers) - requires 8GB+ RAM + PARTICIPANTS=15 make local-vm-run + + # Use default (3 containers) - good balance make local-vm-run ``` 2. **Work Inside the VM** - All testing is now done inside the VM's graphical desktop. - + 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. 3. **Example: Deploying WordPress** + + **In the VM's Terminal**, get a root shell and SSH into a 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) + connect hopper - # 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. + # Or direct SSH (password: root) + 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 containers # List all 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 @@ -144,7 +154,25 @@ The USB environment includes: --- -## ๐Ÿงน Cleanup +## โš™๏ธ Environment Variables + +Control workshop behavior with environment variables: + +```bash +# Number of containers (1-15, default: 3) +export PARTICIPANTS=5 +make local-vm-run + +# Workshop domain for cloud deployment +export WORKSHOP_DOMAIN=myworkshop.com + +# USB device for flashing +export USB_DEVICE=/dev/sdb +``` + +--- + +## ๐Ÿงน Cleanup & Management ```bash # Clean local build artifacts @@ -153,7 +181,12 @@ make clean # Destroy Hetzner cloud infrastructure make destroy-cloud -# To stop the local VM, simply close its window +# Verify VM builds correctly +make check-vm + +# Run development tools +make opencode # Start development environment +make lint # Code quality checks ``` --- @@ -166,7 +199,10 @@ make destroy-cloud ``` - **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) +- **Resources:** + - 2-3 containers: 4GB+ RAM + - 5-10 containers: 8GB+ RAM + - 15 containers: 16GB+ RAM --- @@ -175,6 +211,39 @@ make destroy-cloud 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 +4. **Development:** Use local VM with `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. + +--- + +## ๐Ÿ› Troubleshooting + +### VM Won't Start +```bash +# Check if build works +make check-vm + +# Try with fewer containers +PARTICIPANTS=2 make local-vm-run +``` + +### Containers Not Accessible +```bash +# Check container status inside VM +sudo containers + +# View setup logs +sudo logs + +# Manual SSH test +ssh root@192.168.100.11 # Password: root +``` + +### Abra Not Working in Container +```bash +# Inside container, check installation +ls -la /root/.local/bin/abra +export PATH="/root/.local/bin:$PATH" +abra --version +``` diff --git a/flake.nix b/flake.nix index adb889e..e31b8cc 100644 --- a/flake.nix +++ b/flake.nix @@ -1,433 +1,435 @@ { - description = "Workshop VM with Participant Containers + USB ISO"; + description = "Workshop VM with Participant Containers + USB ISO"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + nixos-generators = { + url = "github:nix-community/nixos-generators"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - nixos-generators = { - url = "github:nix-community/nixos-generators"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; + outputs = { self, nixpkgs, nixos-generators }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + # All possible participant names for the workshop + allParticipantNames = [ + "hopper" "curie" "lovelace" "noether" "hamilton" + "franklin" "johnson" "clarke" "goldberg" "liskov" + "wing" "rosen" "shaw" "karp" "rich" + ]; + + # Dynamic participant count (default 3, max 15) + participantsEnv = builtins.getEnv "PARTICIPANTS"; + numParticipants = + if participantsEnv != "" && builtins.match "^[0-9]+$" participantsEnv != null + then + let num = builtins.fromJSON participantsEnv; + in if num >= 1 && num <= 15 then num else 3 + else 3; + + # Selected participant names based on count + participantNames = builtins.genList + (i: builtins.elemAt allParticipantNames i) + numParticipants; + in + { + packages.${system} = { + local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm; - outputs = { self, nixpkgs, nixos-generators }: - let - system = "x86_64-linux"; - pkgs = nixpkgs.legacyPackages.${system}; - allParticipantNames = [ - "hopper" - "curie" - "lovelace" - "noether" - "hamilton" - "franklin" - "johnson" - "clarke" - "goldberg" - "liskov" - "wing" - "rosen" - "shaw" - "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} = { - local-vm = self.nixosConfigurations.workshop-vm.config.system.build.vm; + live-iso = nixos-generators.nixosGenerate { + inherit system; + format = "iso"; - live-iso = nixos-generators.nixosGenerate { - inherit system; - format = "iso"; + modules = [ + ({ pkgs, ... }: { + system.stateVersion = "25.05"; - modules = [ - ({ pkgs, ... }: { - system.stateVersion = "25.05"; + isoImage.makeEfiBootable = true; + isoImage.makeUsbBootable = true; - isoImage.makeEfiBootable = true; - isoImage.makeUsbBootable = true; + networking.wireless.enable = true; + networking.networkmanager.enable = true; + networking.hostName = "workshop-live"; - networking.wireless.enable = true; - networking.networkmanager.enable = true; - networking.hostName = "workshop-live"; + services.getty.autologinUser = "workshop"; + users.users.workshop = { + isNormalUser = true; + shell = pkgs.zsh; + extraGroups = [ "networkmanager" "wheel" ]; + password = ""; + }; - services.getty.autologinUser = "workshop"; - users.users.workshop = { - isNormalUser = true; - shell = pkgs.zsh; - extraGroups = [ "networkmanager" "wheel" ]; - password = ""; - }; + security.sudo.wheelNeedsPassword = false; - security.sudo.wheelNeedsPassword = false; + environment.systemPackages = with pkgs; [ + openssh curl git networkmanager firefox xterm + ]; - environment.systemPackages = with pkgs; [ - openssh - curl - git - networkmanager - firefox - xterm - ]; + programs.zsh = { + enable = true; + interactiveShellInit = '' + echo "CODE CRISPIES Workshop Environment" + echo "Available servers:" + ${builtins.concatStringsSep "\n" (map (name: + "echo \" - ${name}.codecrispi.es\"" + ) allParticipantNames)} + echo "" + echo "Commands: connect | recipes | help" + + connect() { + [ -z "$1" ] && { echo "Usage: connect "; 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 -S --domain=myapp..codecrispi.es" + echo "Browse all: https://recipes.coopcloud.tech" + } + + help() { + echo "CODE CRISPIES Workshop Commands:" + echo "" + echo "connect - 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 + ''; + }; - programs.zsh = { - enable = true; - interactiveShellInit = '' - echo "CODE CRISPIES Workshop Environment" - echo "Available servers:" - ${builtins.concatStringsSep "\n" (map (name: - "echo \" - ${name}.codecrispi.es\"" - ) allParticipantNames)} - echo "" - echo "Commands: connect | recipes | help" - - connect() { - [ -z "$1" ] && { echo "Usage: connect "; 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 -S --domain=myapp..codecrispi.es" - echo "Browse all: https://recipes.coopcloud.tech" - } - - help() { - echo "CODE CRISPIES Workshop Commands:" - echo "" - echo "connect - 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 - ''; - }; + services.xserver = { + enable = true; + desktopManager.xfce.enable = true; + displayManager = { + lightdm.enable = true; + autoLogin.enable = true; + autoLogin.user = "workshop"; + }; + }; - services.xserver = { - enable = true; - desktopManager.xfce.enable = true; - displayManager = { - lightdm.enable = true; - autoLogin.enable = true; - autoLogin.user = "workshop"; - }; - }; + systemd.user.services.workshop-welcome = { + wantedBy = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &"; + serviceConfig.Type = "forking"; + }; + }) + ]; + }; + }; - systemd.user.services.workshop-welcome = { - wantedBy = [ "graphical-session.target" ]; - after = [ "graphical-session.target" ]; - script = "${pkgs.xterm}/bin/xterm -title 'CODE CRISPIES Workshop' -e 'zsh' &"; - serviceConfig.Type = "forking"; - }; - }) - ]; - }; - }; + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + markdownlint-cli + jq + nixpkgs-fmt + ]; + }; - devShells.${system}.default = pkgs.mkShell { - packages = with pkgs; [ - markdownlint-cli - jq - nixpkgs-fmt - ]; - }; + nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ({ config, pkgs, ... }: { + system.stateVersion = "25.05"; - nixosConfigurations.workshop-vm = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - ({ config, pkgs, ... }: { - system.stateVersion = "25.05"; + boot.loader.grub.enable = false; + boot.loader.generic-extlinux-compatible.enable = true; + boot.kernel.sysctl."net.ipv4.ip_forward" = 1; - boot.loader.grub.enable = false; - boot.loader.generic-extlinux-compatible.enable = true; - boot.kernel.sysctl."net.ipv4.ip_forward" = 1; + users.users.workshop = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + password = "workshop"; + shell = pkgs.bash; + }; - users.users.workshop = { - isNormalUser = true; - extraGroups = [ "wheel" ]; - password = ""; - shell = pkgs.bash; - }; + security.sudo.wheelNeedsPassword = false; - security.pam.services.login.allowNullPassword = true; - security.sudo.wheelNeedsPassword = false; + services.xserver = { + enable = true; + desktopManager.xfce.enable = true; + displayManager.lightdm.enable = true; + }; - services.xserver = { - enable = true; - desktopManager.xfce.enable = true; - displayManager.lightdm.enable = true; - }; + services.displayManager = { + autoLogin.enable = true; + autoLogin.user = "workshop"; + }; - services.displayManager = { - autoLogin.enable = true; - 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:\"; + ${builtins.concatStringsSep "\n" (builtins.genList (i: + let + name = builtins.elemAt participantNames i; + ip = "192.168.100.${toString (11 + i)}"; + in "echo \" sudo connect ${name} # Container login to ${name} (${ip})\"" + ) (builtins.length 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 + '" & + ''; - 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 - git - jq - nano - tree - nixos-container - - (pkgs.writeScriptBin "connect" '' - #!/bin/bash - if [ -z "$1" ]; then - echo "Usage: connect " - 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 -S --domain=myapp..local" - echo " abra app deploy myapp..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 - 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; - nat = { - enable = true; - 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)); - }; + environment.systemPackages = with pkgs; [ + firefox curl git jq nano tree nixos-container + + (pkgs.writeScriptBin "connect" '' + #!/bin/bash + if [ -z "$1" ]; then + echo "Usage: connect " + 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 + echo "" + echo "Container IPs:" + ${builtins.concatStringsSep "\n" (builtins.genList (i: + let + name = builtins.elemAt participantNames i; + ip = "192.168.100.${toString (11 + i)}"; + in "echo \" ${name}: ${ip}\"" + ) (builtins.length participantNames))} + '') + + (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 "" + 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 "Usage in container:" + echo " abra app new -S --domain=myapp..local" + echo " abra app deploy myapp..local" + echo "" + echo "Browse all: https://recipes.coopcloud.tech" + '') + + (pkgs.writeScriptBin "help" '' + #!/bin/bash + echo "CODE CRISPIES Workshop VM Commands:" + echo "" + echo "Container Management:" + echo " connect - 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 (${toString numParticipants}): ${builtins.concatStringsSep " " participantNames}" + '') + ]; - containers = builtins.listToAttrs (builtins.genList - (i: - let - name = builtins.elemAt participantNames i; - ip = "192.168.100.${toString (11 + i)}"; - in - { - inherit name; - value = { - autoStart = true; - privateNetwork = true; - hostAddress = "192.168.100.1"; - localAddress = ip; + # Local DNS resolution for .local domains + networking = { + hostName = "workshop-vm"; + firewall.enable = false; + nat = { + enable = true; + 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)); + }; - config = { - system.stateVersion = "25.05"; + # Dynamic container generation + containers = builtins.listToAttrs (builtins.genList + (i: + let + name = builtins.elemAt participantNames i; + ip = "192.168.100.${toString (11 + i)}"; + in + { + inherit name; + value = { + autoStart = true; + privateNetwork = true; + hostAddress = "192.168.100.1"; + localAddress = ip; - users.users.root.password = ""; - users.users.workshop = { - isNormalUser = true; - password = ""; - extraGroups = [ "wheel" "docker" ]; - }; + config = { + system.stateVersion = "25.05"; - services.openssh = { - enable = true; - settings = { - PasswordAuthentication = true; - PermitRootLogin = "yes"; - PermitEmptyPasswords = true; - }; - }; + users.users.root.password = "root"; + users.users.workshop = { + isNormalUser = true; + password = "workshop"; + extraGroups = [ "wheel" "docker" ]; + }; - networking = { - hostName = name; - nameservers = [ "8.8.8.8" ]; - firewall.enable = false; - }; + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = true; + PermitRootLogin = "yes"; + }; + }; - security.sudo.wheelNeedsPassword = false; - security.pam.services.login.allowNullPassword = true; - virtualisation.docker.enable = true; + networking = { + hostName = name; + nameservers = [ "8.8.8.8" "1.1.1.1" ]; + firewall.enable = false; + }; - environment.systemPackages = with pkgs; [ - docker - curl - git - wget - jq - bash - ]; + security.sudo.wheelNeedsPassword = false; + virtualisation.docker.enable = true; - systemd.services.workshop-setup = { - wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" "docker.service" ]; - wants = [ "network-online.target" ]; - script = '' - 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 - - ${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 - - 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 - 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} (no password)" - echo "Abra: Available via 'abra' command" - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - StandardOutput = "journal"; - StandardError = "journal"; - }; - }; + environment.systemPackages = with pkgs; [ + docker curl git wget jq bash nano tree + ]; - environment.sessionVariables = { - PATH = [ "/root/.local/bin" ]; - }; - }; - }; - } - ) - (builtins.length participantNames)); - }) - ]; - }; - }; + systemd.services.workshop-setup = { + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" "docker.service" ]; + wants = [ "network-online.target" ]; + script = '' + echo "Setting up ${name} container..." + + # Wait for network connectivity + for i in {1..15}; 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/15)" + sleep 3 + done + + # Initialize Docker Swarm + ${pkgs.docker}/bin/docker swarm init --advertise-addr ${ip} || echo "Swarm already initialized or failed" + + # Install abra + 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 to /root/.local/bin/abra" + fi + + # Setup PATH in .bashrc + if ! grep -q "/.local/bin" /root/.bashrc 2>/dev/null; then + echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc + fi + + # Create system symlink for abra + if [ -f /root/.local/bin/abra ]; then + ln -sf /root/.local/bin/abra /usr/local/bin/abra 2>/dev/null || true + fi + + # Add abra server config + if [ -f /root/.local/bin/abra ]; then + export PATH="/root/.local/bin:$PATH" + /root/.local/bin/abra server add ${name}.local 2>/dev/null || echo "Server already added or command failed" + fi + + echo "${name} container ready!" + echo "SSH: ssh root@${ip} (password: root)" + echo "Workshop user: ssh workshop@${ip} (password: workshop)" + echo "Abra: Available via 'abra' command" + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + StandardOutput = "journal"; + StandardError = "journal"; + TimeoutStartSec = "300"; + }; + }; + + environment.sessionVariables = { + PATH = [ "/root/.local/bin" ]; + }; + }; + }; + } + ) + (builtins.length participantNames)); + }) + ]; + }; + }; }