6 Commits

Author SHA1 Message Date
9908286062 scripts: fix create-host flake.nix insertion point
Some checks failed
Run nix flake check / flake-check (pull_request) Successful in 2m12s
Run nix flake check / flake-check (push) Failing after 8m24s
Fix bug where new hosts were added outside of nixosConfigurations block
instead of inside it.

Issues fixed:
1. Pattern was looking for "packages =" but actual text is "packages = forAllSystems"
2. Replacement was putting new entry AFTER closing brace instead of BEFORE
3. testvm01 was at top-level flake output instead of in nixosConfigurations

Changes:
- Update pattern to match "packages = forAllSystems"
- Put new entry BEFORE the closing brace of nixosConfigurations
- Move testvm01 to correct location inside nixosConfigurations block

Result: nix flake show now correctly shows testvm01 as NixOS configuration

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:04 +01:00
cec496dda7 terraform: use local storage for cloud-init disks
Fix error "500 can't upload to storage type 'zfspool'" by using "local"
storage pool for cloud-init disks instead of the VM's storage pool.

Cloud-init disks require storage that supports ISO/snippet content types,
which zfspool does not. The "local" storage pool (directory-based) supports
this content type.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:04 +01:00
fca50562c3 terraform: fix cloud-init conditional type inconsistency
Fix OpenTofu error where static IP and DHCP branches had different object
structures in the subnets array. Move conditional to network_config level
so both branches return complete, consistent yamlencode() results.

Error was: "The true and false result expressions must have consistent types"

Solution: Make network_config itself conditional rather than the subnets
array, ensuring both branches return the same type (string from yamlencode).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:04 +01:00
1f1829dc2f docs: update terraform README for cloud-init refactoring
Remove mention of .generated/ directory and clarify that cloud-init.tf
manages all cloud-init disks, not just branch-specific ones.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:04 +01:00
21a32e0521 terraform: refactor cloud-init to use proxmox_cloud_init_disk resource
Replace SSH upload approach with native proxmox_cloud_init_disk resource
for cleaner, more maintainable cloud-init management.

Changes:
- Use proxmox_cloud_init_disk for all VMs (not just branch-specific ones)
- Include SSH keys, network config, and metadata in cloud-init disk
- Conditionally include NIXOS_FLAKE_BRANCH for VMs with flake_branch set
- Replace ide2 cloudinit disk with cdrom reference to cloud-init disk
- Remove built-in cloud-init parameters (ciuser, sshkeys, etc.)
- Remove cicustom parameter (no longer needed)
- Remove proxmox_host variable (no SSH uploads required)
- Remove .gitignore entry for .generated/ directory

