From c25586f80f4df3801ca59ec9bc5c3ece00985056 Mon Sep 17 00:00:00 2001 From: Chewbaccalakis Date: Fri, 19 Jun 2026 14:59:55 -0700 Subject: [PATCH] adding initial files --- .terraform.lock.hcl | 110 ++++++++++++++++++++++++++++++ ansible/inventories/generated.yml | 22 ++++++ backend.tf | 23 +++++++ main.tf | 79 +++++++++++++++++++++ providers.tf | 24 +++++++ secrets.tmpl | 13 ++++ terraform.tfvars | 32 +++++++++ vars.tf | 93 +++++++++++++++++++++++++ 8 files changed, 396 insertions(+) create mode 100644 .terraform.lock.hcl create mode 100644 ansible/inventories/generated.yml create mode 100644 backend.tf create mode 100644 main.tf create mode 100644 providers.tf create mode 100644 secrets.tmpl create mode 100644 terraform.tfvars create mode 100644 vars.tf diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..48adea9 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,110 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/external" { + version = "2.4.0" + hashes = [ + "h1:/+SUsgcSNW/RVmImj/m1Z+hSUU5BUMu8CMbLwyuhMMU=", + "h1:/MpYrEgBd7Gu/lyuyViR4ccaYSpHHIiHZfIxy7AGy2U=", + "h1:0SJyK1GT4ma4uTSUz8Y619fc1xRTzJ9FKFvcpvZlSIo=", + "h1:4WFLgDJ0NXqNlKJ2UFniW8/GMMterkrWBXmi/+yW2do=", + "h1:5vgd2l3wLBjETL3h0npHKNm8efKSE7Xb2uxh+8cysm8=", + "h1:6dSIL7ou4oq3hcuRUx4OLRc31PQ1oAC1d885hnQlOh0=", + "h1:7OLUuwL6BIhAYehzbDX5NVTIoDAIBf2dYZGrpSKe0r0=", + "h1:9PmuE7y6euSGJtnl7W0TIBvZBU1pNojfa615jnJAiIs=", + "h1:ME8EoyLapCOOAzPP8uPTm+OSF7nil1r0wOeJ+Q9KNZ8=", + "h1:PxaDF/WKlkCtfmiqzws5/23jq3QgcoJ5TMeEIimtvDQ=", + "h1:auXuC9e9AsQxrYQj6OyYgqKHgjry0IkJnKHpPShoLHM=", + "h1:eBAcMZAh1wmCyYQMCIPyXljazVRYAHkEu/31RbZD6Sw=", + "h1:jkfHEdIUc6uxIm8zuw0lVZliedeqgzDE9qb1Y608VUI=", + "h1:mC6Rw0cABPfsBZLVXfdOByUUW1hHkfscaQufTzVM+rw=", + "h1:nE1mN6A/5lJVLiNQmVm43m9ME2uHlqwIYUVxlZgblSk=", + "zh:483962b782cee2c970f4bdf6118e4bb665f37a0d488024c660b7a7c9853afc93", + "zh:4a8b42651f6de0ea93854ece3ccdf8e2b21b911145859402a9a6ec6ecf31a23c", + "zh:56836ea1468cb328e98cccc76cccc208fb7336513bc76de76309541b8e5ceef2", + "zh:6d30c2c1aff7e0ddcfffdf815e570f5ed8b77d1ce2d31440c7c50c6f3e7cbe97", + "zh:80a21a23bd9bfafb74e4fc5f2a0e2bc33517c418d5f16dc020b7febba9ef6cd2", + "zh:95d9ce0e6407f199f7e9d3aefc3345a6e67c18f0c8280207417272cbb3f973ad", + "zh:98e46c6504da5f1020489730d23f03a1337e643ed452f271c2a13e6af4687a16", + "zh:a3f59b81da319a87bb3ac4a31e669874a090f082959da29975fa6f11f53b4731", + "zh:a5793f88bb25000d6e6b8b2cd5ca71366a8d4e373e17a247089a80331ccf824e", + "zh:cfc9af183162936b2e9a726c4a68d773b4511ada23b2c764f5add90f080e747d", + "zh:d85e722d771fb7865d9d5b591334d0f2b7598b7e66f534f93923effe6d5134ed", + "zh:de6e5d954ef91bb0ad41f30305ab8c31718ea39308523529f528babd0b27db71", + "zh:fca19dda5e05221e231d400fc39fda4f9e9abfe33e8e833a215eb094fe226ed8", + "zh:fcf67b347f2dc3c609c819ad5f27078fa3d0235311cfe5e33242eb49b3a4a6a8", + "zh:fea69a81ffcd63cb776b0f2c13a99a8a6d64e0b9de111ea1926bc4e713023ece", + ] +} + +provider "registry.opentofu.org/hashicorp/local" { + version = "2.9.0" + constraints = "~> 2.2" + hashes = [ + "h1:1dtKYW/5a1qob3yneL6WzOlnSGfYtJ6a2XeejCk9yb4=", + "h1:5NseXq5wU8O20ersTtV4ocrLYFFtgFr7n0pRLO1W2Rw=", + "h1:5d22ZPPK4iiygPbwRz/PJF5Es/0axVpMlPRpCR0Padw=", + "h1:AnwyolirmIlBMjH6+tV8bKkvT+5axJNYxi2y2IguiX4=", + "h1:PBp+HeseY021Fw3sLznCG27idgwPoff4cBuNmKgPL2w=", + "h1:VDxIhe4GbzdOCdmt7mQaqdwERQW6GSI7Roonts42Gr0=", + "h1:ZO6eWWnf8LjjV1q/JNeL9WLtZ6fwIttOnyN5LjCNSEo=", + "h1:dPIAf8oUAz+vW2E0iZunMvpuPddRZIztRsPSY1u+VnY=", + "h1:fwTDVG9AhFVKQZIb1EXkHv4FqzsZNlLWgkyPGDmZZEE=", + "h1:kDc465XPC7/6XFCjrMC4mTqhA9ef0FHKuJ3ZgfGNfeg=", + "h1:kGbjxrI2P8MHeyVtE1U3Q1TbyF71ExnHxtkrE+Aj6UU=", + "h1:kcoK6Afbsj54u9zaEqpecWAFKytqjBijtguCNwV3d4M=", + "h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=", + "h1:t0CMn/Rkwquw8l2yQ+O4ApzbMZfY2UazbsDnZygzACA=", + "h1:tJwgm2BS4xCGlElCDQEFXQoefY9Y4t0JdSKTtsPBbBo=", + "zh:13ef7ecd1e397ec5b20ea588508dd3e3b8d6c50d809ae76b079abf9dd8d02e4b", + "zh:2190c9325980076489ce02b0f5dd2c0b91fc8711cefa99e714d8619a32827ad1", + "zh:2a0cfc5600730093705071707e4a4e4e953e7d9091859e0f66b46daa1060dd5d", + "zh:2ff53eac1af43ab9a2248a0e53c963d46e19cf04bc4c3f323591cfcebb218252", + "zh:4ebc3dee700f60af9da29970052fd02fa947813162b224716862dc9d7f1f7542", + "zh:5fe6dab84ceeaa8eb3f1567c5f05578333370c472240ca5c5bfc25e92d4d5586", + "zh:66bbec16367bbf440045502c9779b11f4ac5b022c8d8d17afe12d431950838b5", + "zh:7641e5c2e4b529e869cde29ab5b1de2fd1091489eb745b19ac2709bd7f4dfd84", + "zh:855bfba0756d17ce07595ff57d7cf664443d1495127cb88fb063362734b8b22a", + "zh:aaec10f237921d60c581d1b7a66f0a8a8019d9802dc04af11b5b981f6682e01d", + "zh:e460835a38ffa1e74f6929904bfd14ef473d217fd537b7ce834abe5ce5e2ce07", + "zh:ecc4295215db0e4aea3c9329611c31e09a853e1ae207d56742403bd4f5516703", + "zh:ee6d9fae63a612072e00402894e14826af7a3351c235b9c5b423b7629a77ca29", + "zh:f2b5c8db74aa7ebcf7cd423672358437d42401675069ef67b01ff910054e49d5", + "zh:f5aff74d3eb96d4592c7bca5cd3ea89b469e84efbf382944bd0f844a57059c09", + ] +} + +provider "registry.opentofu.org/telmate/proxmox" { + version = "3.0.2-rc04" + constraints = "3.0.2-rc04" + hashes = [ + "h1:+cwNDu0k5u7umqhQXFQsaWj8u10fAhYqd6GkbXIMUmE=", + "h1:/gu6nxurXTH52lyU9gLdKplbopmWVWISxG66jCxeu8k=", + "h1:8gE+vAHg3lG9eYID58yJmZBV0juPGfYciqJbXhH8cNg=", + "h1:8tyqYSoNfrqIh0ICT7abFOQVcyGGXry2qFCJVMoc4TE=", + "h1:Fd96pUhKZCMlN3o65grw2wZddYJ8pKT3AlFnzpkKffI=", + "h1:PYeQ8wBBH+L89THsEVgrRynySaoxNQjxiEvRHGug1KQ=", + "h1:SnzOxMwWxJbJl8EIjah6+h0OSLPAVIFFItwnzKkENX4=", + "h1:ZPr93UUVOw1aCqsODLXsqNQ9Lq/WafX5jgCCLxKBHl4=", + "h1:cJqOL86cwKzfgdfceHWQJT+EGD72EA7Ce01UwDFhYHI=", + "h1:fb+/e4VKjjeF86t8rHkwwOtkzlGFShFvsX+nxPRYjOo=", + "h1:o0bK2WRHzFFinCL7G83hD3/Gtli6bnI81s3IbelrHuM=", + "h1:t4E2D4zMKyLSdibGIWkOCEpVvHyFH8GLpGrG0mqFdwk=", + "h1:u/obsRXL8vD4p4JhrQa81o1zZLoH9LRgaOKUT5C0Ba8=", + "h1:xBZTbr96GHQVwVr2OA2cEBDaPNNua36HJucBUgD3qWg=", + "zh:00586a3e02061734f5ff4c309696ec1bbd6e00a7ed1281c44626f02929417f6e", + "zh:05af12aababd0a67a95309a803123f3c238b16ecdb73dc59c8d9c3d4a0b691c5", + "zh:4b6457bd0138f5c66c8b7043bdbffe3b8abcce2383293d77f653dbd77d14428f", + "zh:7b1b6e76d6b03c7829b8dadeb30de236782be314f5146d9f29ee12bd01961d7a", + "zh:87c21e0a8244b73c62ad3221773ec1c72fb918e0025b0664183a8ad8bebeb5bc", + "zh:954997c550285dfc167977683b97b045dd62923c251fad32c4ca81b564c2005f", + "zh:98781ef0199877580256e9f9a7f0666107f76af23f54f67d6392a66a6f5001b5", + "zh:9fc4dbd1752e2b9125223be2dd25597c0e6d534aa784c4829af3c3e3e58eb973", + "zh:a3f672019aee2d45c202668b95c48113e3c50001b740bd48956738ad1418c6b7", + "zh:a539852a4034132442e6dc6f644444994d83b0dc7a4d30bbe6464de420cfdf2d", + "zh:aade0b50a559a1da683c49ec8527e5e66b12ed42d4b1f10e69b25cbbf6c67805", + "zh:bdb7c2b78cfe039247948fa75fad49f871f8a8bb9b969aec4c45a24edc80ab0f", + "zh:c16209a6748f87a379a5a3132ca4f471a4807bd478664499593e09433ba39b7b", + "zh:cb015fb4a0a50d151c35c6a3505cdf7a337edd0b65eba82a43c8142c523adfb8", + ] +} diff --git a/ansible/inventories/generated.yml b/ansible/inventories/generated.yml new file mode 100644 index 0000000..b622390 --- /dev/null +++ b/ansible/inventories/generated.yml @@ -0,0 +1,22 @@ +# File generated by running OpenTofu, not meant to be manually edited +all: + children: + proxmox: + children: + proxmox_lxc: + hosts: + proxmox_vm: + hosts: + + lxc: + children: + proxmox_lxc: + + vm: + children: + proxmox_vm: + + + vars: + ansible_user: "ansible" + ansible_ssh_private_key_file: ".././.ssh_key" diff --git a/backend.tf b/backend.tf new file mode 100644 index 0000000..22bb428 --- /dev/null +++ b/backend.tf @@ -0,0 +1,23 @@ +terraform { + # Local backend: stores state in the parent repo so it stays out of this + # submodule. Switch to the remote backend below once you have an S3 bucket. + backend "local" { + path = "./terraform.tfstate" + } + + # Remote backend (S3 or S3-compatible, e.g. MinIO). Comment out the local + # backend above and fill in your values to use this instead. + # + # backend "s3" { + # bucket = "my-tofu-state" + # key = "site/terraform.tfstate" + # region = "us-east-1" + # + # # For MinIO or other S3-compatible stores: + # # endpoint = "https://minio.example.com" + # # force_path_style = true + # # skip_credentials_validation = true + # # skip_metadata_api_check = true + # # skip_region_validation = true + # } +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..9bd0677 --- /dev/null +++ b/main.tf @@ -0,0 +1,79 @@ +resource "local_sensitive_file" "ssh_private_key" { + filename = "${path.module}/.ssh_key" + content_base64 = data.external.infisical.result["ssh_private_key_b64"] + file_permission = "0600" +} + +locals { + # Shared inputs passed to every proxmox_node module call. + node_common = { + search_domain = var.search_domain + dns_nameservers = var.dns_nameservers + ssh_key = data.external.infisical.result["ssh_public_key"] + ansible_user = var.ansible_user + lxc_password = data.external.infisical.result["lxc_password"] + } +} + +# Provider aliases must be statically declared, so each hypervisor needs its +# own module block. To add or remove a hypervisor, add/remove the corresponding +# provider alias in providers.tf and a module block here. Everything else +# (node contents, common config) lives in the parent repo's terraform.tfvars. + +module "proxmox" { + source = "./opentofu/modules/proxmox_node" + + node_name = var.hypervisors["proxmox"].node_name + storage = var.hypervisors["proxmox"].storage + lxc = try(var.nodes["proxmox"].lxc, {}) + machines = try(var.nodes["proxmox"].machines, {}) + search_domain = local.node_common.search_domain + dns_nameservers = local.node_common.dns_nameservers + ssh_key = local.node_common.ssh_key + ansible_user = local.node_common.ansible_user + lxc_password = local.node_common.lxc_password + + providers = { + proxmox = proxmox.proxmox + } +} + +locals { + hv_hosts = { + proxmox = { lxc = module.proxmox.lxc_hosts, vm = module.proxmox.vm_hosts } + } + + all_hosts = flatten([ + for hv_name, hv in local.hv_hosts : [ + for h in concat(hv.lxc, hv.vm) : [ + for tag in h.filtered_tags : { + tag = tag + name = h.name + ip = h.ip + vmid = h.vmid + } + ] + ] + ]) + + tag_groups = { + for tag in distinct([for h in local.all_hosts : h.tag]) : + tag => [ + for h in local.all_hosts : { + name = h.name + ip = h.ip + vmid = h.vmid + } if h.tag == tag + ] + } +} + +resource "local_file" "ansible_inventory" { + filename = "${path.module}/${var.ansible_inventory_path}" + content = templatefile("${path.module}/opentofu/inventory.tpl", { + hypervisors = local.hv_hosts + tag_groups = local.tag_groups + ansible_user = var.ansible_user + ansible_ssh_private_key_file = local_sensitive_file.ssh_private_key.filename + }) +} diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..09b3523 --- /dev/null +++ b/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + proxmox = { + source = "telmate/proxmox" + version = "3.0.2-rc04" + } + local = { + source = "hashicorp/local" + version = "~> 2.2" + } + } +} + +data "external" "infisical" { + program = ["bash", "-c", "infisical export --template=./secrets.tmpl"] +} + +provider "proxmox" { + alias = "proxmox" + pm_api_url = var.hypervisors["proxmox"].api_url + pm_api_token_id = data.external.infisical.result["token_id"] + pm_api_token_secret = data.external.infisical.result["hv2_token_secret"] + pm_tls_insecure = true +} diff --git a/secrets.tmpl b/secrets.tmpl new file mode 100644 index 0000000..3533fd0 --- /dev/null +++ b/secrets.tmpl @@ -0,0 +1,13 @@ +{{$secrets := secret "02ba885c-7f22-4290-9b0e-845f65cf7d7c" "prod" "/terraform"}} +{ + {{- range $secret := $secrets }} + {{- if eq $secret.Key "HV_USER" }}"token_id": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "PROXMOX_TOKEN_SECRET" }}"hv2_token_secret": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "LXC_PASSWORD" }}"lxc_password": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "SSH_PRIVATE_KEY" }}"ssh_private_key_b64": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "SSH_PUBLIC_KEY" }}"ssh_public_key": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "IPAM_USERNAME" }}"ipam_username": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "IPAM_PASSWORD" }}"ipam_password": "{{$secret.Value}}",{{ end }} + {{- end }} + "_sentinel": "1" +} diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..bfa2a96 --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1,32 @@ +# ── Site configuration ───────────────────────────────────────────────────────── + +hypervisors = { + proxmox = { + api_url = "https://192.168.0.240:8006/api2/json" + node_name = "proxmox" + storage = "local-lvm" + } +} + +enable_ipam = false +ipam_endpoint = "https://ipam.example.net/api" + +network_subnets = { + lan = "192.168.0.0/24" +} + +ansible_user = "ansible" +search_domain = "lan.oswaldtheotter.net" +dns_nameservers = "1.1.1.1" + +# ── Node definitions ─────────────────────────────────────────────────────────── + +nodes = { + proxmox = { + lxc = { + } + machines = { + } + } + +} diff --git a/vars.tf b/vars.tf new file mode 100644 index 0000000..5c24a8f --- /dev/null +++ b/vars.tf @@ -0,0 +1,93 @@ +variable "hypervisors" { + description = "Per-hypervisor configuration. Keys must match provider aliases in providers.tf (e.g. hv2, hv3)." + type = map(object({ + api_url = string + node_name = string + storage = string + })) +} + +variable "enable_ipam" { + description = "When true, register all IPs in PHPIPAM. Requires ipam_endpoint and IPAM credentials in Infisical." + type = bool + default = false +} + +variable "ipam_endpoint" { + description = "Base URL of the PHPIPAM API, e.g. https://ipam.example.com/api" + type = string + default = "" +} + +variable "network_subnets" { + description = "Named map of CIDR ranges managed by PHPIPAM. Only used when enable_ipam = true." + type = map(string) + default = {} +} + +variable "ansible_user" { + description = "User created on each managed host for Ansible access." + type = string +} + +variable "search_domain" { + description = "DNS search domain set on all managed hosts." + type = string +} + +variable "dns_nameservers" { + description = "DNS nameserver(s) set on all managed hosts." + type = string +} + +variable "ansible_inventory_path" { + description = "Path where the generated Ansible inventory file is written. Defaults to ../ansible/inventories/generated.yml relative to this module, which points to the parent repo when used as a git submodule." + type = string + default = "./ansible/inventories/generated.yml" +} + +# LXC and VM definitions per hypervisor, keyed by the same names used in +# hypervisors and providers.tf. Both lxc and machines default to {} so a node +# can be declared with only one type of workload. +variable "nodes" { + type = map(object({ + lxc = optional(map(object({ + hostname = string + vmid = number + template = string + unprivileged = bool + onboot = bool + tags = string + memory = number + disk_size = string + nic_name = string + bridge = string + ip = string + gw = string + })), {}) + machines = optional(map(object({ + hostname = string + vmid = number + ip = string + ip2 = optional(string, null) + vlan = optional(number, 0) + template = string + full_clone = bool + onboot = bool + ciupgrade = optional(bool, false) + tags = string + agent = number + memory = number + disk_size = optional(string, "32") + balloon = number + cpu_type = string + cores = number + sockets = number + vcpus = number + bios = string + machine = string + bridge = string + })), {}) + })) + default = {} +}