Compare commits

...

15 Commits

Author SHA1 Message Date
Artemis (Iron Legion)
9a6a0e39c3 Revert "Add homelab services stack PRD"
This reverts commit d60bc96f1d.
2026-05-25 17:58:43 -04:00
Artemis (Iron Legion)
d60bc96f1d Add homelab services stack PRD
Verifies 16 DockerHub images, assigns target nodes per locked policy,
defines 3-phase deployment order (Infra → Media → Polish),
and captures open questions for Bobby.

Services: Traefik, Technitium DNS, AdGuard Home, Prometheus, Grafana,
Beszel, Dozzle, Portainer, Homepage, Authelia, Vaultwarden, Jellyfin,
Sonarr, Radarr, Prowlarr, Nextcloud

Domain: *.ai.home
No public internet exposure.
2026-05-25 17:17:23 -04:00
Artemis (Iron Legion)
f3e7c5d108 Move autoinstall/ to 412friendly/backups.git (staging repo) — Iron-Legion repo is ansible-pull only 2026-05-23 19:01:30 -04:00
Artemis (Iron Legion)
2370049e48 Archive MAAS preseed to archive/maas/ with README — project abandoned, replaced by USB autoinstall 2026-05-23 18:58:46 -04:00
Artemis (Iron Legion)
091f11f036 Add minimal autoinstall + first-boot.sh (fleet baseline, Docker, NFS, Tailscale) 2026-05-23 18:50:08 -04:00
Artemis (Iron Legion)
74c26b370b Add fleet-standard Ubuntu autoinstall template (G9 N150, nvme0n1, Docker, SSH key, no-swap) 2026-05-23 18:27:08 -04:00
Artemis (Iron Legion)
b09139bbe7 Add Portainer CE stack for MK-7 (v2.39.2, uses :latest for Docker 29+ compat) 2026-05-23 16:46:36 -04:00
Artemis (Iron Legion)
49b0ddcc7f Clarify autoinstall file naming: user-data and meta-data have no extensions 2026-05-23 16:21:44 -04:00
Artemis (Iron Legion)
39ff842a65 Add Ubuntu Server autoinstall for Iron Legion G9 — headless, zero-touch, replaces MAAS for small batches 2026-05-23 16:20:21 -04:00
Artemis (Iron Legion)
89e518612a MAAS preseed v5: isolate-safe, targets /dev/nvme0n1, no apt-get during late_commands 2026-05-23 13:54:52 -04:00
Artemis (Iron Legion)
f857f15d50 feat: add nfs-common to fleet baseline packages for Igor NAS access 2026-05-23 10:51:54 -04:00
Artemis (Iron Legion)
95a7058eb2 fix: rename mk-39/mk-42 host_vars to match hostname pattern 2026-05-22 23:54:17 -04:00
Artemis (Iron Legion)
38efe66851 fix: rename host_vars for mk-33/mk-34 to match hostname 2026-05-22 23:52:44 -04:00
Artemis (Iron Legion)
909934155e Add host_vars for 4x G9 Docker Swarm workers (MK-33, MK-34, MK-39, MK-42 Extremis) 2026-05-22 15:05:23 -04:00
Artemis (Iron Legion)
377efe11d2 Add Mark VII host_vars — automation and services node 2026-05-22 05:48:11 -04:00
11 changed files with 568 additions and 0 deletions

21
archive/maas/README.md Normal file
View File

@@ -0,0 +1,21 @@
# MAAS Archive
**Status: ABANDONED — May 23, 2026**
These files are the final state of the MAAS PXE deployment effort for Iron Legion fleet nodes. MAAS was removed from Shield after repeated failures on isolated subnets, IP exhaustion, and general complexity. Replaced by USB autoinstall approach.
## Files
- `curtin_userdata_fleet_v5.yaml` — Fleet preseed targeting `/dev/nvme0n1`, no apt-get (isolated subnet safe), SSH enable only. Final working version before abandonment.
## Lessons Captured
1. MAAS ephemeral commissioning fails on subnets without internet (apt-get hangs)
2. IP exhaustion on /27 pools after failed deployments (sticky reservations)
3. G9 Intel I226-V NIC (enp5s0) drops link during Linux driver init
4. Linux enumerates first NVMe as `nvme0n1`, not `nvme1`
5. Cloud-init stomps hostname unless `preserve_hostname: true` is set
## Replacement
See `autoinstall/` in repo root for the USB-based replacement workflow.

