22 Commits

Author SHA1 Message Date
f207f774de fix: parse terraform output JSON robustly in enroll step
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 19s
Handle setup-terraform wrapper prefixes by decoding from first JSON object before reading VM outputs.
2026-02-28 02:21:57 +00:00
83d277d144 feat: enroll tailscale via Proxmox guest agent by VMID
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 19s
Replace SSH/IP-based enrollment with Proxmox API guest-agent execution using Terraform outputs, set per-VM hostnames from resource names, and reset cloned tailscale state before join for unique node identities.
2026-02-28 02:14:39 +00:00
3335020db5 fix: make tailscale enrollment clone-safe and hostname-aware
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 17s
Reset cloned tailscale state before first join, remove one-shot marker dependency, and allow workflow host entries in host=hostname format so nodes join with VM-aligned tailscale names.
2026-02-28 02:01:48 +00:00
a7f68c0c4b fix: tolerate extra output in destroy guard parser
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 3m34s
Parse the first JSON object from terraform show output to avoid failures when extra non-JSON lines are present.
2026-02-28 01:23:07 +00:00
d1a7ccc98c chore: serialize Terraform workflows to prevent races
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 3m34s
Add global workflow concurrency group with queueing enabled so plan/apply/destroy runs do not overlap and contend for shared remote state.
2026-02-28 01:17:51 +00:00
afe19041d9 fix: make destroy guard parse tfplan JSON robustly
Some checks failed
Terraform Plan / Terraform Plan (push) Has been cancelled
Use terraform show with no-color and resilient JSON extraction to avoid parser failures when workflow output includes non-JSON noise.
2026-02-28 01:16:19 +00:00
c9be2a2fc8 fix: align VM boot disk and add Terraform safety workflows
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 3m35s
Switch VM boot order/disks to scsi0 to match cloned NixOS template boot layout, add destroy guards to plan/apply workflows, and replace destroy workflow with a confirmed manual dispatch nuke flow that uses remote B2 state.
2026-02-28 01:10:31 +00:00
47f950d667 fix: update S3 backend config for Terraform init
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 17s
Use non-deprecated s3 endpoint settings, switch to use_path_style, and trim newline characters from B2 credentials when generating backend.hcl in CI.
2026-02-28 00:56:12 +00:00
b0768db7a7 feat: store Terraform state in Backblaze B2
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 9s
Configure an s3 backend and initialize Terraform in CI with backend config from Gitea secrets so state persists across runs and apply operations stay consistent.
2026-02-28 00:52:40 +00:00
c0dd091b51 chore: align template base with live VM config
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 16s
Set NixOS stateVersion to 25.05 and include neovim in the default utility package set.
2026-02-28 00:44:08 +00:00
595df12b3e update: automate tailscale enrollment from Gitea secrets
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 16s
Add a first-boot tailscale enrollment service to the NixOS template and wire terraform-apply to inject TS auth key at runtime from secrets, so keys are not baked into templates or repo files.
2026-02-28 00:33:14 +00:00
e714a56980 update: switch Terraform to NixOS template workflow
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 17s
- Point clone_template to nixos-template and trim cloud-init to Nix-safe hostname/DNS only
- Remove SSH/Tailscale cloud-init variables and workflow secret dependencies
- Add reusable NixOS template-base config with bootloader, Tailscale, fish, and utility packages
2026-02-28 00:06:25 +00:00
4247d16c24 fix: upgrade proxmox provider for Proxmox 9 permissions
All checks were successful
Terraform Plan / Terraform Plan (push) Successful in 15s
Move Telmate provider to 3.0.2-rc07, which includes Proxmox 9 permission compatibility and avoids requiring deprecated VM.Monitor.
2026-02-27 21:04:44 +00:00
59fbbb07df fix: load static token id and validate token secret
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 14s
- Store non-sensitive Proxmox token id in terraform.tfvars
- Inject only token secret via workflow-generated secrets.auto.tfvars
- Add variable validations for token id format and non-empty token secret
- Add workflow debug output for token secret length and selected token id
2026-02-27 21:00:44 +00:00
c3a0ef251c debug: show secret lengths to verify they are set
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 15s
2026-02-27 20:56:41 +00:00
841abb8fe3 fix: create secrets.auto.tfvars dynamically in workflow
Some checks failed
Terraform Plan / Terraform Plan (push) Failing after 14s
- Generate secrets.auto.tfvars file during workflow run
- Terraform automatically loads *.auto.tfvars files
- This bypasses any issues with TF_VAR_ environment variables
2026-02-27 20:48:41 +00:00
364dc6b35b fix: use TF_VAR_ prefix for token credentials
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 13s
- Restore pm_api_token_id and pm_api_token_secret variables
- Use TF_VAR_pm_api_token_id and TF_VAR_pm_api_token_secret env vars
- This is the standard Terraform way to pass variables via environment
2026-02-27 20:43:39 +00:00
9c1476b6bf fix: use PM_API_TOKEN_ID/SECRET env vars directly
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 13s
- Remove token from Terraform variables (provider reads from env)
- Update workflows to set PM_API_TOKEN_ID and PM_API_TOKEN_SECRET directly
- Provider now reads credentials from environment variables
2026-02-27 20:36:44 +00:00
4a123e0fb6 fix: apply terraform fmt
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 14s
2026-02-27 20:27:20 +00:00
5633d18276 fix: terraform fmt alignment
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 11s
2026-02-27 20:22:44 +00:00
c6fc9edcc4 fix: terraform fmt formatting
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 12s
2026-02-27 20:06:23 +00:00
c8b86c7443 fix: switch to API token authentication for Proxmox
Some checks failed
Gitea Actions Demo / Terraform Plan (push) Failing after 11s
- Replace user/password auth with API token auth
- Update provider config to use pm_api_token_id and pm_api_token_secret
- Update workflow secrets to use PM_API_TOKEN_ID and PM_API_TOKEN_SECRET
- Remove unused pm_user and proxmox_password variables
2026-02-27 20:02:22 +00:00
12 changed files with 525 additions and 158 deletions

