Compare commits
5 commits
e03c21e853
...
9d43c19189
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d43c19189 | |||
| d4782d00cc | |||
| 4d8e641466 | |||
| 6f42e43efb | |||
| 102fad268c |
8 changed files with 322 additions and 14 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,3 +1,14 @@
|
|||
# OpenTofu / Terraform
|
||||
.terraform/
|
||||
.tofu/
|
||||
.terraform.lock.hcl
|
||||
.tofu.lock.hcl
|
||||
terraform.tfstate
|
||||
terraform.tfstate.*
|
||||
crash.log
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
|
||||
inventory.ini
|
||||
venv/*
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
proxmox_sshkey_path: "/home/{{ new_user }}/.ssh/authorized_keys" # Path to pubkey file for cloud-init injection
|
||||
proxmox_ci_upgrade: true # If true, run package upgrade on first boot
|
||||
|
||||
# Auto-install qemu-guest-agent in clones via cloud-init snippet
|
||||
qemu_agent_snippet_filename: "user-data-qemu-agent.yaml"
|
||||
|
||||
# Storage to import disk into; use existing storage like local-lvm or your ZFS pool name
|
||||
proxmox_image_storage: "{{ zfs_pool_name }}"
|
||||
|
||||
|
|
@ -36,8 +39,6 @@
|
|||
changed_when: false
|
||||
failed_when: pve_version_check.rc != 0
|
||||
|
||||
|
||||
|
||||
- name: Ensure destination directory exists for cloud image
|
||||
file:
|
||||
path: "{{ debian_cloud_image_dest_dir }}"
|
||||
|
|
@ -57,6 +58,42 @@
|
|||
force: false
|
||||
when: not debian_image_stat.stat.exists
|
||||
|
||||
- name: Ensure local storage allows snippets content (used for cloud-init snippets)
|
||||
command: >
|
||||
pvesm set local --content images,iso,vztmpl,snippets
|
||||
failed_when: false
|
||||
|
||||
- name: Ensure snippets directory exists on storage mountpoint
|
||||
file:
|
||||
path: "{{ zfs_pool_mountpoint }}/snippets"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Read SSH public key content
|
||||
slurp:
|
||||
src: "{{ proxmox_sshkey_path }}"
|
||||
register: ssh_key_content
|
||||
|
||||
- name: Extract SSH keys from authorized_keys file
|
||||
set_fact:
|
||||
ssh_keys_list: "{{ ssh_key_content.content | b64decode | split('\n') | select('match', '^ssh-') | list }}"
|
||||
|
||||
- name: Write cloud-init vendor-data snippet to install qemu-guest-agent
|
||||
copy:
|
||||
dest: "{{ zfs_pool_mountpoint }}/snippets/{{ qemu_agent_snippet_filename }}"
|
||||
mode: '0644'
|
||||
content: |
|
||||
#cloud-config
|
||||
# Vendor-data snippet: Proxmox will automatically set hostname from VM name when using vendor-data
|
||||
# User info (ciuser/sshkeys) is set separately via Terraform/Proxmox parameters
|
||||
package_update: true
|
||||
package_upgrade: true
|
||||
packages:
|
||||
- qemu-guest-agent
|
||||
runcmd:
|
||||
- systemctl enable qemu-guest-agent
|
||||
- systemctl start qemu-guest-agent
|
||||
|
||||
- name: Check if VMID already exists
|
||||
command: qm config {{ proxmox_template_vmid }}
|
||||
register: vmid_config_check
|
||||
|
|
@ -91,6 +128,22 @@
|
|||
when:
|
||||
- vmid_config_check.rc != 0 or not vm_already_template
|
||||
|
||||
- name: Check if ide2 (cloudinit) drive exists
|
||||
command: qm config {{ proxmox_template_vmid }}
|
||||
register: vm_config_check
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
when:
|
||||
- vmid_config_check.rc == 0
|
||||
|
||||
- name: Remove existing ide2 (cloudinit) drive if it exists for idempotency
|
||||
command: >
|
||||
qm set {{ proxmox_template_vmid }} --delete ide2
|
||||
register: ide2_removed
|
||||
when:
|
||||
- vmid_config_check.rc == 0
|
||||
- "'ide2:' in vm_config_check.stdout"
|
||||
|
||||
- name: Build consolidated qm set argument list (simplified)
|
||||
set_fact:
|
||||
qm_set_args: >-
|
||||
|
|
@ -110,15 +163,16 @@
|
|||
]
|
||||
+ (proxmox_ci_upgrade | bool
|
||||
| ternary(['--ciupgrade 1'], []))
|
||||
+ ['--cicustom vendor=local:snippets/' ~ qemu_agent_snippet_filename]
|
||||
}}
|
||||
when:
|
||||
- vmid_config_check.rc != 0 or not vm_already_template
|
||||
- vmid_config_check.rc != 0 or not vm_already_template | default(false) or ide2_removed.changed | default(false)
|
||||
|
||||
- name: Apply consolidated qm set
|
||||
command: >
|
||||
qm set {{ proxmox_template_vmid }} {{ qm_set_args | join(' ') }}
|
||||
when:
|
||||
- vmid_config_check.rc != 0 or not vm_already_template
|
||||
- vmid_config_check.rc != 0 or not vm_already_template | default(false) or ide2_removed.changed | default(false)
|
||||
|
||||
- name: Resize primary disk to requested size
|
||||
command: >
|
||||
|
|
@ -130,13 +184,3 @@
|
|||
command: qm template {{ proxmox_template_vmid }}
|
||||
when:
|
||||
- vmid_config_check.rc == 0 and not vm_already_template or vmid_config_check.rc != 0
|
||||
|
||||
- name: Show resulting template configuration
|
||||
command: qm config {{ proxmox_template_vmid }}
|
||||
register: final_template_config
|
||||
changed_when: false
|
||||
|
||||
- name: Debug final template config
|
||||
debug:
|
||||
msg: "{{ final_template_config.stdout }}"
|
||||
|
||||
|
|
|
|||
66
tofu/nodito/README.md
Normal file
66
tofu/nodito/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
## Nodito VMs with OpenTofu (Proxmox)
|
||||
|
||||
This directory lets you declare VMs on the `nodito` Proxmox node and apply with OpenTofu. It clones the Ansible-built template `debian-13-cloud-init` and places disks on the ZFS pool `proxmox-tank-1`.
|
||||
|
||||
### Prereqs
|
||||
- Proxmox API token with VM privileges. Example: user `root@pam`, token name `tofu`.
|
||||
- OpenTofu installed.
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg
|
||||
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://get.opentofu.org/opentofu.gpg | sudo tee /etc/apt/keyrings/opentofu.gpg >/dev/null
|
||||
curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | sudo gpg --no-tty --batch --dearmor -o /etc/apt/keyrings/opentofu-repo.gpg >/dev/null
|
||||
sudo chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg
|
||||
|
||||
echo \
|
||||
"deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main
|
||||
deb-src [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main" | \
|
||||
sudo tee /etc/apt/sources.list.d/opentofu.list > /dev/null
|
||||
sudo chmod a+r /etc/apt/sources.list.d/opentofu.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y tofu
|
||||
tofu version
|
||||
```
|
||||
- The Ansible template exists: `debian-13-cloud-init` (VMID 9001 by default).
|
||||
|
||||
### Provider Auth
|
||||
Create a `terraform.tfvars` (copy from `terraform.tfvars.example`) and set:
|
||||
- `proxmox_api_url` (e.g. `https://nodito:8006/api2/json`)
|
||||
- `proxmox_api_token_id` (e.g. `root@pam!tofu`)
|
||||
- `proxmox_api_token_secret`
|
||||
- `ssh_authorized_keys` (your public key content)
|
||||
|
||||
Alternatively, you can export env vars and reference them in a tfvars file.
|
||||
|
||||
### Declare VMs
|
||||
Edit `terraform.tfvars` and fill the `vms` map. Example entry:
|
||||
```
|
||||
vms = {
|
||||
web1 = {
|
||||
name = "web1"
|
||||
cores = 2
|
||||
memory_mb = 2048
|
||||
disk_size_gb = 20
|
||||
ipconfig0 = "ip=dhcp" # or "ip=192.168.1.50/24,gw=192.168.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All VM disks are created on `zfs_storage_name` (defaults to `proxmox-tank-1`). Network attaches to `vmbr0`. VLAN can be set per-VM with `vlan_tag`.
|
||||
|
||||
### Usage
|
||||
```
|
||||
tofu init
|
||||
tofu plan -var-file=terraform.tfvars
|
||||
tofu apply -var-file=terraform.tfvars
|
||||
```
|
||||
|
||||
### Notes
|
||||
- Clones are full clones by default (`full_clone = true`).
|
||||
- Cloud-init injects `cloud_init_user` and `ssh_authorized_keys`.
|
||||
- Disks use `scsi0` on ZFS with `discard` enabled.
|
||||
|
||||
|
||||
70
tofu/nodito/main.tf
Normal file
70
tofu/nodito/main.tf
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
locals {
|
||||
default_ipconfig0 = "ip=dhcp"
|
||||
}
|
||||
|
||||
resource "proxmox_vm_qemu" "vm" {
|
||||
for_each = var.vms
|
||||
|
||||
name = each.value.name
|
||||
target_node = var.proxmox_node
|
||||
vmid = try(each.value.vmid, null)
|
||||
|
||||
onboot = true
|
||||
agent = 1
|
||||
clone = var.template_name
|
||||
full_clone = true
|
||||
vga {
|
||||
type = "serial0"
|
||||
}
|
||||
|
||||
cpu {
|
||||
sockets = 1
|
||||
cores = each.value.cores
|
||||
type = "host"
|
||||
}
|
||||
memory = each.value.memory_mb
|
||||
|
||||
scsihw = "virtio-scsi-pci"
|
||||
boot = "c"
|
||||
bootdisk = "scsi0"
|
||||
|
||||
serial {
|
||||
id = 0
|
||||
type = "socket"
|
||||
}
|
||||
|
||||
# Network: bridge vmbr0, optional VLAN tag
|
||||
network {
|
||||
id = 0
|
||||
model = "virtio"
|
||||
bridge = "vmbr0"
|
||||
tag = try(each.value.vlan_tag, 0)
|
||||
}
|
||||
|
||||
# Cloud-init: user, ssh keys, IP, and custom snippet for qemu-guest-agent
|
||||
# Note: Using vendor-data snippet (instead of user-data) allows Proxmox to automatically
|
||||
# set the hostname from the VM name. User info is set separately via ciuser/sshkeys.
|
||||
# Using 'local' storage for snippets (not ZFS) as ZFS storage doesn't properly support snippet paths
|
||||
ciuser = var.cloud_init_user
|
||||
sshkeys = var.ssh_authorized_keys
|
||||
ipconfig0 = try(each.value.ipconfig0, local.default_ipconfig0)
|
||||
cicustom = "vendor=local:snippets/user-data-qemu-agent.yaml"
|
||||
|
||||
# Disk on ZFS storage
|
||||
disk {
|
||||
slot = "scsi0"
|
||||
type = "disk"
|
||||
storage = var.zfs_storage_name
|
||||
size = "${each.value.disk_size_gb}G"
|
||||
# optional flags like iothread/ssd/discard differ by provider versions; keep minimal
|
||||
}
|
||||
|
||||
# Cloud-init CD-ROM so ipconfig0/sshkeys apply
|
||||
disk {
|
||||
slot = "ide2"
|
||||
type = "cloudinit"
|
||||
storage = var.zfs_storage_name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
tofu/nodito/provider.tf
Normal file
8
tofu/nodito/provider.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
provider "proxmox" {
|
||||
pm_api_url = var.proxmox_api_url
|
||||
pm_api_token_id = var.proxmox_api_token_id
|
||||
pm_api_token_secret = var.proxmox_api_token_secret
|
||||
pm_tls_insecure = true
|
||||
}
|
||||
|
||||
|
||||
35
tofu/nodito/terraform.tfvars.example
Normal file
35
tofu/nodito/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
proxmox_api_url = "https://nodito:8006/api2/json"
|
||||
proxmox_api_token_id = "root@pam!tofu"
|
||||
proxmox_api_token_secret = "REPLACE_ME"
|
||||
|
||||
proxmox_node = "nodito"
|
||||
zfs_storage_name = "proxmox-tank-1"
|
||||
template_name = "debian-13-cloud-init"
|
||||
cloud_init_user = "counterweight"
|
||||
|
||||
# paste your ~/.ssh/id_ed25519.pub or similar
|
||||
ssh_authorized_keys = <<EOKEY
|
||||
ssh-ed25519 AAAA... your-key
|
||||
EOKEY
|
||||
|
||||
vms = {
|
||||
web1 = {
|
||||
name = "web1"
|
||||
vmid = 1101
|
||||
cores = 2
|
||||
memory_mb = 2048
|
||||
disk_size_gb = 20
|
||||
ipconfig0 = "ip=dhcp"
|
||||
}
|
||||
|
||||
db1 = {
|
||||
name = "db1"
|
||||
vmid = 1102
|
||||
cores = 4
|
||||
memory_mb = 4096
|
||||
disk_size_gb = 40
|
||||
ipconfig0 = "ip=dhcp"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
62
tofu/nodito/variables.tf
Normal file
62
tofu/nodito/variables.tf
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
variable "proxmox_api_url" {
|
||||
description = "Base URL for Proxmox API, e.g. https://nodito:8006/api2/json"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "proxmox_api_token_id" {
|
||||
description = "Proxmox API token ID, e.g. root@pam!tofu"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_api_token_secret" {
|
||||
description = "Proxmox API token secret"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_node" {
|
||||
description = "Target Proxmox node name"
|
||||
type = string
|
||||
default = "nodito"
|
||||
}
|
||||
|
||||
variable "zfs_storage_name" {
|
||||
description = "Proxmox storage name backed by ZFS (from Ansible: zfs_pool_name)"
|
||||
type = string
|
||||
default = "proxmox-tank-1"
|
||||
}
|
||||
|
||||
variable "template_name" {
|
||||
description = "Cloud-init template to clone (created by Ansible)"
|
||||
type = string
|
||||
default = "debian-13-cloud-init"
|
||||
}
|
||||
|
||||
variable "cloud_init_user" {
|
||||
description = "Default cloud-init user"
|
||||
type = string
|
||||
default = "counterweight"
|
||||
}
|
||||
|
||||
variable "ssh_authorized_keys" {
|
||||
description = "SSH public key content to inject via cloud-init"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "vms" {
|
||||
description = "Map of VMs to create"
|
||||
type = map(object({
|
||||
name = string
|
||||
vmid = optional(number)
|
||||
cores = number
|
||||
memory_mb = number
|
||||
disk_size_gb = number
|
||||
vlan_tag = optional(number)
|
||||
ipconfig0 = optional(string) # e.g. "ip=dhcp" or "ip=192.168.1.50/24,gw=192.168.1.1"
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
|
||||
12
tofu/nodito/versions.tf
Normal file
12
tofu/nodito/versions.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "Telmate/proxmox"
|
||||
version = "= 3.0.2-rc05"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue