diff --git a/.icons/proxmox.svg b/.icons/proxmox.svg new file mode 100755 index 000000000..c18256e23 --- /dev/null +++ b/.icons/proxmox.svg @@ -0,0 +1,137 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/umair/.images/avatar.jpeg b/registry/umair/.images/avatar.jpeg new file mode 100644 index 000000000..6495306b9 Binary files /dev/null and b/registry/umair/.images/avatar.jpeg differ diff --git a/registry/umair/README.md b/registry/umair/README.md new file mode 100644 index 000000000..c579b28bf --- /dev/null +++ b/registry/umair/README.md @@ -0,0 +1,13 @@ +--- +display_name: "Muhammad Uamir Ali" +bio: "Cloud Engineer | Infrastructure as code, Kubernetes | SRE" +github: "m4rrypro" +avatar: "./.images/avatar.jpeg" +linkedin: "https://www.linkedin.com/in/m4rry" +support_email: "m.umair.ali200@gmail.com" +status: "community" +--- + +# Muhammad Umair Ali + +Cloud Engineer | Infrastructure as code, Kubernetes | SRE diff --git a/registry/umair/templates/proxmox-vm/README.md b/registry/umair/templates/proxmox-vm/README.md new file mode 100644 index 000000000..0f2f44ea5 --- /dev/null +++ b/registry/umair/templates/proxmox-vm/README.md @@ -0,0 +1,161 @@ +--- +display_name: Proxmox VM +description: Provision VMs on Proxmox VE as Coder workspaces +icon: ../../../../.icons/proxmox.svg +verified: false +tags: [proxmox, vm, cloud-init, qemu] +--- + +# Proxmox VM Template for Coder + +Provision Linux VMs on Proxmox as [Coder workspaces](https://coder.com/docs/workspaces). The template clones a cloud‑init base image, injects user‑data via Snippets, and runs the Coder agent under the workspace owner's Linux user. + +## Prerequisites + +- Proxmox VE 8/9 +- Proxmox API token with access to nodes and storages +- SSH access from Coder provisioner to Proxmox VE +- Storage with "Snippets" content enabled +- Ubuntu cloud‑init image/template on Proxmox + - Latest images: https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/)) + +## Prepare a Proxmox Cloud‑Init Template (once) + +Run on the Proxmox node. This uses a RELEASE variable so you always pull a current image. + +```bash +# Choose a release (e.g., jammy or noble) +RELEASE=jammy +IMG_URL="https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.img" +IMG_PATH="/var/lib/vz/template/iso/${RELEASE}-server-cloudimg-amd64.img" + +# Download cloud image +wget "$IMG_URL" -O "$IMG_PATH" + +# Create base VM (example ID 999), enable QGA, correct boot order +NAME="ubuntu-${RELEASE}-cloudinit" +qm create 999 --name "$NAME" --memory 4096 --cores 2 \ + --net0 virtio,bridge=vmbr0 --agent enabled=1 +qm set 999 --scsihw virtio-scsi-pci +qm importdisk 999 "$IMG_PATH" local-lvm +qm set 999 --scsi0 local-lvm:vm-999-disk-0 +qm set 999 --ide2 local-lvm:cloudinit +qm set 999 --serial0 socket --vga serial0 +qm set 999 --boot 'order=scsi0;ide2;net0' + +# Enable Snippets on storage 'local' (one‑time) +pvesm set local --content snippets,vztmpl,backup,iso + +# Convert to template +qm template 999 +``` + +Verify: + +```bash +qm config 999 | grep -E 'template:|agent:|boot:|ide2:|scsi0:' +``` + +### Enable Snippets via GUI + +- Datacenter → Storage → select `local` → Edit → Content → check "Snippets" → OK +- Ensure `/var/lib/vz/snippets/` exists on the node for snippet files +- Template page → Cloud‑Init → Snippet Storage: `local` → File: your yml → Apply + +## Configure this template + +Edit `terraform.tfvars` with your environment: + +```hcl +# Proxmox API +proxmox_api_url = "https://:8006/api2/json" +proxmox_api_token_id = "!" +proxmox_api_token_secret = "" + +# SSH to the node (for snippet upload) +proxmox_host = "" +proxmox_password = "" +proxmox_ssh_user = "root" + +# Infra defaults +proxmox_node = "pve" +disk_storage = "local-lvm" +snippet_storage = "local" +bridge = "vmbr0" +vlan = 0 +clone_template_vmid = 999 +``` + +### Variables (terraform.tfvars) + +- These values are standard Terraform variables that the template reads at apply time. +- Place secrets (e.g., `proxmox_api_token_secret`, `proxmox_password`) in `terraform.tfvars` or inject with environment variables using `TF_VAR_*` (e.g., `TF_VAR_proxmox_api_token_secret`). +- You can also override with `-var`/`-var-file` if you run Terraform directly. With Coder, the repo's `terraform.tfvars` is bundled when pushing the template. + +Variables expected: + +- `proxmox_api_url`, `proxmox_api_token_id`, `proxmox_api_token_secret` (sensitive) +- `proxmox_host`, `proxmox_password` (sensitive), `proxmox_ssh_user` +- `proxmox_node`, `disk_storage`, `snippet_storage`, `bridge`, `vlan`, `clone_template_vmid` +- Coder parameters: `cpu_cores`, `memory_mb`, `disk_size_gb` + +## Proxmox API Token (GUI/CLI) + +Docs: https://pve.proxmox.com/wiki/User_Management#pveum_tokens + +GUI: + +1. (Optional) Create automation user: Datacenter → Permissions → Users → Add (e.g., `terraform@pve`) +2. Permissions: Datacenter → Permissions → Add → User Permission + - Path: `/` (or narrower covering your nodes/storages) + - Role: `PVEVMAdmin` + `PVEStorageAdmin` (or `PVEAdmin` for simplicity) +3. Token: Datacenter → Permissions → API Tokens → Add → copy Token ID and Secret +4. Test: + +```bash +curl -k -H "Authorization: PVEAPIToken=!=" \ + https:// < PVE_HOST > :8006/api2/json/version +``` + +CLI: + +```bash +pveum user add terraform@pve --comment 'Terraform automation user' +pveum aclmod / -user terraform@pve -role PVEAdmin +pveum user token add terraform@pve terraform --privsep 0 +``` + +## Use + +```bash +# From this directory +coder templates push --yes proxmox-cloudinit --directory . | cat +``` + +Create a workspace from the template in the Coder UI. First boot usually takes 60–120s while cloud‑init runs. + +## How it works + +- Uploads rendered cloud‑init user‑data to `:snippets/.yml` via the provider's `proxmox_virtual_environment_file` +- VM config: `virtio-scsi-pci`, boot order `scsi0, ide2, net0`, QGA enabled +- Linux user equals Coder workspace owner (sanitized). To avoid collisions, reserved names (`admin`, `root`, etc.) get a suffix (e.g., `admin1`). User is created with `primary_group: adm`, `groups: [sudo]`, `no_user_group: true` +- systemd service runs as that user: + - `coder-agent.service` + +## Troubleshooting quick hits + +- iPXE boot loop: ensure template has bootable root disk and boot order `scsi0,ide2,net0` +- QGA not responding: install/enable QGA in template; allow 60–120s on first boot +- Snippet upload errors: storage must include `Snippets`; token needs Datastore permissions; path format `:snippets/` handled by provider +- Permissions errors: ensure the token's role covers the target node(s) and storages +- Verify snippet/QGA: `qm config | egrep 'cicustom|ide2|ciuser'` + +## References + +- Ubuntu Cloud Images (latest): https://cloud-images.ubuntu.com/ ([source](https://cloud-images.ubuntu.com/)) +- Proxmox qm(1) manual: https://pve.proxmox.com/pve-docs/qm.1.html +- Proxmox Cloud‑Init Support: https://pve.proxmox.com/wiki/Cloud-Init_Support +- Terraform Proxmox provider (bpg): `bpg/proxmox` on the Terraform Registry +- Coder – Best practices & templates: + - https://coder.com/docs/tutorials/best-practices/speed-up-templates + - https://coder.com/docs/tutorials/template-from-scratch diff --git a/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl b/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl new file mode 100644 index 000000000..2cd34d0ac --- /dev/null +++ b/registry/umair/templates/proxmox-vm/cloud-init/user-data.tftpl @@ -0,0 +1,53 @@ +#cloud-config +hostname: ${hostname} + +users: + - name: ${linux_user} + groups: [sudo] + shell: /bin/bash + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + +package_update: false +package_upgrade: false +packages: + - curl + - ca-certificates + - git + - jq + +write_files: + - path: /opt/coder/init.sh + permissions: "0755" + owner: root:root + encoding: b64 + content: | + ${coder_init_script_b64} + + - path: /etc/systemd/system/coder-agent.service + permissions: "0644" + owner: root:root + content: | + [Unit] + Description=Coder Agent + Wants=network-online.target + After=network-online.target + + [Service] + Type=simple + User=${linux_user} + WorkingDirectory=/home/${linux_user} + Environment=HOME=/home/${linux_user} + Environment=CODER_AGENT_TOKEN=${coder_token} + ExecStart=/opt/coder/init.sh + OOMScoreAdjust=-1000 + Restart=always + RestartSec=5 + + [Install] + WantedBy=multi-user.target + +runcmd: + - systemctl daemon-reload + - systemctl enable --now coder-agent.service + +final_message: "Cloud-init complete on ${hostname}" \ No newline at end of file diff --git a/registry/umair/templates/proxmox-vm/main.tf b/registry/umair/templates/proxmox-vm/main.tf new file mode 100644 index 000000000..86da81b6c --- /dev/null +++ b/registry/umair/templates/proxmox-vm/main.tf @@ -0,0 +1,283 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + proxmox = { + source = "bpg/proxmox" + } + } +} + +provider "coder" {} + +provider "proxmox" { + endpoint = var.proxmox_api_url + api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}" + insecure = true + + # SSH is needed for file uploads to Proxmox + ssh { + username = var.proxmox_ssh_user + password = var.proxmox_password + + node { + name = var.proxmox_node + address = var.proxmox_host + } + } +} + +variable "proxmox_api_url" { + type = string +} + +variable "proxmox_api_token_id" { + type = string + sensitive = true +} + +variable "proxmox_api_token_secret" { + type = string + sensitive = true +} + + +variable "proxmox_host" { + description = "Proxmox node IP or DNS for SSH" + type = string +} + +variable "proxmox_password" { + description = "Proxmox password (used for SSH)" + type = string + sensitive = true +} + +variable "proxmox_ssh_user" { + description = "SSH username on Proxmox node" + type = string + default = "root" +} + +variable "proxmox_node" { + description = "Target Proxmox node" + type = string + default = "pve" +} +variable "disk_storage" { + description = "Disk storage (e.g., local-lvm)" + type = string + default = "local-lvm" +} + +variable "snippet_storage" { + description = "Storage with Snippets content" + type = string + default = "local" +} + +variable "bridge" { + description = "Bridge (e.g., vmbr0)" + type = string + default = "vmbr0" +} + +variable "vlan" { + description = "VLAN tag (0 none)" + type = number + default = 0 +} + +variable "clone_template_vmid" { + description = "VMID of the cloud-init base template to clone" + type = number +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "cpu_cores" { + name = "cpu_cores" + display_name = "CPU Cores" + type = "number" + default = 2 + mutable = true +} + +data "coder_parameter" "memory_mb" { + name = "memory_mb" + display_name = "Memory (MB)" + type = "number" + default = 4096 + mutable = true +} + +data "coder_parameter" "disk_size_gb" { + name = "disk_size_gb" + display_name = "Disk Size (GB)" + type = "number" + default = 20 + mutable = true + validation { + min = 10 + max = 100 + monotonic = "increasing" + } +} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + + env = { + GIT_AUTHOR_NAME = data.coder_workspace_owner.me.name + GIT_AUTHOR_EMAIL = data.coder_workspace_owner.me.email + } + + startup_script_behavior = "non-blocking" + startup_script = <<-EOT + set -e + # Add any startup scripts here + EOT + + metadata { + display_name = "CPU Usage" + key = "cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + order = 1 + } + + metadata { + display_name = "RAM Usage" + key = "ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + order = 2 + } + + metadata { + display_name = "Disk Usage" + key = "disk_usage" + script = "coder stat disk" + interval = 600 + timeout = 30 + order = 3 + } +} + +locals { + hostname = lower(data.coder_workspace.me.name) + vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${local.hostname}" + snippet_filename = "${local.vm_name}.yml" + base_user = replace(replace(replace(lower(data.coder_workspace_owner.me.name), " ", "-"), "/", "-"), "@", "-") # to avoid special characters in the username + linux_user = contains(["root", "admin", "daemon", "bin", "sys"], local.base_user) ? "${local.base_user}1" : local.base_user # to avoid conflict with system users + + rendered_user_data = templatefile("${path.module}/cloud-init/user-data.tftpl", { + coder_token = coder_agent.dev.token + coder_init_script_b64 = base64encode(coder_agent.dev.init_script) + hostname = local.vm_name + linux_user = local.linux_user + }) +} + +resource "proxmox_virtual_environment_file" "cloud_init_user_data" { + content_type = "snippets" + datastore_id = var.snippet_storage + node_name = var.proxmox_node + + source_raw { + data = local.rendered_user_data + file_name = local.snippet_filename + } +} + +resource "proxmox_virtual_environment_vm" "workspace" { + name = local.vm_name + node_name = var.proxmox_node + + clone { + node_name = var.proxmox_node + vm_id = var.clone_template_vmid + full = false + retries = 5 + } + + agent { + enabled = true + } + + on_boot = true + started = true + + startup { + order = 1 + } + + scsi_hardware = "virtio-scsi-pci" + boot_order = ["scsi0", "ide2"] + + memory { + dedicated = data.coder_parameter.memory_mb.value + } + + cpu { + cores = data.coder_parameter.cpu_cores.value + sockets = 1 + type = "host" + } + + network_device { + bridge = var.bridge + model = "virtio" + vlan_id = var.vlan == 0 ? null : var.vlan + } + + vga { + type = "serial0" + } + + serial_device { + device = "socket" + } + + disk { + interface = "scsi0" + datastore_id = var.disk_storage + size = data.coder_parameter.disk_size_gb.value + } + + initialization { + type = "nocloud" + datastore_id = var.disk_storage + + user_data_file_id = proxmox_virtual_environment_file.cloud_init_user_data.id + + ip_config { + ipv4 { + address = "dhcp" + } + } + } + + tags = ["coder", "workspace", local.vm_name] + + depends_on = [proxmox_virtual_environment_file.cloud_init_user_data] +} + +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.3.1" + agent_id = coder_agent.dev.id +} + +module "cursor" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cursor/coder" + version = "1.3.0" + agent_id = coder_agent.dev.id +} \ No newline at end of file