View File

@@ -1,29 +1,45 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is deploying with Terraform 🚀
name: Terraform Apply
on:
push:
branches:
- master
concurrency:
group: terraform-global
cancel-in-progress: false
jobs:
terraform:
name: "Terraform Apply"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
TF_VAR_SSH_KEY_PUBLIC: ${{ secrets.SSH_KEY_PUBLIC }}
TF_VAR_TS_AUTHKEY: ${{ secrets.TS_AUTHKEY }}
TF_VAR_proxmox_password: ${{ secrets.PROXMOX_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create secrets.tfvars
working-directory: terraform
run: |
cat > secrets.auto.tfvars << EOF
pm_api_token_secret = "${{ secrets.PM_API_TOKEN_SECRET }}"
EOF
cat > backend.hcl << EOF
bucket = "${{ secrets.B2_TF_BUCKET }}"
key = "terraform.tfstate"
region = "us-east-005"
endpoints = {
s3 = "${{ secrets.B2_TF_ENDPOINT }}"
}
access_key = "$(printf '%s' "${{ secrets.B2_KEY_ID }}" | tr -d '\r\n')"
secret_key = "$(printf '%s' "${{ secrets.B2_APPLICATION_KEY }}" | tr -d '\r\n')"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
use_path_style = true
EOF
- name: Set up Terraform
uses: hashicorp/setup-terraform@v2
with:
@@ -31,13 +47,176 @@ jobs:
- name: Terraform Init
working-directory: terraform
run: terraform init
run: terraform init -reconfigure -backend-config=backend.hcl
- name: Terraform Plan
working-directory: terraform
run: terraform plan
run: terraform plan -out=tfplan
- name: Block accidental destroy
env:
ALLOW_TF_DESTROY: ${{ secrets.ALLOW_TF_DESTROY }}
working-directory: terraform
run: |
terraform show -json -no-color tfplan > tfplan.json
DESTROY_COUNT=$(python3 -c 'import json; raw=open("tfplan.json","rb").read().decode("utf-8","ignore"); start=raw.find("{"); data=json.JSONDecoder().raw_decode(raw[start:])[0]; print(sum(1 for rc in data.get("resource_changes", []) if "delete" in rc.get("change", {}).get("actions", [])))')
echo "Planned deletes: $DESTROY_COUNT"
if [ "$DESTROY_COUNT" -gt 0 ] && [ "${ALLOW_TF_DESTROY}" != "true" ]; then
echo "Destroy actions detected. Set ALLOW_TF_DESTROY=true to allow."
exit 1
fi
- name: Terraform Apply
working-directory: terraform
run: terraform apply -auto-approve
run: terraform apply -auto-approve tfplan
- name: Enroll VMs in Tailscale
env:
TS_AUTHKEY: ${{ secrets.TS_AUTHKEY }}
PM_API_TOKEN_SECRET: ${{ secrets.PM_API_TOKEN_SECRET }}
working-directory: terraform
run: |
if [ -z "$TS_AUTHKEY" ] || [ -z "$PM_API_TOKEN_SECRET" ]; then
echo "Skipping Tailscale enrollment (missing TS_AUTHKEY or PM_API_TOKEN_SECRET)."
exit 0
fi
PM_API_URL=$(awk -F'"' '/^pm_api_url/{print $2}' terraform.tfvars)
PM_API_TOKEN_ID=$(awk -F'"' '/^pm_api_token_id/{print $2}' terraform.tfvars)
TARGET_NODE=$(awk -F'"' '/^target_node/{print $2}' terraform.tfvars)
export PM_API_URL PM_API_TOKEN_ID TARGET_NODE
terraform output -json > tfoutputs.json
cat > enroll_tailscale.py <<'PY'
import json
import os
import ssl
import sys
import time
import urllib.parse
import urllib.request
api_url = os.environ["PM_API_URL"].rstrip("/")
if api_url.endswith("/api2/json"):
api_url = api_url[: -len("/api2/json")]
token_id = os.environ["PM_API_TOKEN_ID"].strip()
token_secret = os.environ["PM_API_TOKEN_SECRET"].strip()
target_node = os.environ["TARGET_NODE"].strip()
ts_authkey = os.environ["TS_AUTHKEY"]
if not token_id or not token_secret:
raise SystemExit("Missing Proxmox token id/secret")
raw_outputs = open("tfoutputs.json", "rb").read().decode("utf-8", "ignore")
start = raw_outputs.find("{")
if start == -1:
raise SystemExit("Could not find JSON payload in terraform output")
outputs = json.JSONDecoder().raw_decode(raw_outputs[start:])[0]
targets = []
for output_name in ("alpaca_vm_ids", "llama_vm_ids"):
mapping = outputs.get(output_name, {}).get("value", {})
if isinstance(mapping, dict):
for hostname, vmid in mapping.items():
targets.append((str(hostname), int(vmid)))
if not targets:
print("No VMs found in terraform outputs; skipping tailscale enrollment")
raise SystemExit(0)
print("Tailscale enrollment targets:", ", ".join(f"{h}:{v}" for h, v in targets))
ssl_ctx = ssl._create_unverified_context()
auth_header = f"PVEAPIToken={token_id}={token_secret}"
def api_request(method, path, data=None):
url = f"{api_url}{path}"
headers = {"Authorization": auth_header}
body = None
if data is not None:
body = urllib.parse.urlencode(data, doseq=True).encode("utf-8")
headers["Content-Type"] = "application/x-www-form-urlencoded"
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req, context=ssl_ctx, timeout=30) as resp:
payload = resp.read().decode("utf-8")
return json.loads(payload)
def wait_for_guest_agent(vmid, timeout_seconds=420):
deadline = time.time() + timeout_seconds
while time.time() < deadline:
try:
res = api_request("GET", f"/api2/json/nodes/{target_node}/qemu/{vmid}/agent/ping")
if res.get("data") == "pong":
return True
except Exception:
pass
time.sleep(5)
return False
def exec_guest(vmid, command):
res = api_request(
"POST",
f"/api2/json/nodes/{target_node}/qemu/{vmid}/agent/exec",
{
"command": "/run/current-system/sw/bin/sh",
"extra-args": ["-lc", command],
},
)
pid = res["data"]["pid"]
for _ in range(120):
status = api_request(
"GET",
f"/api2/json/nodes/{target_node}/qemu/{vmid}/agent/exec-status?pid={pid}",
).get("data", {})
if status.get("exited"):
return (
int(status.get("exitcode", 1)),
status.get("out-data", ""),
status.get("err-data", ""),
)
time.sleep(2)
return (124, "", "Timed out waiting for guest command")
failures = []
safe_key = ts_authkey.replace("'", "'\"'\"'")
for hostname, vmid in targets:
print(f"\n== Enrolling {hostname} (vmid {vmid}) ==")
if not wait_for_guest_agent(vmid):
failures.append(f"{hostname}: guest agent not ready")
print(f"ERROR: guest agent not ready for vmid {vmid}")
continue
safe_hostname = hostname.replace("'", "'\"'\"'")
cmd = (
"set -e; "
f"printf '%s' '{safe_key}' > /etc/tailscale/authkey; "
f"printf '%s' '{safe_hostname}' > /etc/tailscale/hostname; "
"chmod 600 /etc/tailscale/authkey; "
f"hostnamectl set-hostname '{safe_hostname}' || true; "
"systemctl restart tailscaled; "
"systemctl start tailscale-firstboot.service; "
"tailscale status || true"
)
exitcode, stdout, stderr = exec_guest(vmid, cmd)
if stdout:
print(stdout)
if stderr:
print(stderr, file=sys.stderr)
if exitcode != 0:
failures.append(f"{hostname}: command failed exit {exitcode}")
print(f"ERROR: tailscale enrollment failed for {hostname} (exit {exitcode})")
if failures:
print("\nEnrollment failures:")
for failure in failures:
print(f"- {failure}")
raise SystemExit(1)
print("\nTailscale enrollment completed for all managed VMs")
PY
python3 enroll_tailscale.py

View File

@@ -1,28 +1,65 @@
name: Gitea Destroy Terraform
run-name: ${{ gitea.actor }} triggered a Terraform Destroy 🧨
name: Terraform Destroy
run-name: ${{ gitea.actor }} requested Terraform destroy
on:
workflow_dispatch: # Manual trigger
workflow_dispatch:
inputs:
confirm:
description: "Type NUKE to confirm destroy"
required: true
type: string
target:
description: "Destroy scope"
required: true
default: all
type: choice
options:
- all
- alpacas
- llamas
concurrency:
group: terraform-global
cancel-in-progress: false
jobs:
destroy:
name: "Terraform Destroy"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
TF_VAR_SSH_KEY: ${{ secrets.TF_VAR_SSH_KEY_PUBLIC }}
TF_VAR_TS_AUTHKEY: ${{ secrets.TF_VAR_TS_AUTHKEY }}
TF_VAR_PROXMOX_PASSWORD: ${{ secrets.TF_VAR_PROXMOX_PASSWORD }}
steps:
- name: Validate confirmation phrase
run: |
if [ "${{ inputs.confirm }}" != "NUKE" ]; then
echo "Confirmation failed. You must type NUKE."
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v4
- name: Create Terraform secret files
working-directory: terraform
run: |
cat > secrets.auto.tfvars << EOF
pm_api_token_secret = "${{ secrets.PM_API_TOKEN_SECRET }}"
EOF
cat > backend.hcl << EOF
bucket = "${{ secrets.B2_TF_BUCKET }}"
key = "terraform.tfstate"
region = "us-east-005"
endpoints = {
s3 = "${{ secrets.B2_TF_ENDPOINT }}"
}
access_key = "$(printf '%s' "${{ secrets.B2_KEY_ID }}" | tr -d '\r\n')"
secret_key = "$(printf '%s' "${{ secrets.B2_APPLICATION_KEY }}" | tr -d '\r\n')"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
use_path_style = true
EOF
- name: Set up Terraform
uses: hashicorp/setup-terraform@v2
with:
@@ -30,9 +67,27 @@ jobs:
- name: Terraform Init
working-directory: terraform
run: terraform init
run: terraform init -reconfigure -backend-config=backend.hcl
- name: Terraform Destroy
- name: Terraform Destroy Plan
working-directory: terraform
run: terraform destroy -auto-approve
run: |
case "${{ inputs.target }}" in
all)
terraform plan -destroy -out=tfdestroy
;;
alpacas)
terraform plan -destroy -target=proxmox_vm_qemu.alpacas -out=tfdestroy
;;
llamas)
terraform plan -destroy -target=proxmox_vm_qemu.llamas -out=tfdestroy
;;
*)
echo "Invalid destroy target: ${{ inputs.target }}"
exit 1
;;
esac
- name: Terraform Destroy Apply
working-directory: terraform
run: terraform apply -auto-approve tfdestroy