View File

@@ -0,0 +1,49 @@
#cloud-config
# MAAS Fleet Preseed v5 — Iron Legion DR Standard
# No apt-get during late_commands (isolated subnet)
# Targets /dev/nvme0n1 (first NVMe, skip eMMC)
# Defer internet tasks to post-deploy on fleet LAN
# Corrected 2026-05-23: nvme1 → nvme0n1 (Linux enumeration)
debconf_selections:
maas: |
{{for line in str(curtin_preseed).splitlines()}}
{{line}}
{{endfor}}
storage:
swap:
size: 0
config:
- type: disk
id: root-disk
path: /dev/nvme0n1
ptable: gpt
wipe: superblock-recursive
- type: partition
id: root-partition
device: root-disk
size: -1
flag: boot
- type: format
id: root-format
volume: root-partition
fstype: ext4
- type: mount
id: root-mount
device: root-format
path: /
late_commands:
fleet_01_create_user: ["curtin", "in-target", "--", "sh", "-c", "useradd -m -s /bin/bash -G sudo jarvis && echo 'jarvis:ubuntu' | chpasswd"]
fleet_02_hostname: ["curtin", "in-target", "--", "sh", "-c", "hostnamectl set-hostname $(echo {{node.hostname}} | sed 's/[^a-zA-Z0-9-]//g') && echo $(echo {{node.hostname}} | sed 's/[^a-zA-Z0-9-]//g') > /etc/hostname"]
fleet_02b_preserve_hostname: ["curtin", "in-target", "--", "sh", "-c", "echo 'preserve_hostname: true' > /etc/cloud/cloud.cfg.d/99_preserve_hostname.cfg"]
fleet_03_enable_ssh: ["curtin", "in-target", "--", "systemctl", "enable", "ssh"]
fleet_04_start_ssh: ["curtin", "in-target", "--", "systemctl", "start", "ssh"]
fleet_05_ssh_dir: ["curtin", "in-target", "--", "sh", "-c", "mkdir -p /home/jarvis/.ssh && chmod 700 /home/jarvis/.ssh"]
fleet_06_auth_keys: ["curtin", "in-target", "--", "sh", "-c", "echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPSBrRCROUHOiZX9IB3teEK89VFfghbdu7OF5NoJ1Y6g Generated By Termius' > /home/jarvis/.ssh/authorized_keys"]
fleet_07_chmod: ["curtin", "in-target", "--", "chmod", "600", "/home/jarvis/.ssh/authorized_keys"]
fleet_08_chown: ["curtin", "in-target", "--", "chown", "-R", "jarvis:jarvis", "/home/jarvis/.ssh"]
fleet_09_sudo: ["curtin", "in-target", "--", "sh", "-c", "echo 'jarvis ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jarvis && chmod 440 /etc/sudoers.d/jarvis"]
fleet_10_ansible_dirs: ["curtin", "in-target", "--", "sh", "-c", "mkdir -p /var/lib/ansible/local"]
maas: [wget, '--no-proxy', {{node_disable_pxe_url|escape.json}}, '--post-data', {{node_disable_pxe_data|escape.json}}, '-O', '/dev/null']

47
host_vars/mark-vii.yml Normal file
View File

@@ -0,0 +1,47 @@
---
# Mark VII (The Avengers Suit) — Automation, backup, and services node
node_type: services
has_gpu: false
# Services node: Docker-focused packages
extra_packages:
- docker.io # Container runtime
- docker-compose # Compose support
- cifs-utils # SMB/CIFS mount for Igor NAS
- nfs-common # NFS mount option
- ncdu # Disk usage analyzer
- tree # Directory structure
- rsync # Sync utility
# No Ollama — Mark VII is not a model host
ollama_models: []
# Services to manage
managed_services:
- name: docker
enabled: true
- name: duplicati
enabled: true
# Mark VII role flags (for playbook logic)
mark_vii_role:
duplicati: true # Backup to Igor
n8n: true # Workflow automation
grafana: true # Metrics dashboard
prometheus: true # Metrics collection
dashboard: true # Single pane of glass
traefik: false # Internal TLS (deferred — needs design)
dns: false # DNS filter (deferred)
postgresql: true # Colocated DB
# Backup target: Igor (ZimaOS NAS)
duplicati_target:
type: smb
host: igor
share: backups
path: /var/lib/duplicati/backups
# Node notes
notes: >
32GB RAM / 1TB NVMe. Docker host for fleet automation stack.
Proxmox possible on separate 4-mini cluster, NOT on Mark VII.

