From 289dd6c22a9e860b9c154b3650c52e3862695bbb Mon Sep 17 00:00:00 2001 From: xnoto Date: Sun, 21 Dec 2025 00:26:50 -0700 Subject: [PATCH] feat: initial libvirt domain module for provider v0.9.x --- .checkov.yml | 6 ++ .github/workflows/ci.yml | 43 +++++++++ .gitignore | 12 +++ .pre-commit-config.yaml | 36 ++++++++ .terraform-docs.yml | 16 ++++ .tflint.hcl | 10 +++ Makefile | 44 +++++++++ README.md | 95 ++++++++++++++++++++ main.tf | 189 +++++++++++++++++++++++++++++++++++++++ outputs.tf | 19 ++++ providers.tf | 13 +++ vars.tf | 124 +++++++++++++++++++++++++ 12 files changed, 607 insertions(+) create mode 100644 .checkov.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .terraform-docs.yml create mode 100644 .tflint.hcl create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 providers.tf create mode 100644 vars.tf diff --git a/.checkov.yml b/.checkov.yml new file mode 100644 index 0000000..34ce1d8 --- /dev/null +++ b/.checkov.yml @@ -0,0 +1,6 @@ +soft-fail: true +framework: + - terraform +skip-check: + - CKV_TF_1 # Ensure Terraform module sources use a commit hash + - CKV_TF_2 # Ensure Terraform module sources use a tag with a version number diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3619e00 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + +permissions: + contents: write + +jobs: + test: + name: Pre-commit Tests + runs-on: ubuntu-latest + container: + image: ghcr.io/makeitworkcloud/runner:latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Initialize OpenTofu + run: tofu init -backend=false + + - name: Run tests + run: make test + + release: + name: Create Release + runs-on: ubuntu-latest + needs: [test] + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..531e42d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# vim swap files +**/*.sw[po] + +# Terraform state and lock files +**/.terraform.lock.hcl +**/.terraform + +# IDE Folders +**/.vscode + +# Mac Finder cache +**/.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f3e633f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-vcs-permalinks + - id: destroyed-symlinks + - id: detect-private-key + - id: mixed-line-ending + - id: trailing-whitespace +- repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.104.0 + hooks: + - id: terraform_validate + args: + - --hook-config=--retry-once-with-cleanup=true + - --args=-no-color + - --tf-init-args=-reconfigure + - --tf-init-args=-upgrade + - id: terraform_tflint + args: + - --args=--minimum-failure-severity=error + - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl + - id: terraform_checkov + args: + - --args=--config-file __GIT_WORKING_DIR__/.checkov.yml + - id: terraform_fmt + args: + - --args=-no-color + - --args=-diff + - --args=-recursive + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..989a3fd --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,16 @@ +formatter: "markdown" + +output: + file: "README.md" + mode: inject + +settings: + color: false + lockfile: false + +sort: + enabled: true + by: name + +recursive: + enabled: false diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..e1e8412 --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,10 @@ +plugin "terraform" { + enabled = true + preset = "recommended" +} + +config { + call_module_type = "local" + force = false + disabled_by_default = false +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37137b8 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +SHELL := /bin/bash +TERRAFORM := $(shell which tofu) + +.PHONY: help init test pre-commit-check-deps pre-commit-install-hooks + +help: + @echo "Terraform Module targets" + @echo "------------------------" + @echo + @echo "\thelp: show this help text" + @echo "\tinit: run 'terraform init' (no backend)" + @echo "\ttest: run pre-commit checks" + @echo + @echo "One-time repo init targets" + @echo "--------------------------" + @echo + @echo "\tpre-commit-install-hooks: install pre-commit hooks" + @echo "\tpre-commit-check-deps: check pre-commit dependencies" + @echo + +init: + @${TERRAFORM} init -backend=false -upgrade + +test: .git/hooks/pre-commit + @pre-commit run -a + +DEPS_PRE_COMMIT=$(shell which pre-commit || echo "pre-commit not found") +DEPS_TERRAFORM_DOCS=$(shell which terraform-docs || echo "terraform-docs not found") +DEPS_TFLINT=$(shell which tflint || echo "tflint not found") +DEPS_CHECKOV=$(shell which checkov || echo "checkov not found") +DEPS_JQ=$(shell which jq || echo "jq not found") +pre-commit-check-deps: + @echo "Checking for pre-commit and its dependencies:" + @echo " pre-commit: ${DEPS_PRE_COMMIT}" + @echo " terraform-docs: ${DEPS_TERRAFORM_DOCS}" + @echo " tflint: ${DEPS_TFLINT}" + @echo " checkov: ${DEPS_CHECKOV}" + @echo " jq: ${DEPS_JQ}" + @echo "" + +pre-commit-install-hooks: .git/hooks/pre-commit + +.git/hooks/pre-commit: pre-commit-check-deps + @pre-commit install --install-hooks diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a12feb --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# terraform-libvirt-domain + +Terraform module for creating libvirt domains (VMs) with cloud-init and optional Ansible Automation Platform integration. + +## Usage + +```hcl +module "vm" { + source = "git::https://github.com/makeitworkcloud/terraform-libvirt-domain.git?ref=v1.0.0" + + name = "my-vm" + description = "My virtual machine" + vcpu = 2 + memory = 4096 + + cloudinit_meta_data_template = file("${path.module}/templates/meta-data.tpl") + cloudinit_meta_data_vars = { hostname = "my-vm" } + cloudinit_user_data_template = file("${path.module}/templates/user-data.tpl") + cloudinit_user_data_vars = {} + cloudinit_network_config_template = file("${path.module}/templates/network-config.tpl") + cloudinit_network_config_vars = { ip_address = "192.168.1.100" } + + private_ip_addr = "192.168.1.100" + proxyhost = "bastion.example.com" +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [libvirt](#requirement\_libvirt) | >= 0.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aap](#provider\_aap) | n/a | +| [libvirt](#provider\_libvirt) | >= 0.9.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| aap_host.host | resource | +| aap_job.job | resource | +| [libvirt_cloudinit_disk.commoninit](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/cloudinit_disk) | resource | +| [libvirt_domain.vm](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/domain) | resource | +| [libvirt_volume.boot](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/volume) | resource | +| [libvirt_volume.cloudinit](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/volume) | resource | +| [libvirt_volume.extra](https://registry.terraform.io/providers/dmacvicar/libvirt/latest/docs/resources/volume) | resource | +| aap_inventory.inventory | data source | +| aap_job_template.job_template | data source | +| aap_organization.org | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aap\_inventory\_name](#input\_aap\_inventory\_name) | Name of the AAP inventory to use. | `string` | `"libvirt-infra"` | no | +| [aap\_job\_template\_name](#input\_aap\_job\_template\_name) | Name of the AAP job template to run. If left empty, will default to configure\_ | `string` | `""` | no | +| [aap\_org\_name](#input\_aap\_org\_name) | Name of the Ansible Automation Platform (AAP) organization. | `string` | `"Default"` | no | +| [boot\_image\_url](#input\_boot\_image\_url) | URL for the base QCOW2 image used as the boot disk. | `string` | `"https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"` | no | +| [bridge\_name](#input\_bridge\_name) | Name of the network bridge for the second network interface. | `string` | `"nm-bridge"` | no | +| [cloudinit\_meta\_data\_template](#input\_cloudinit\_meta\_data\_template) | The template content for cloud-init meta-data configuration. | `string` | n/a | yes | +| [cloudinit\_meta\_data\_vars](#input\_cloudinit\_meta\_data\_vars) | Variable map for the cloud-init meta-data template. | `map(string)` | n/a | yes | +| [cloudinit\_network\_config\_template](#input\_cloudinit\_network\_config\_template) | The template content for cloud-init network configuration. | `string` | n/a | yes | +| [cloudinit\_network\_config\_vars](#input\_cloudinit\_network\_config\_vars) | Variable map for the cloud-init network configuration template. | `map(string)` | n/a | yes | +| [cloudinit\_user\_data\_template](#input\_cloudinit\_user\_data\_template) | The template content for cloud-init user-data configuration. | `string` | n/a | yes | +| [cloudinit\_user\_data\_vars](#input\_cloudinit\_user\_data\_vars) | Variable map for the cloud-init user-data template. Set to {} if not used. | `map(string)` | n/a | yes | +| [description](#input\_description) | Description for the libvirt domain (virtual machine). | `string` | `""` | no | +| [enable\_aap](#input\_enable\_aap) | Whether to provision Ansible Automation Platform (AAP) resources for this domain. | `bool` | `false` | no | +| [extra\_volumes](#input\_extra\_volumes) | List of additional volumes to attach to the domain. Each object should contain:
- name: Name of the volume.
- size: Size of the volume in bytes.
Example:
[
{
name = "runner-var-lib-docker.qcow2"
size = 107374182400
}
] |
list(object({
name = string
size = number
}))
| `[]` | no | +| [memory](#input\_memory) | Amount of memory (in MB) to assign to the domain. | `number` | `2048` | no | +| [name](#input\_name) | The name of the libvirt domain (virtual machine) and related resources. | `string` | n/a | yes | +| [private\_ip\_addr](#input\_private\_ip\_addr) | Private IP address to assign to the VM (used for network config and inventory). | `string` | n/a | yes | +| [proxyhost](#input\_proxyhost) | Proxy host for SSH connection, used in ansible\_ssh\_common\_args. | `string` | n/a | yes | +| [storage\_pool](#input\_storage\_pool) | Name of the libvirt storage pool where volumes will be created. | `string` | `"default"` | no | +| [vcpu](#input\_vcpu) | Number of virtual CPUs to assign to the domain. | `number` | `1` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [boot\_volume\_id](#output\_boot\_volume\_id) | The ID of the boot volume | +| [cloudinit\_disk\_id](#output\_cloudinit\_disk\_id) | The ID of the cloud-init disk | +| [domain\_id](#output\_domain\_id) | The ID of the libvirt domain | +| [domain\_name](#output\_domain\_name) | The name of the libvirt domain | + diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..e9675fb --- /dev/null +++ b/main.tf @@ -0,0 +1,189 @@ +resource "libvirt_volume" "boot" { + name = "${var.name}.qcow2" + pool = var.storage_pool + + target = { + format = { + type = "qcow2" + } + } + + create = { + content = { + url = var.boot_image_url + } + } +} + +resource "libvirt_volume" "extra" { + count = length(var.extra_volumes) + name = var.extra_volumes[count.index].name + pool = var.storage_pool + capacity = var.extra_volumes[count.index].size +} + +resource "libvirt_cloudinit_disk" "commoninit" { + name = "${var.name}_commoninit" + meta_data = templatefile(var.cloudinit_meta_data_template, var.cloudinit_meta_data_vars) + user_data = templatefile(var.cloudinit_user_data_template, var.cloudinit_user_data_vars) + network_config = templatefile(var.cloudinit_network_config_template, var.cloudinit_network_config_vars) +} + +resource "libvirt_volume" "cloudinit" { + name = "${var.name}_cloudinit.iso" + pool = var.storage_pool + + create = { + content = { + url = libvirt_cloudinit_disk.commoninit.path + } + } +} + +resource "libvirt_domain" "vm" { + name = var.name + type = "kvm" + description = var.description + vcpu = var.vcpu + memory = var.memory + memory_unit = "MiB" + running = true + + cpu = { + mode = "host-passthrough" + } + + os = { + type = "hvm" + type_arch = "x86_64" + boot_devices = [{ dev = "hd" }] + } + + devices = { + disks = concat( + [ + { + source = { + volume = { + pool = var.storage_pool + volume = libvirt_volume.boot.name + } + } + target = { + dev = "vda" + bus = "virtio" + } + }, + { + source = { + volume = { + pool = var.storage_pool + volume = libvirt_volume.cloudinit.name + } + } + target = { + dev = "vdb" + bus = "virtio" + } + device = "cdrom" + } + ], + [ + for idx, vol in libvirt_volume.extra : { + source = { + volume = { + pool = var.storage_pool + volume = vol.name + } + } + target = { + dev = "vd${substr("cdefghij", idx, 1)}" + bus = "virtio" + } + } + ] + ) + + interfaces = [ + { + model = { + type = "virtio" + } + source = { + network = { + network = "default" + } + } + }, + { + model = { + type = "virtio" + } + source = { + bridge = { + bridge = var.bridge_name + } + } + } + ] + + graphics = [ + { + vnc = { + auto_port = true + listen = "0.0.0.0" + listeners = [ + { + address = { + address = "0.0.0.0" + } + } + ] + } + } + ] + } + + lifecycle { + ignore_changes = [devices] + } +} + +data "aap_organization" "org" { + count = var.enable_aap ? 1 : 0 + name = var.aap_org_name + depends_on = [libvirt_domain.vm] +} + +data "aap_inventory" "inventory" { + count = var.enable_aap ? 1 : 0 + name = var.aap_inventory_name + organization_name = data.aap_organization.org[0].name + depends_on = [data.aap_organization.org] +} + +resource "aap_host" "host" { + count = var.enable_aap ? 1 : 0 + name = var.name + description = var.description + inventory_id = data.aap_inventory.inventory[0].id + enabled = true + variables = jsonencode({ + ansible_host = var.private_ip_addr + ansible_ssh_common_args = "-o ProxyCommand=\"ssh -o StrictHostKeyChecking=no -W %h:%p ${var.proxyhost}\"" + }) + depends_on = [data.aap_inventory.inventory] +} + +data "aap_job_template" "job_template" { + count = var.enable_aap ? 1 : 0 + name = var.aap_job_template_name != "" ? var.aap_job_template_name : "configure_${var.name}" + organization_name = data.aap_organization.org[0].name + depends_on = [data.aap_organization.org] +} + +resource "aap_job" "job" { + count = var.enable_aap ? 1 : 0 + job_template_id = data.aap_job_template.job_template[0].id + depends_on = [aap_host.host, data.aap_job_template.job_template] +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..a3ef12e --- /dev/null +++ b/outputs.tf @@ -0,0 +1,19 @@ +output "domain_id" { + description = "The ID of the libvirt domain" + value = libvirt_domain.vm.id +} + +output "domain_name" { + description = "The name of the libvirt domain" + value = libvirt_domain.vm.name +} + +output "boot_volume_id" { + description = "The ID of the boot volume" + value = libvirt_volume.boot.id +} + +output "cloudinit_disk_id" { + description = "The ID of the cloud-init disk" + value = libvirt_cloudinit_disk.commoninit.id +} diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..64ae94e --- /dev/null +++ b/providers.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + libvirt = { + source = "dmacvicar/libvirt" + version = ">= 0.9.0" + } + aap = { + source = "registry.terraform.io/ansible/aap" + } + } +} diff --git a/vars.tf b/vars.tf new file mode 100644 index 0000000..44e6bd7 --- /dev/null +++ b/vars.tf @@ -0,0 +1,124 @@ +variable "name" { + description = "The name of the libvirt domain (virtual machine) and related resources." + type = string +} + +variable "description" { + description = "Description for the libvirt domain (virtual machine)." + type = string + default = "" +} + +variable "vcpu" { + description = "Number of virtual CPUs to assign to the domain." + type = number + default = 1 +} + +variable "memory" { + description = "Amount of memory (in MB) to assign to the domain." + type = number + default = 2048 +} + +variable "storage_pool" { + description = "Name of the libvirt storage pool where volumes will be created." + type = string + default = "default" +} + +variable "boot_image_url" { + description = "URL for the base QCOW2 image used as the boot disk." + type = string + default = "https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2" +} + +variable "extra_volumes" { + description = <