View File

@@ -1,5 +1,4 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
name: Terraform Plan
on:
push:
@@ -7,24 +6,46 @@ on:
- stage
- test
concurrency:
group: terraform-global
cancel-in-progress: false
jobs:
terraform:
name: "Terraform Plan"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
TF_VAR_SSH_KEY_PUBLIC: ${{ secrets.SSH_KEY_PUBLIC }}
TF_VAR_TS_AUTHKEY: ${{ secrets.TS_AUTHKEY }}
TF_VAR_proxmox_password: ${{ secrets.PROXMOX_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create secrets.tfvars
working-directory: terraform
run: |
echo "PM_API_TOKEN_SECRET length: $(echo -n '${{ secrets.PM_API_TOKEN_SECRET }}' | wc -c)"
cat > secrets.auto.tfvars << EOF
pm_api_token_secret = "${{ secrets.PM_API_TOKEN_SECRET }}"
EOF
cat > backend.hcl << EOF
bucket = "${{ secrets.B2_TF_BUCKET }}"
key = "terraform.tfstate"
region = "us-east-005"
endpoints = {
s3 = "${{ secrets.B2_TF_ENDPOINT }}"
}
access_key = "$(printf '%s' "${{ secrets.B2_KEY_ID }}" | tr -d '\r\n')"
secret_key = "$(printf '%s' "${{ secrets.B2_APPLICATION_KEY }}" | tr -d '\r\n')"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
use_path_style = true
EOF
echo "Created secrets.auto.tfvars:"
cat secrets.auto.tfvars | sed 's/=.*/=***/'
echo "Using token ID from terraform.tfvars:"
grep '^pm_api_token_id' terraform.tfvars
- name: Set up Terraform
uses: hashicorp/setup-terraform@v2
with:
@@ -32,7 +53,7 @@ jobs:
- name: Terraform Init
working-directory: terraform
run: terraform init
run: terraform init -reconfigure -backend-config=backend.hcl
- name: Terraform Format Check
working-directory: terraform
@@ -46,9 +67,21 @@ jobs:
working-directory: terraform
run: terraform plan -out=tfplan
- name: Block accidental destroy
env:
ALLOW_TF_DESTROY: ${{ secrets.ALLOW_TF_DESTROY }}
working-directory: terraform
run: |
terraform show -json -no-color tfplan > tfplan.json
DESTROY_COUNT=$(python3 -c 'import json; raw=open("tfplan.json","rb").read().decode("utf-8","ignore"); start=raw.find("{"); data=json.JSONDecoder().raw_decode(raw[start:])[0]; print(sum(1 for rc in data.get("resource_changes", []) if "delete" in rc.get("change", {}).get("actions", [])))')
echo "Planned deletes: $DESTROY_COUNT"
if [ "$DESTROY_COUNT" -gt 0 ] && [ "${ALLOW_TF_DESTROY}" != "true" ]; then
echo "Destroy actions detected. Set ALLOW_TF_DESTROY=true to allow."
exit 1
fi
- name: Upload Terraform Plan
uses: actions/upload-artifact@v3
with:
name: terraform-plan
path: terraform/tfplan

View File

@@ -0,0 +1,27 @@
# NixOS Proxmox Template Base
This folder contains a minimal NixOS base config you can copy into a new
template VM build.
## Files
- `flake.nix`: pins `nixos-24.11` and exposes one host config.
- `configuration.nix`: base settings for Proxmox guest use.
## Before first apply
1. Replace `REPLACE_WITH_YOUR_SSH_PUBLIC_KEY` in `configuration.nix`.
2. Add `hardware-configuration.nix` from the VM install:
- `nixos-generate-config --root /`
- copy `/etc/nixos/hardware-configuration.nix` next to `configuration.nix`
## Build/apply example inside the VM
```bash
sudo nixos-rebuild switch --flake .#template
```
## Notes
- This is intentionally minimal and avoids cloud-init assumptions.
- If you want host-specific settings, create additional modules and import them.

View File

@@ -0,0 +1,90 @@
{ lib, pkgs, ... }:
{
imports =
lib.optional (builtins.pathExists ./hardware-configuration.nix)
./hardware-configuration.nix;
networking.hostName = "nixos-template";
networking.useDHCP = lib.mkDefault true;
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.grub = {
enable = true;
device = "/dev/sda";
};
services.qemuGuest.enable = true;
services.openssh.enable = true;
services.tailscale.enable = true;
services.openssh.settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "prohibit-password";
};
programs.fish.enable = true;
users.users.micqdf = {
isNormalUser = true;
extraGroups = [ "wheel" ];
shell = pkgs.fish;
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDyfhho9WSqK2OWxizt45Q5KHgox3uVWDnbvMBJaDnRph6CZeKmzaS60/+HN/o7MtIm+q86TfdYeWJVt4erPEvrYN8AWfvCWi+hP2Y0l18wS8GEA+efEXyQ5CLCefraXvIneORObKetzO73bq0HytDRXDowc4J0NcbEFB7ncf2RqVTC6QRlNPRD3jHLkUeKXVmyteNgTtGdMz4MFHCC7xtzgL7kEuuHDEWuVhPkK+dkeGBejq+RzkYcd8v37L7NjFZCK91jANBVcQnTLQVUVVlMovVPyoaROn4N8KpIhb85SYZIJGUEKMhmCowb2NnZLJNC07qn8sz1dmNZO635aquuWMhZTevCySJjvIuMxDSffhBaAjkK1aVixMCW3jyzbpFIEG6FOj27TpcMnen6a0j0AecdCKgXI/Ezb08pj9qmVppAvJPyYoqN4OwHNHGWb8U2X3GghFesei8ZmBgch12RkIaXYxVzkNqv3FG4kAMFMEnGe4e6aqAAuDzUIkcjsPl2XrNJp+pxnPWDc7EMTKPUuKIcteXVDgCVgufQjPBO5/DgUyygLTzt8py9sZyyFDsqRAZ6E3IzBpxyWfUOoN81mUL6G31pZ/1b3YKpNs7DuqvP/aXIvb94o8KsLPQeoG7L2ulcOWX7I0yhlAgd8QUjhNoNq3mK/sQylq9Zy63GhQ=="
];
# optional while testing noVNC login:
# initialPassword = "changeme123";
};
security.sudo.wheelNeedsPassword = false;
systemd.services.tailscale-firstboot = {
description = "One-time Tailscale enrollment";
after = [ "network-online.target" "tailscaled.service" ];
wants = [ "network-online.target" "tailscaled.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
if [ ! -s /etc/tailscale/authkey ]; then
exit 0
fi
key="$(cat /etc/tailscale/authkey)"
ts_hostname=""
if [ -s /etc/tailscale/hostname ]; then
ts_hostname="--hostname=$(cat /etc/tailscale/hostname)"
fi
rm -f /var/lib/tailscale/tailscaled.state
${pkgs.tailscale}/bin/tailscale up --reset --auth-key="$key" $ts_hostname
rm -f /etc/tailscale/authkey
rm -f /etc/tailscale/hostname
'';
};
environment.systemPackages = with pkgs; [
btop
curl
dig
eza
fd
fzf
git
htop
jq
ripgrep
tailscale
tree
unzip
vim
neovim
wget
];
system.stateVersion = "25.05";
}

View File

@@ -0,0 +1,14 @@
{
description = "Base NixOS config for Proxmox template";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
};
outputs = { nixpkgs, ... }: {
nixosConfigurations.template = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
};
}