34
host_vars/mk-33.yml Normal file
View File

@@ -0,0 +1,34 @@
---
# MK-33 Silver Centurion — Docker Swarm worker 1
node_type: swarm
has_gpu: false
# Swarm essentials
extra_packages:
- docker.io # Container runtime
- docker-compose # Compose support
- cifs-utils # SMB/CIFS for Igor NAS
- nfs-common # NFS mount option
- curl # Fleet tooling
- git # Git ops
- ufw # Firewall
- htop # Process monitor
- ncdu # Disk usage
- tree # Directory structure
# No Ollama — N150 is CPU-only, no dGPU
ollama_models: []
# Docker Swarm mode
swarm_role: worker # Will join Swarm initiated by Mk7 (manager)
# Node-specific vars
hardware:
cpu: "Intel N150 (4 cores)"
ram: "12GB"
storage: "512GB SSD"
network: "1x Ethernet (integrated)"
notes: >
GMKtec G9 mini PC. Swarm worker only — lightweight containers (homepage,
Prometheus node-exporter, Traefik worker, n8n workers, Duplicati agent).

30
host_vars/mk-34.yml Normal file
View File

@@ -0,0 +1,30 @@
---
# MK-34 Southpaw — Docker Swarm worker 2
node_type: swarm
has_gpu: false
# Swarm essentials
extra_packages:
- docker.io
- docker-compose
- cifs-utils
- nfs-common
- curl
- git
- ufw
- htop
- ncdu
- tree
ollama_models: []
swarm_role: worker
hardware:
cpu: "Intel N150 (4 cores)"
ram: "12GB"
storage: "512GB SSD"
network: "1x Ethernet (integrated)"
notes: >
GMKtec G9 mini PC. Swarm worker.

30
host_vars/mk-39.yml Normal file
View File

@@ -0,0 +1,30 @@
---
# MK-39 Gemini — Docker Swarm worker 3
node_type: swarm
has_gpu: false
# Swarm essentials
extra_packages:
- docker.io
- docker-compose
- cifs-utils
- nfs-common
- curl
- git
- ufw
- htop
- ncdu
- tree
ollama_models: []
swarm_role: worker
hardware:
cpu: "Intel N150 (4 cores)"
ram: "12GB"
storage: "512GB SSD"
network: "1x Ethernet (integrated)"
notes: >
GMKtec G9 mini PC. Swarm worker.

30
host_vars/mk-42.yml Normal file
View File

@@ -0,0 +1,30 @@
---
# MK-42 Extremis — Docker Swarm worker 4
node_type: swarm
has_gpu: false
# Swarm essentials
extra_packages:
- docker.io
- docker-compose
- cifs-utils
- nfs-common
- curl
- git
- ufw
- htop
- ncdu
- tree
ollama_models: []
swarm_role: worker
hardware:
cpu: "Intel N150 (4 cores)"
ram: "12GB"
storage: "512GB SSD"
network: "1x Ethernet (integrated)"
notes: >
GMKtec G9 mini PC. Swarm worker.

View File

@@ -33,6 +33,7 @@
- vim
- python3-pip
- cifs-utils
- nfs-common
state: present
when: ansible_os_family == "Debian"
tags: [baseline]

View File