Benefits:
- No SSH access to Proxmox required
- All cloud-init config managed in Terraform
- Consistent approach for all VMs
- Cleaner state management

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 17:41:04 +01:00
7fe0aa0f54 test: add testvm01 for pipeline testing 2026-02-01 17:41:04 +01:00
10 changed files with 151 additions and 79 deletions

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ terraform/terraform.tfvars
terraform/*.auto.tfvars terraform/*.auto.tfvars
terraform/crash.log terraform/crash.log
terraform/crash.*.log terraform/crash.*.log
terraform/.generated/

View File

@@ -334,6 +334,22 @@
sops-nix.nixosModules.sops sops-nix.nixosModules.sops
]; ];
}; };
testvm01 = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit inputs self sops-nix;
};
modules = [
(
{ config, pkgs, ... }:
{
nixpkgs.overlays = commonOverlays;
}
)
./hosts/testvm01
sops-nix.nixosModules.sops
];
};
}; };
packages = forAllSystems ( packages = forAllSystems (
{ pkgs }: { pkgs }:

View File

@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
../template2/hardware-configuration.nix
../../system
../../common/vm
];
nixpkgs.config.allowUnfree = true;
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/vda";
networking.hostName = "testvm01";
networking.domain = "home.2rjus.net";
networking.useNetworkd = true;
networking.useDHCP = false;
services.resolved.enable = false;
networking.nameservers = [
"10.69.13.5"
"10.69.13.6"
];
systemd.network.enable = true;
systemd.network.networks."ens18" = {
matchConfig.Name = "ens18";
address = [
"10.69.13.101/24"
];
routes = [
{ Gateway = "10.69.13.1"; }
];
linkConfig.RequiredForOnline = "routable";
};
time.timeZone = "Europe/Oslo";
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
nix.settings.tarball-ttl = 0;
environment.systemPackages = with pkgs; [
vim
wget
git
];
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
networking.firewall.enable = false;
system.stateVersion = "25.11"; # Did you read the comment?
}

View File

@@ -0,0 +1,5 @@
{ ... }: {
imports = [
./configuration.nix
];
}

View File

@@ -50,17 +50,17 @@ def update_flake_nix(config: HostConfig, repo_root: Path, force: bool = False) -
if count == 0: if count == 0:
raise ValueError(f"Could not find existing entry for {config.hostname} in flake.nix") raise ValueError(f"Could not find existing entry for {config.hostname} in flake.nix")
else: else:
# Insert new entry before closing brace # Insert new entry before closing brace of nixosConfigurations
# Pattern: " };\n packages =" # Pattern: " };\n packages = forAllSystems"
pattern = r"( \};)\n( packages =)" pattern = r"( \};)\n( packages = forAllSystems)"
replacement = rf"\g<1>\n{new_entry}\g<2>" replacement = rf"{new_entry}\g<1>\n\g<2>"
new_content, count = re.subn(pattern, replacement, content) new_content, count = re.subn(pattern, replacement, content)
if count == 0: if count == 0:
raise ValueError( raise ValueError(
"Could not find insertion point in flake.nix. " "Could not find insertion point in flake.nix. "
"Looking for pattern: ' };\\n packages ='" "Looking for pattern: ' };\\n packages = forAllSystems'"
) )
flake_path.write_text(new_content) flake_path.write_text(new_content)

View File

@@ -7,16 +7,15 @@
{ {
imports = [ imports = [
../template/hardware-configuration.nix ../template2/hardware-configuration.nix
../../system ../../system
../../common/vm ../../common/vm
]; ];
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
# Use the systemd-boot EFI boot loader.
boot.loader.grub.enable = true; boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda"; boot.loader.grub.device = "/dev/vda";
networking.hostName = "{{ hostname }}"; networking.hostName = "{{ hostname }}";
networking.domain = "{{ domain }}"; networking.domain = "{{ domain }}";

View File

@@ -198,11 +198,10 @@ deployment_summary = {
- `main.tf` - Provider configuration - `main.tf` - Provider configuration
- `variables.tf` - Variable definitions and defaults - `variables.tf` - Variable definitions and defaults
- `vms.tf` - VM definitions and deployment logic - `vms.tf` - VM definitions and deployment logic
- `cloud-init.tf` - Custom cloud-init configuration for branch-specific bootstrap - `cloud-init.tf` - Cloud-init disk management (SSH keys, networking, branch config)
- `outputs.tf` - Output definitions for deployed VMs - `outputs.tf` - Output definitions for deployed VMs
- `terraform.tfvars.example` - Example credentials file - `terraform.tfvars.example` - Example credentials file
- `terraform.tfvars` - Your actual credentials (gitignored) - `terraform.tfvars` - Your actual credentials (gitignored)
- `.generated/` - Auto-generated cloud-init files (gitignored)
- `vm.tf.old` - Archived single-VM configuration (reference) - `vm.tf.old` - Archived single-VM configuration (reference)
## Notes ## Notes

View File

@@ -1,55 +1,58 @@
# Cloud-init configuration for branch-specific bootstrap # Cloud-init configuration for all VMs
# #
# This file manages custom cloud-init snippets for VMs that need to bootstrap # This file manages cloud-init disks for all VMs using the proxmox_cloud_init_disk resource.
# from a specific git branch (non-master). Production VMs omit flake_branch # VMs with flake_branch set will include NIXOS_FLAKE_BRANCH environment variable.
# and use the default master branch.
# Generate cloud-init snippets for VMs with custom branch configuration resource "proxmox_cloud_init_disk" "ci" {
resource "local_file" "cloud_init_branch" { for_each = local.vm_configs
for_each = {
for name, vm in local.vm_configs : name => vm
if vm.flake_branch != null
}
filename = "${path.module}/.generated/cloud-init-${each.key}.yml" name = each.key
content = yamlencode({ pve_node = each.value.target_node
# Write NIXOS_FLAKE_BRANCH to /etc/environment storage = "local" # Cloud-init disks must be on storage that supports ISO/snippets
# This will be read by bootstrap.nix service via EnvironmentFile
write_files = [{ # User data includes SSH keys and optionally NIXOS_FLAKE_BRANCH
path = "/etc/environment" user_data = <<-EOT
content = "NIXOS_FLAKE_BRANCH=${each.value.flake_branch}\n" #cloud-config
append = true ssh_authorized_keys:
- ${each.value.ssh_public_key}
${each.value.flake_branch != null ? <<-BRANCH
write_files:
- path: /etc/environment
content: |
NIXOS_FLAKE_BRANCH=${each.value.flake_branch}
append: true
BRANCH
: ""}
EOT
# Network configuration - static IP or DHCP
network_config = each.value.ip != null ? yamlencode({
version = 1
config = [{
type = "physical"
name = "ens18"
subnets = [{
type = "static"
address = each.value.ip
gateway = each.value.gateway
dns_nameservers = split(" ", each.value.nameservers)
dns_search = [each.value.search_domain]
}]
}]
}) : yamlencode({
version = 1
config = [{
type = "physical"
name = "ens18"
subnets = [{
type = "dhcp"
}]
}] }]
}) })
file_permission = "0644" # Instance metadata
meta_data = yamlencode({
instance_id = sha1(each.key)
local-hostname = each.key
})
} }
# Upload cloud-init snippets to Proxmox
# Note: This requires SSH access to the Proxmox host
# Alternative: Manually copy files or use Proxmox API if available
resource "null_resource" "upload_cloud_init" {
for_each = {
for name, vm in local.vm_configs : name => vm
if vm.flake_branch != null
}
# Trigger re-upload when content changes
triggers = {
content_hash = local_file.cloud_init_branch[each.key].content
}
# Upload the cloud-init file to Proxmox snippets directory
provisioner "local-exec" {
command = <<-EOT
scp -o StrictHostKeyChecking=no \
${local_file.cloud_init_branch[each.key].filename} \
${var.proxmox_host}:/var/lib/vz/snippets/cloud-init-${each.key}.yml
EOT
}
depends_on = [local_file.cloud_init_branch]
}
# Ensure VMs depend on cloud-init being uploaded
# This is handled implicitly by the cicustom reference in vms.tf

View File

@@ -21,12 +21,6 @@ variable "proxmox_tls_insecure" {
default = true default = true
} }
variable "proxmox_host" {
description = "Proxmox host for SSH access (used to upload cloud-init snippets)"
type = string
default = "pve1.home.2rjus.net"
}
# Default values for VM configurations # Default values for VM configurations
# These can be overridden per-VM in vms.tf # These can be overridden per-VM in vms.tf

View File

@@ -31,6 +31,13 @@ locals {
# Example Minimal VM using all defaults (uncomment to deploy): # Example Minimal VM using all defaults (uncomment to deploy):
# "minimal-vm" = {} # "minimal-vm" = {}
# "bootstrap-verify-test" = {} # "bootstrap-verify-test" = {}
"testvm01" = {
ip = "10.69.13.101/24"
cpu_cores = 2
memory = 2048
disk_size = "20G"
flake_branch = "pipeline-testing-improvements"
}
} }
# Compute VM configurations with defaults applied # Compute VM configurations with defaults applied
@@ -97,8 +104,9 @@ resource "proxmox_vm_qemu" "vm" {
} }
ide { ide {
ide2 { ide2 {
cloudinit { # Reference the custom cloud-init disk created in cloud-init.tf
storage = each.value.storage cdrom {
iso = proxmox_cloud_init_disk.ci[each.key].id
} }
} }
} }
@@ -110,18 +118,6 @@ resource "proxmox_vm_qemu" "vm" {
# Agent # Agent
agent = 1 agent = 1
# Cloud-init configuration
ciuser = "root"
sshkeys = each.value.ssh_public_key
nameserver = each.value.nameservers
searchdomain = each.value.search_domain
# Network configuration - DHCP or static IP
ipconfig0 = each.value.ip != null ? "ip=${each.value.ip},gw=${each.value.gateway}" : "ip=dhcp"
# Custom cloud-init disk for branch configuration (if flake_branch is set)
cicustom = each.value.flake_branch != null ? "user=${each.value.storage}:snippets/cloud-init-${each.key}.yml" : null
# Skip IPv6 since we don't use it # Skip IPv6 since we don't use it
skip_ipv6 = true skip_ipv6 = true