View File

@@ -2,21 +2,21 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/local" {
version = "2.5.2"
version = "2.7.0"
hashes = [
"h1:JlMZD6nYqJ8sSrFfEAH0Vk/SL8WLZRmFaMUF9PJK5wM=",
"zh:136299545178ce281c56f36965bf91c35407c11897f7082b3b983d86cb79b511",
"zh:3b4486858aa9cb8163378722b642c57c529b6c64bfbfc9461d940a84cd66ebea",
"zh:4855ee628ead847741aa4f4fc9bed50cfdbf197f2912775dd9fe7bc43fa077c0",
"zh:4b8cd2583d1edcac4011caafe8afb7a95e8110a607a1d5fb87d921178074a69b",
"zh:52084ddaff8c8cd3f9e7bcb7ce4dc1eab00602912c96da43c29b4762dc376038",
"zh:71562d330d3f92d79b2952ffdda0dad167e952e46200c767dd30c6af8d7c0ed3",
"h1:2RYa3j7m/0WmET2fqotY4CHxE1Hpk0fgn47/126l+Og=",
"zh:261fec71bca13e0a7812dc0d8ae9af2b4326b24d9b2e9beab3d2400fab5c5f9a",
"zh:308da3b5376a9ede815042deec5af1050ec96a5a5410a2206ae847d82070a23e",
"zh:3d056924c420464dc8aba10e1915956b2e5c4d55b11ffff79aa8be563fbfe298",
"zh:643256547b155459c45e0a3e8aab0570db59923c68daf2086be63c444c8c445b",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:805f81ade06ff68fa8b908d31892eaed5c180ae031c77ad35f82cb7a74b97cf4",
"zh:8b6b3ebeaaa8e38dd04e56996abe80db9be6f4c1df75ac3cccc77642899bd464",
"zh:ad07750576b99248037b897de71113cc19b1a8d0bc235eb99173cc83d0de3b1b",
"zh:b9f1c3bfadb74068f5c205292badb0661e17ac05eb23bfe8bd809691e4583d0e",
"zh:cc4cbcd67414fefb111c1bf7ab0bc4beb8c0b553d01719ad17de9a047adff4d1",
"zh:7aa4d0b853f84205e8cf79f30c9b2c562afbfa63592f7231b6637e5d7a6b5b27",
"zh:7dc251bbc487d58a6ab7f5b07ec9edc630edb45d89b761dba28e0e2ba6b1c11f",
"zh:7ee0ca546cd065030039168d780a15cbbf1765a4c70cd56d394734ab112c93da",
"zh:b1d5d80abb1906e6c6b3685a52a0192b4ca6525fe090881c64ec6f67794b1300",
"zh:d81ea9856d61db3148a4fc6c375bf387a721d78fc1fea7a8823a027272a47a78",
"zh:df0a1f0afc947b8bfc88617c1ad07a689ce3bd1a29fd97318392e6bdd32b230b",
"zh:dfbcad800240e0c68c43e0866f2a751cff09777375ec701918881acf67a268da",
]
}
@@ -38,23 +38,23 @@ provider "registry.terraform.io/hashicorp/template" {
}
provider "registry.terraform.io/telmate/proxmox" {
version = "3.0.1-rc8"
constraints = "3.0.1-rc8"
version = "3.0.2-rc07"
constraints = "3.0.2-rc07"
hashes = [
"h1:W5X4T5AZUaqO++aAequNECUKJaXLC5upcws6Vp7mkBk=",
"zh:0272f1600251abf9b139c2683f83cde0a907ac762f5ead058b84de18ddc1d78e",
"zh:328e708a8063a133516612b17c8983a9372fa42766530925d1d37aeb1daa30ec",
"zh:3449150e4d57f79af6f9583e93e3a5ab84fb475bc594de75b968534f57af2871",
"zh:58d803a0203241214f673c80350d43ce1a5ce57b21b83ba08d0d08e8c389dcc4",
"zh:59e3e99afc1ea404e530100725403c1610d682cfd27eeeaf35190c119b76a4db",
"zh:666cb7d299824152714202e8fda000c2e37346f2ae6d0a0e3c6f6bd68ef5d9ca",
"zh:6a1290b85e7bf953664b21b2a1ea554923a060f2a8347d8d5bb3d2b5157f85d2",
"zh:72230960c49fe7050a5e80ee10fa24cdac94dbab82744bccb6aa251741eb5aa9",
"zh:91f655c41f5af9a9fdcf6104c3d0a553eaa0fb3390af81051e744f30accd5b52",
"zh:aa08a22bf737d5840573bb6030617ab6bba2a292f4b9c88b20477cdcfb9676a9",
"zh:b72012cc284cad488207532b6668c58999c972d837b5f486db1d7466d686d5fd",
"zh:e24f934249a6ab4d3705c1398226d4d9df1e81ef8a36592389be02bc35cc661f",
"zh:e9e6bcef8b6a6b5ff2317168c2c23e4c55ae23f883ba158d2c4fd6324a0413e5",
"zh:ffa1e742a8c50babd8dbfcd6884740f9bea8453ec4d832717ff006a4fbfffa91",
"h1:zp5hpQJQ4t4zROSLqdltVpBO+Riy9VugtfFbpyTw1aM=",
"zh:2ee860cd0a368b3eaa53f4a9ea46f16dab8a97929e813ea6ef55183f8112c2ca",
"zh:415965fd915bae2040d7f79e45f64d6e3ae61149c10114efeac1b34687d7296c",
"zh:6584b2055df0e32062561c615e3b6b2c291ca8c959440adda09ef3ec1e1436bd",
"zh:65dcfad71928e0a8dd9befc22524ed686be5020b0024dc5cca5184c7420eeb6b",
"zh:7253dc29bd265d33f2791ac4f779c5413f16720bb717de8e6c5fcb2c858648ea",
"zh:7ec8993da10a47606670f9f67cfd10719a7580641d11c7aa761121c4a2bd66fb",
"zh:999a3f7a9dcf517967fc537e6ec930a8172203642fb01b8e1f78f908373db210",
"zh:a50e6df7280eb6584a5fd2456e3f5b6df13b2ec8a7fa4605511e438e1863be42",
"zh:b25b329a1e42681c509d027fee0365414f0cc5062b65690cfc3386aab16132ae",
"zh:c028877fdb438ece48f7bc02b65bbae9ca7b7befbd260e519ccab6c0cbb39f26",
"zh:cf0eaa3ea9fcc6d62793637947f1b8d7c885b6ad74695ab47e134e4ff132190f",
"zh:d5ade3fae031cc629b7c512a7b60e46570f4c41665e88a595d7efd943dde5ab2",
"zh:f388c15ad1ecfc09e7361e3b98bae9b627a3a85f7b908c9f40650969c949901c",
"zh:f415cc6f735a3971faae6ac24034afdb9ee83373ef8de19a9631c187d5adc7db",
]
}