@@ -0,0 +1,35 @@
version: '3.8'
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
ports:
- "8000:8000" # Edge agent port
- "9000:9000" # HTTP web UI
- "9443:9443" # HTTPS web UI (optional)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
environment:
- TZ=America/New_York
networks:
- portainer_net
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.nb.bobbysh.me`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
# Optional: disable Traefik if not using it yet
# - "traefik.enable=false"
volumes:
portainer_data:
driver: local
networks:
portainer_net:
driver: bridge
attachable: true

View File

@@ -0,0 +1,156 @@
# Ubuntu Server Autoinstall — Iron Legion G9
Headless, zero-touch Ubuntu 24.04 LTS deployment for GMKtec G9 mini-PCs.
Replaces MAAS for small batches. Fully autonomous after USB boot.
## Files
| File | Purpose |
|------|---------|
| `autoinstall.yaml` | Main config — save as `user-data` on USB |
| `meta-data` | Empty companion file (required by nocloud) |
## Quick Start
### 1. Create the USB stick
1. Download **Ubuntu Server 24.04 LTS ISO** (not Desktop)
2. Flash to USB with **Rufus** (Windows) or `dd` (Linux)
- Partition scheme: GPT
- Target system: UEFI (non-CSM)
3. Mount the USB's writable partition
### 2. Add autoinstall files
Create these two files in the **root of the USB's writable partition**:
**`user-data`** — copy `autoinstall.yaml` and rename it to `user-data` (no extension):
```bash
cp autoinstall.yaml /media/usb/user-data
```
**`meta-data`** — create an empty file literally named `meta-data` (no extension):
```bash
touch /media/usb/meta-data
```
Optional content for `meta-data`:
```yaml
instance-id: iid-ironlegion01
local-hostname: ubuntu
```
### 3. Configure GRUB for headless boot
Edit `boot/grub/grub.cfg` on the USB. Find the first menuentry and add kernel params:
```grub
set timeout=5
menuentry "Try or Install Ubuntu Server" {
linux /casper/vmlinuz autoinstall ds=nocloud\;s=/cdrom/ quiet ---
initrd /casper/initrd
}
```
**`timeout=5`** means 5-second countdown then auto-boots. Fully headless.
### 4. Boot the G9
1. Plug USB into G9
2. Power on
3. Walk away — 5 minutes later it's done
4. SSH in as `jarvis` / `ubuntu`
## What Happens
| Stage | Action |
|-------|--------|
| Boot | Ubuntu Server installer loads from USB |
| Storage | GPT on `/dev/nvme0n1` — 1GB boot + rest root (ext4) |
| Identity | Creates `jarvis` user with fleet SSH key |
| Hostname | Sets from MAC → `mk-33`, `mk-34`, `mk-39`, `mk-42`, or `g9-<last4>` |
| Packages | Installs Docker, Tailscale, git, curl, ufw, nfs-common |
| Docker | Adds `jarvis` to `docker` group |
| Tailscale | Installed, ready for `tailscale up` |
| Ansible | Clones `ansible-pull-deploy` repo to `~/.ansible-repo` |
| Network | Wildcard DHCP on all `en*` interfaces |
| SSH | Open on port 22, password auth enabled (fleet standard) |
| Firewall | UFW enabled — allows SSH only |
| Final | Reboots into fresh system |
## Post-Install (one command per node)
After the node is up:
```bash
# Join Tailscale (interactive — generates auth URL)
sudo tailscale up
# Or with pre-generated auth key:
sudo tailscale up --auth-key=tskey-auth-xxxxxxxx
# Run ansible-pull for remaining fleet config
cd ~/.ansible-repo && ansible-pull -U https://gitea.nb.bobbysh.me/Iron-Legion/ansible-pull-deploy.git -d .
```
## MAC-to-Hostname Mapping
| MAC | Hostname |
|-----|----------|
| `e0:51:d8:1c:5d:56` | `mk-33` |
| `e0:51:d8:1c:5c:75` | `mk-34` |
| `e0:51:d8:1c:5d:ca` | `mk-39` |
| `e0:51:d8:1c:5d:5c` | `mk-42` |
| other | `g9-<last4mac>` |
## Tailscale Auth Key
To auto-join without interaction, generate a reusable auth key:
```bash
tailscale login # on Artemis or any node
tailscale web
tailscale authkeys create --reusable --preauthorized --tags=tag:g9
```
Add the key to `late-commands` in `autoinstall.yaml`:
```yaml
- sudo tailscale up --auth-key=tskey-auth-xxxxxxxx
```
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| Stops at boot menu | Check GRUB `timeout` is set, not `#` commented |
| Can't find `user-data` | Verify files are on writable partition root, not inside a folder |
| Wrong disk | Confirm G9 has 1TB NVMe in first slot (`/dev/nvme0n1`) |
| Hostname is `ubuntu` | Check MAC is correct; check `/etc/cloud/cloud-init.disabled` exists |
| No SSH | Verify `openssh-server` installed; check `ufw status` |
## Differences from MAAS
| | MAAS | Autoinstall |
|--|------|-------------|
| PXE server | Required (Shield) | Not needed |
| Subnet | Isolated `/27` required | Any network with DHCP |
| Internet during install | Not available | Available via your LAN |
| Time per node | 15-30 min | ~5 min |
| Touch required | Power on + rename in UI | Zero (headless) |
| Scale | Excellent for 10+ nodes | Ideal for 1-10 nodes |
| Phase 2 bootstrap | Required manually | Included in late-commands |
## Notes
- Password is fleet standard: `ubuntu`
- SSH key is the same fleet-wide key used on all nodes
- Cloud-init is disabled post-install to prevent hostname stomping
- If you get more G9s, just update the MAC→hostname mapping and re-flash
## Version
- Ubuntu 24.04 LTS (Noble Numbat)
- Docker CE latest
- Tailscale stable
- Ansible 2.x (pulled from repo)