View File

@@ -2,10 +2,8 @@ data "template_file" "cloud_init_global" {
template = file("${path.module}/files/cloud_init_global.tpl")
vars = {
hostname = "generic"
domain = "home.arpa"
TS_AUTHKEY = var.TS_AUTHKEY
SSH_KEY_PUBLIC = var.SSH_KEY_PUBLIC
hostname = "generic"
domain = "home.arpa"
}
}

View File

@@ -6,42 +6,5 @@ resolv_conf:
- 8.8.8.8
- 1.1.1.1
package_update: true
package_upgrade: true
# APT fails to acquire GPG keys if package dirmngr is missing
bootcmd:
- [ cloud-init-per, once, dirmngr-aptupdate, apt-get, update ]
- [ cloud-init-per, once, dirmngr-aptinstall, apt-get, install, dirmngr, -y ]
- [ cloud-init-per, once, dirmngr-aptinstall, apt-get, install, gnupg2, -y ]
packages:
- jq
- curl
- qemu-guest-agent
users:
- name: stuart
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ${SSH_KEY_PUBLIC}
preserve_hostname: false
fqdn: ${hostname}.${domain}
#cloud-config
# The above header must generally appear on the first line of a cloud config
# file, but all other lines that begin with a # are optional comments.
runcmd:
# One-command install, from https://tailscale.com/download/
- ['sh', '-c', 'curl -fsSL https://tailscale.com/install.sh | sh']
# Set sysctl settings for IP forwarding (useful when configuring an exit node)
- ['sh', '-c', "echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && sudo sysctl -p /etc/sysctl.d/99-tailscale.conf" ]
# Generate an auth key from your Admin console
# https://login.tailscale.com/admin/settings/keys
# and replace the placeholder below
- ['tailscale', 'up', '--auth-key=${TS_AUTHKEY}']

View File

@@ -1,17 +1,19 @@
terraform {
backend "s3" {}
required_providers {
proxmox = {
source = "Telmate/proxmox"
version = "3.0.1-rc8"
version = "3.0.2-rc07"
}
}
}
provider "proxmox" {
pm_api_url = var.pm_api_url
pm_user = var.pm_user
pm_password = var.proxmox_password
pm_tls_insecure = true
pm_api_url = var.pm_api_url
pm_api_token_id = var.pm_api_token_id
pm_api_token_secret = var.pm_api_token_secret
pm_tls_insecure = true
}
resource "proxmox_vm_qemu" "alpacas" {
@@ -24,19 +26,21 @@ resource "proxmox_vm_qemu" "alpacas" {
os_type = "cloud-init"
agent = 1
sockets = var.sockets
cores = var.cores
cpu {
sockets = var.sockets
cores = var.cores
}
memory = var.memory
scsihw = "virtio-scsi-pci"
boot = "order=virtio0"
bootdisk = "virtio0"
boot = "order=scsi0"
bootdisk = "scsi0"
ipconfig0 = "ip=dhcp"
cicustom = "user=local:snippets/cloud_init_global.yaml"
disks {
virtio {
virtio0 {
scsi {
scsi0 {
disk {
size = var.disk_size
storage = var.storage
@@ -71,18 +75,20 @@ resource "proxmox_vm_qemu" "llamas" {
os_type = "cloud-init"
agent = 1
sockets = var.sockets
cores = var.cores
cpu {
sockets = var.sockets
cores = var.cores
}
memory = var.memory
scsihw = "virtio-scsi-pci"
boot = "order=virtio0"
bootdisk = "virtio0"
boot = "order=scsi0"
bootdisk = "scsi0"
ipconfig0 = "ip=dhcp"
cicustom = "user=local:snippets/cloud_init_global.yaml"
disks {
virtio {
virtio0 {
scsi {
scsi0 {
disk {
size = var.disk_size
storage = var.storage
@@ -106,4 +112,3 @@ resource "proxmox_vm_qemu" "llamas" {
bridge = var.bridge
}
}

View File

@@ -1,10 +1,10 @@
target_node = "flex"
clone_template = "ubuntu-cloudinit"
cores = 1
memory = 1024
disk_size = "15G"
sockets = 1
bridge = "vmbr0"
storage = "Flash"
pm_api_url = "https://100.105.0.115:8006/api2/json"
pm_user = "terraform-prov@pve"
target_node = "flex"
clone_template = "nixos-template"
cores = 1
memory = 1024
disk_size = "15G"
sockets = 1
bridge = "vmbr0"
storage = "Flash"
pm_api_url = "https://100.105.0.115:8006/api2/json"
pm_api_token_id = "terraform-prov@pve!mytoken"

View File

@@ -1,5 +1,22 @@
variable "proxmox_password" {
type = string
variable "pm_api_token_id" {
type = string
description = "Proxmox API token ID (format: user@realm!tokenid)"
validation {
condition = can(regex(".+!.+", trimspace(var.pm_api_token_id)))
error_message = "pm_api_token_id must be in format user@realm!tokenid."
}
}
variable "pm_api_token_secret" {
type = string
sensitive = true
description = "Proxmox API token secret"
validation {
condition = length(trimspace(var.pm_api_token_secret)) > 0
error_message = "pm_api_token_secret cannot be empty. Check your Gitea secret PM_API_TOKEN_SECRET."
}
}
variable "target_node" {
@@ -38,10 +55,6 @@ variable "pm_api_url" {
type = string
}
variable "pm_user" {
type = string
}
variable "alpaca_vm_count" {
type = number
default = 1
@@ -53,13 +66,3 @@ variable "llama_vm_count" {
default = 1
description = "How many Llama VMs to create"
}
variable "TS_AUTHKEY" {
type = string
description = "Tailscale auth key used in cloud-init"
}
variable "SSH_KEY_PUBLIC" {
type = string
description = "My Public SSH key for ssh auth list"
}