View File

@@ -0,0 +1,135 @@
# Ubuntu Server Autoinstall — Iron Legion G9 Headless Deploy
# Usage: Place as 'user-data' on nocloud USB alongside empty 'meta-data' file
# See README.md for USB creation instructions
version: 1
autoinstall:
# ── Identity ──
identity:
hostname: ubuntu
username: jarvis
password: "$6$iypA63f5vLDzTGQ2$eOrvsyhnM6c4istoy65GUConWL4St.rzy28wFt8QxUWk7F3fSx7mHytwnjHosvIj7JAMBPeC4jkUctAZJeKDx/"
ssh:
install-server: true
allow-pw: true
authorized-keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPSBrRCROUHOiZX9IB3teEK89VFfghbdu7OF5NoJ1Y6g
# ── Storage ──
storage:
config:
- type: disk
id: disk0
ptable: gpt
wipe: superblock-recursive
path: /dev/nvme0n1
- type: partition
id: boot-part
number: 1
size: 1GB
device: disk0
flag: boot
grub_device: true
- type: format
id: boot-format
fstype: ext4
volume: boot-part
- type: mount
id: boot-mount
device: boot-format
path: /boot
- type: partition
id: root-part
number: 2
size: -1
device: disk0
- type: format
id: root-format
fstype: ext4
volume: root-part
- type: mount
id: root-mount
device: root-format
path: /
# ── Networking ──
network:
version: 2
ethernets:
en0:
match:
name: "en*"
dhcp4: true
dhcp6: true
# ── Packages ──
packages:
- openssh-server
- curl
- git
- ca-certificates
- gnupg
- net-tools
- iputils-ping
- htop
- ufw
- nfs-common
# ── First boot scripts ──
late-commands:
# Hostname from MAC address
- |
MAC=$(cat /sys/class/net/en*/address 2>/dev/null | head -1 | tr -d ':')
case "$MAC" in
e051d81c5d56) HOST="mk-33" ;;
e051d81c5c75) HOST="mk-34" ;;
e051d81c5dca) HOST="mk-39" ;;
e051d81c5d5c) HOST="mk-42" ;;
*) HOST="g9-$(echo $MAC | tail -c 5)" ;;
esac
hostnamectl set-hostname "$HOST"
echo "$HOST" > /etc/hostname
printf "127.0.1.1\t%s\n" "$HOST" >> /etc/hosts
# Disable cloud-init re-run
- |
mkdir -p /etc/cloud/cloud.cfg.d
echo "preserve_hostname: true" > /etc/cloud/cloud.cfg.d/99_preserve_hostname.cfg
touch /etc/cloud/cloud-init.disabled
# Install Docker
- |
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker jarvis
# Install Tailscale
- |
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list
apt-get update
apt-get install -y tailscale
# Clone ansible-pull repo
- |
sudo -u jarvis bash -c 'mkdir -p ~/.ansible-repo && cd ~/.ansible-repo && git clone https://gitea.nb.bobbysh.me/Iron-Legion/ansible-pull-deploy.git . 2>/dev/null || true'
# SSH hardening (keep PW auth enabled for fleet standard)
- |
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart ssh
# Enable UFW basic rules
- |
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enable
# Reboot to finalize
- reboot