├── .dockerignore ├── .gitignore ├── .tflint.hcl ├── .devcontainer ├── setup.sh ├── login.sh ├── inputrc ├── devcontainer.json └── Dockerfile ├── renovate.json5 ├── .github └── workflows │ └── lint.yml ├── qemu-agent-guest-exec ├── libvirt-domain.xsl ├── .terraform.lock.hcl ├── README.md ├── main.tf └── renovate.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !requirements.txt 3 | !requirements.yml 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | .terraform/ 3 | *terraform.tfstate* 4 | tfplan 5 | *.log 6 | -------------------------------------------------------------------------------- /.tflint.hcl: -------------------------------------------------------------------------------- 1 | # NB the terraform plugin is built into tflint, so no need to declare it here. 2 | -------------------------------------------------------------------------------- /.devcontainer/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | pushd /home/vscode 5 | sudo chown vscode:vscode .ssh && sudo chmod 700 .ssh 6 | popd 7 | -------------------------------------------------------------------------------- /.devcontainer/login.sh: -------------------------------------------------------------------------------- 1 | export EDITOR=code 2 | export PAGER=less 3 | 4 | alias l='ls -lF --color' 5 | alias ll='l -a' 6 | alias h='history 25' 7 | alias j='jobs -l' 8 | -------------------------------------------------------------------------------- /.devcontainer/inputrc: -------------------------------------------------------------------------------- 1 | set input-meta on 2 | set output-meta on 3 | set show-all-if-ambiguous on 4 | set completion-ignore-case on 5 | 6 | "\e[A": history-search-backward 7 | "\e[B": history-search-forward 8 | "\eOD": backward-word 9 | "\eOC": forward-word 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Terraform", 3 | "dockerFile": "Dockerfile", 4 | "runArgs": [ 5 | "--group-add=120", // 120 is the id of the libvirt group. 6 | "-v=${localEnv:HOME}/.ssh/id_rsa:/home/vscode/.ssh/id_rsa:ro", 7 | "-v=${localEnv:HOME}/.ssh/id_rsa.pub:/home/vscode/.ssh/id_rsa.pub:ro", 8 | "-v=/var/run/libvirt/libvirt-sock:/var/run/libvirt/libvirt-sock" 9 | ], 10 | "postCreateCommand": "bash .devcontainer/setup.sh", 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "hashicorp.terraform", 15 | "streetsidesoftware.code-spell-checker" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | // see https://docs.renovatebot.com/templates/ 2 | // see https://docs.renovatebot.com/modules/manager/ 3 | // see https://docs.renovatebot.com/modules/manager/regex/ 4 | // see https://docs.renovatebot.com/configuration-options/ 5 | { 6 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 7 | "regexManagers": [ 8 | // default datasources. 9 | { 10 | "fileMatch": [ 11 | "\\.yml$", 12 | ".sh$", 13 | ], 14 | "matchStrings": [ 15 | "# renovate: datasource=(?[^:]+?) depName=(?.+?)( versioning=(?.+?))?( extractVersion=(?.+?))?( registryUrl=(?.+?))?\\s.+?[:=]\\s*[\"']?(?.+?)[\"']?\\s" 16 | ], 17 | "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver-coerced{{/if}}", 18 | "extractVersionTemplate": "{{#if extractVersion}}{{{extractVersion}}}{{else}}^v?(?.+)${{/if}}" 19 | }, 20 | ] 21 | } -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push] 3 | jobs: 4 | lint: 5 | name: Lint 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - uses: actions/checkout@v6 9 | - name: Cache the plugins directory 10 | uses: actions/cache@v4 11 | with: 12 | path: ~/.tflint.d/plugins 13 | key: tflint-${{ hashFiles('.tflint.hcl') }} 14 | - uses: terraform-linters/setup-tflint@v6 15 | name: Setup 16 | with: 17 | # see https://github.com/terraform-linters/tflint/releases 18 | # renovate: datasource=github-releases depName=terraform-linters/tflint 19 | tflint_version: v0.60.0 20 | - name: Init 21 | run: tflint --init 22 | env: 23 | # https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/plugins.md#avoiding-rate-limiting 24 | GITHUB_TOKEN: ${{ github.token }} 25 | - name: Lint 26 | run: tflint --format compact 27 | fmt: 28 | name: terraform fmt check 29 | runs-on: ubuntu-22.04 30 | steps: 31 | - uses: actions/checkout@v6 32 | - name: terraform fmt check 33 | run: terraform fmt -check -diff 34 | -------------------------------------------------------------------------------- /qemu-agent-guest-exec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | Execute a command inside the guest. 4 | ''' 5 | 6 | import base64 7 | import json 8 | import libvirt 9 | import libvirt_qemu 10 | import logging 11 | import sys 12 | import time 13 | 14 | domain_name = sys.argv[1] 15 | command_path = sys.argv[2] 16 | command_args = sys.argv[3:] 17 | 18 | try: 19 | connection = libvirt.open(None) 20 | except libvirt.libvirtError: 21 | logging.exception('Failed to open connection to the hypervisor') 22 | exit(1) 23 | 24 | try: 25 | domain = connection.lookupByName(domain_name) 26 | except libvirt.libvirtError: 27 | logging.exception(f'Domain {domain_name} is not running') 28 | exit(1) 29 | 30 | # execute a command inside the guest. 31 | # see https://libvirt.org/html/libvirt-libvirt-qemu.html#virDomainQemuAgentCommand 32 | command = json.dumps({ 33 | 'execute': 'guest-exec', 34 | 'arguments': { 35 | 'path': command_path, 36 | 'arg': command_args, 37 | 'capture-output': True, 38 | } 39 | }) 40 | result = json.loads(libvirt_qemu.qemuAgentCommand(domain, command, -2, 0)) 41 | command = json.dumps({ 42 | 'execute': 'guest-exec-status', 43 | 'arguments': { 44 | 'pid': result['return']['pid'], 45 | } 46 | }) 47 | while True: 48 | result = json.loads(libvirt_qemu.qemuAgentCommand(domain, command, -2, 0)) 49 | if result['return']['exited']: 50 | print(base64.b64decode(result['return']['out-data']).decode('utf-8')) 51 | break 52 | time.sleep(0.1) 53 | -------------------------------------------------------------------------------- /libvirt-domain.xsl: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker.io/docker/dockerfile:1.20 2 | 3 | # see https://github.com/hashicorp/terraform/releases 4 | # renovate: datasource=github-releases depName=hashicorp/terraform 5 | ARG TERRAFORM_VERSION='1.14.0' 6 | 7 | # see https://github.com/devcontainers/images/tree/main/src/base-debian/history 8 | FROM mcr.microsoft.com/devcontainers/base:2.1.2-trixie 9 | 10 | RUN <<'EOF' 11 | #!/usr/bin/bash 12 | set -euxo pipefail 13 | export DEBIAN_FRONTEND=noninteractive 14 | apt-get update 15 | apt-get -y install --no-install-recommends \ 16 | bash-completion \ 17 | curl \ 18 | git \ 19 | libvirt-clients \ 20 | mkisofs \ 21 | openssh-client \ 22 | python3-argcomplete \ 23 | python3-libvirt \ 24 | sudo \ 25 | unzip \ 26 | wget \ 27 | xorriso \ 28 | xsltproc 29 | apt-get clean 30 | rm -rf /var/lib/apt/lists/* 31 | activate-global-python-argcomplete 32 | EOF 33 | 34 | ARG TERRAFORM_VERSION 35 | ENV CHECKPOINT_DISABLE=1 36 | RUN <<'EOF' 37 | #!/usr/bin/bash 38 | set -euxo pipefail 39 | terraform_url="https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" 40 | t="$(mktemp -q -d --suffix=.terraform)" 41 | wget -qO "$t/terraform.zip" "$terraform_url" 42 | unzip "$t/terraform.zip" -d "$t" 43 | install "$t/terraform" /usr/local/bin 44 | rm -rf "$t" 45 | terraform -install-autocomplete 46 | EOF 47 | 48 | RUN <<'EOF' 49 | #!/usr/bin/bash 50 | set -euxo pipefail 51 | # ensure /etc/profile is called at the top of the file, when running in a 52 | # login shell. 53 | sed -i '0,/esac/s/esac/&\n\nsource \/etc\/profile/' /home/vscode/.bashrc 54 | EOF 55 | COPY inputrc /etc/inputrc 56 | COPY login.sh /etc/profile.d/login.sh 57 | 58 | ENV LIBVIRT_DEFAULT_URI='qemu:///system' 59 | -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/dmacvicar/libvirt" { 5 | version = "0.8.3" 6 | constraints = "0.8.3" 7 | hashes = [ 8 | "h1:Tttxr3E9O75MM+dDmq5sYHQEw29PwtIj+XDj/5drdfE=", 9 | "zh:06ff0169beafd1891dc5a30616983abd32004a4f570d1d3dbb5851d84bd1c007", 10 | "zh:2dbdd726d0987cda73b56ecdfbcb98a67485e86a7a44aec976c0081b7239d89d", 11 | "zh:2e195a7bbdfcc13c45460571a5ba848a5c1e746b477c8381058767560f0ac93b", 12 | "zh:3952da13080018c5aec498b73e343c4c22ad884afb8c983138fb7255617aa991", 13 | "zh:478841bcf57df938726ddb90f55c7953fad09db4f6348747519afe7fc84b403b", 14 | "zh:53bce78b03a82c4782acfe1f32c2b46a68fa5fb2fb90d4a5392c90b436b44244", 15 | "zh:5c157f23e9768c67cddf9e847a571adca441607cb5adfb96dbfdd626ceadf92c", 16 | "zh:6bc78d631959fb695664966851308e140c38f3f5cf648dd89756320c2d91765d", 17 | "zh:8605d7d6915190836802654920a8eea3d751ae437273c4f4476dc0ebb9167a1d", 18 | "zh:8b66a22b97331c2a56aed092fd39152d06ad957fd4810aa3f0c4ade0f9b15755", 19 | "zh:92586a47a04082f70bb33f722672127a287caeed109beaaca2668e2e1d6a9caf", 20 | "zh:99a9ee414f5c4268e287660ce8edec2efcba1f79351f83791b64c7e5ab04f569", 21 | "zh:b7cff09fe74b0eb63b5b9aa94de5b33dadbd006d6d5b9578ac476039ea20b062", 22 | "zh:d4188a343ff32c0e03ff28c7e84abce0f43cad2fdbcd9046eaafc247429039ff", 23 | ] 24 | } 25 | 26 | provider "registry.terraform.io/hashicorp/random" { 27 | version = "3.7.2" 28 | constraints = "3.7.2" 29 | hashes = [ 30 | "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", 31 | "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", 32 | "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", 33 | "zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab", 34 | "zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3", 35 | "zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212", 36 | "zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f", 37 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 38 | "zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34", 39 | "zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967", 40 | "zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d", 41 | "zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62", 42 | "zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Usage (Ubuntu 22.04 host) 2 | 3 | [![Lint](https://github.com/rgl/terraform-libvirt-ubuntu-example/actions/workflows/lint.yml/badge.svg)](https://github.com/rgl/terraform-libvirt-ubuntu-example/actions/workflows/lint.yml) 4 | 5 | Create and install the [base Ubuntu 22.04 UEFI vagrant box](https://github.com/rgl/ubuntu-vagrant). 6 | 7 | Install the dependencies: 8 | 9 | * [Visual Studio Code](https://code.visualstudio.com). 10 | * [Dev Container plugin](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). 11 | 12 | Open this directory with the Dev Container plugin. 13 | 14 | Open `bash` inside the Visual Studio Code Terminal. 15 | 16 | Create the infrastructure: 17 | 18 | ```bash 19 | export CHECKPOINT_DISABLE=1 20 | export TF_LOG=TRACE 21 | export TF_LOG_PATH="$PWD/terraform.log" 22 | terraform init 23 | terraform plan -out=tfplan 24 | time terraform apply tfplan 25 | ``` 26 | 27 | **NB** if you have errors alike `Could not open '/var/lib/libvirt/images/terraform-ubuntu-example-root.img': Permission denied'` you need to reconfigure libvirt by setting `security_driver = "none"` in `/etc/libvirt/qemu.conf` and restart libvirt with `sudo systemctl restart libvirtd`. 28 | 29 | Show information about the libvirt/qemu guest: 30 | 31 | ```bash 32 | virsh dumpxml terraform-ubuntu-example 33 | virsh qemu-agent-command terraform-ubuntu-example '{"execute":"guest-info"}' --pretty 34 | virsh qemu-agent-command terraform-ubuntu-example '{"execute":"guest-network-get-interfaces"}' --pretty 35 | ./qemu-agent-guest-exec terraform-ubuntu-example id 36 | ./qemu-agent-guest-exec terraform-ubuntu-example uname -a 37 | ssh-keygen -f ~/.ssh/known_hosts -R "$(terraform output --raw ip)" 38 | ssh "vagrant@$(terraform output --raw ip)" 39 | ``` 40 | 41 | Destroy the infrastructure: 42 | 43 | ```bash 44 | time terraform destroy -auto-approve 45 | ``` 46 | 47 | List this repository dependencies (and which have newer versions): 48 | 49 | ```bash 50 | GITHUB_COM_TOKEN='YOUR_GITHUB_PERSONAL_TOKEN' ./renovate.sh 51 | ``` 52 | 53 | # Virtual BMC 54 | 55 | You can externally control the VM using the following terraform providers: 56 | 57 | * [vbmc terraform provider](https://registry.terraform.io/providers/rgl/vbmc) 58 | * exposes an [IPMI](https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface) endpoint. 59 | * you can use it with [ipmitool](https://github.com/ipmitool/ipmitool). 60 | * for more information see the [rgl/terraform-provider-vbmc](https://github.com/rgl/terraform-provider-vbmc) repository. 61 | * [sushy-vbmc terraform provider](https://registry.terraform.io/providers/rgl/sushy-vbmc) 62 | * exposes a [Redfish](https://en.wikipedia.org/wiki/Redfish_(specification)) endpoint. 63 | * you can use it with [redfishtool](https://github.com/DMTF/Redfishtool). 64 | * for more information see the [rgl/terraform-provider-sushy-vbmc](https://github.com/rgl/terraform-provider-sushy-vbmc) repository. 65 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | # see https://github.com/hashicorp/terraform 2 | terraform { 3 | required_version = "1.14.0" 4 | required_providers { 5 | # see https://registry.terraform.io/providers/hashicorp/random 6 | # see https://github.com/hashicorp/terraform-provider-random 7 | random = { 8 | source = "hashicorp/random" 9 | version = "3.7.2" 10 | } 11 | # see https://registry.terraform.io/providers/dmacvicar/libvirt 12 | # see https://github.com/dmacvicar/terraform-provider-libvirt 13 | libvirt = { 14 | source = "dmacvicar/libvirt" 15 | version = "0.8.3" 16 | } 17 | } 18 | } 19 | 20 | provider "libvirt" { 21 | uri = "qemu:///system" 22 | } 23 | 24 | variable "prefix" { 25 | type = string 26 | default = "terraform-ubuntu-example" 27 | } 28 | 29 | # NB this uses the vagrant ubuntu image imported from https://github.com/rgl/ubuntu-vagrant. 30 | variable "base_volume_name" { 31 | type = string 32 | default = "ubuntu-22.04-uefi-amd64_vagrant_box_image_0.0.0_box_0.img" 33 | } 34 | 35 | # see https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/website/docs/r/network.markdown 36 | resource "libvirt_network" "example" { 37 | name = var.prefix 38 | mode = "nat" 39 | domain = "example.test" 40 | addresses = ["10.17.3.0/24"] 41 | dhcp { 42 | enabled = false 43 | } 44 | dns { 45 | enabled = true 46 | local_only = false 47 | } 48 | lifecycle { 49 | ignore_changes = [ 50 | dhcp[0].enabled, # see https://github.com/dmacvicar/terraform-provider-libvirt/issues/998 51 | ] 52 | } 53 | } 54 | 55 | # create a cloud-init cloud-config. 56 | # NB this creates an iso image that will be used by the NoCloud cloud-init datasource. 57 | # see https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/website/docs/r/cloudinit.html.markdown 58 | # see journalctl -u cloud-init 59 | # see /run/cloud-init/*.log 60 | # see https://cloudinit.readthedocs.io/en/latest/topics/examples.html#disk-setup 61 | # see https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html#datasource-nocloud 62 | # see createISO at https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/libvirt/cloudinit_def.go#L139-L168 63 | resource "libvirt_cloudinit_disk" "example_cloudinit" { 64 | name = "${var.prefix}_example_cloudinit.iso" 65 | user_data = <<-EOF 66 | #cloud-config 67 | fqdn: example.test 68 | manage_etc_hosts: true 69 | users: 70 | - name: vagrant 71 | passwd: '$6$rounds=4096$NQ.EmIrGxn$rTvGsI3WIsix9TjWaDfKrt9tm3aa7SX7pzB.PSjbwtLbsplk1HsVzIrZbXwQNce6wmeJXhCq9YFJHDx9bXFHH.' 72 | lock_passwd: false 73 | ssh-authorized-keys: 74 | - ${jsonencode(trimspace(file("~/.ssh/id_rsa.pub")))} 75 | disk_setup: 76 | /dev/sdb: 77 | table_type: mbr 78 | layout: 79 | - [100, 83] 80 | overwrite: false 81 | fs_setup: 82 | - label: data 83 | device: /dev/sdb1 84 | filesystem: ext4 85 | overwrite: false 86 | mounts: 87 | - [/dev/sdb1, /data, ext4, 'defaults,discard,nofail', '0', '2'] 88 | runcmd: 89 | - sed -i '/vagrant insecure public key/d' /home/vagrant/.ssh/authorized_keys 90 | EOF 91 | } 92 | 93 | # this uses the vagrant ubuntu image imported from https://github.com/rgl/ubuntu-vagrant. 94 | # see https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/website/docs/r/volume.html.markdown 95 | resource "libvirt_volume" "example_root" { 96 | name = "${var.prefix}-root.img" 97 | base_volume_name = var.base_volume_name 98 | format = "qcow2" 99 | size = 66 * 1024 * 1024 * 1024 # 66GiB. the root FS is automatically resized by cloud-init growpart (see https://cloudinit.readthedocs.io/en/latest/topics/examples.html#grow-partitions). 100 | } 101 | 102 | # a data disk. 103 | # see https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/website/docs/r/volume.html.markdown 104 | resource "libvirt_volume" "example_data" { 105 | name = "${var.prefix}-data.img" 106 | format = "qcow2" 107 | size = 6 * 1024 * 1024 * 1024 # 6GiB. 108 | } 109 | 110 | # see https://github.com/dmacvicar/terraform-provider-libvirt/blob/v0.8.3/website/docs/r/domain.html.markdown 111 | resource "libvirt_domain" "example" { 112 | name = var.prefix 113 | machine = "q35" 114 | firmware = "/usr/share/OVMF/OVMF_CODE.fd" 115 | cpu { 116 | mode = "host-passthrough" 117 | } 118 | vcpu = 2 119 | memory = 1024 120 | qemu_agent = true 121 | cloudinit = libvirt_cloudinit_disk.example_cloudinit.id 122 | xml { 123 | xslt = file("libvirt-domain.xsl") 124 | } 125 | video { 126 | type = "qxl" 127 | } 128 | disk { 129 | volume_id = libvirt_volume.example_root.id 130 | scsi = true 131 | } 132 | disk { 133 | volume_id = libvirt_volume.example_data.id 134 | scsi = true 135 | } 136 | network_interface { 137 | network_id = libvirt_network.example.id 138 | wait_for_lease = true 139 | addresses = ["10.17.3.2"] 140 | } 141 | provisioner "remote-exec" { 142 | inline = [ 143 | <<-EOF 144 | set -x 145 | id 146 | uname -a 147 | cat /etc/os-release 148 | echo "machine-id is $(cat /etc/machine-id)" 149 | hostname --fqdn 150 | cat /etc/hosts 151 | sudo sfdisk -l 152 | lsblk -x KNAME -o KNAME,SIZE,TRAN,SUBSYSTEMS,FSTYPE,UUID,LABEL,MODEL,SERIAL 153 | mount | grep ^/dev 154 | df -h 155 | EOF 156 | ] 157 | connection { 158 | type = "ssh" 159 | user = "vagrant" 160 | host = self.network_interface[0].addresses[0] # see https://github.com/dmacvicar/terraform-provider-libvirt/issues/660 161 | private_key = file("~/.ssh/id_rsa") 162 | } 163 | } 164 | lifecycle { 165 | ignore_changes = [ 166 | nvram, 167 | disk[0].wwn, 168 | disk[1].wwn, 169 | ] 170 | } 171 | } 172 | 173 | output "ip" { 174 | value = length(libvirt_domain.example.network_interface[0].addresses) > 0 ? libvirt_domain.example.network_interface[0].addresses[0] : "" 175 | } 176 | -------------------------------------------------------------------------------- /renovate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # this executes renovate against the local repository. 5 | # NB this uses a temporary gitea instance because running renovate against a 6 | # local directory not (yet?) supported. 7 | # see https://github.com/renovatebot/renovate/issues/3609 8 | 9 | export RENOVATE_USERNAME='renovate' 10 | export RENOVATE_NAME='Renovate Bot' 11 | export RENOVATE_PASSWORD='password' 12 | gitea_container_name="$(basename "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")-renovate-gitea" 13 | 14 | # see https://hub.docker.com/r/gitea/gitea/tags 15 | # renovate: datasource=docker depName=gitea/gitea 16 | gitea_version='1.25.2' 17 | 18 | # see https://hub.docker.com/r/renovate/renovate/tags 19 | # see https://github.com/renovatebot/renovate/releases 20 | # renovate: datasource=docker depName=renovate/renovate 21 | renovate_version='42.19.3' 22 | 23 | # clean. 24 | echo 'Deleting existing Gitea...' 25 | docker rm --force "$gitea_container_name" >/dev/null 2>&1 26 | echo 'Deleting existing temporary files...' 27 | rm -f tmp/renovate-* 28 | install -d tmp 29 | 30 | # start gitea in background. 31 | # see https://docs.gitea.io/en-us/config-cheat-sheet/ 32 | # see https://github.com/go-gitea/gitea/releases 33 | # see https://github.com/go-gitea/gitea/blob/v1.25.2/docker/root/etc/s6/gitea/setup 34 | echo 'Starting Gitea...' 35 | docker run \ 36 | --detach \ 37 | --name "$gitea_container_name" \ 38 | -v /etc/timezone:/etc/timezone:ro \ 39 | -v /etc/localtime:/etc/localtime:ro \ 40 | -e SECRET_KEY=opensesame \ 41 | -p 3000 \ 42 | "gitea/gitea:$gitea_version" \ 43 | >/dev/null 44 | gitea_addr="$(docker port "$gitea_container_name" 3000 | head -1)" 45 | gitea_url="http://$gitea_addr" 46 | export RENOVATE_ENDPOINT="$gitea_url" 47 | export GIT_PUSH_REPOSITORY="http://$RENOVATE_USERNAME:$RENOVATE_PASSWORD@$gitea_addr/$RENOVATE_USERNAME/test.git" 48 | 49 | # wait for gitea to be ready. 50 | echo "Waiting for Gitea to be ready at $gitea_url..." 51 | GITEA_URL="$gitea_url" bash -euc 'while [ -z "$(wget -qO- "$GITEA_URL/api/v1/version" | jq -r ".version | select(.!=null)")" ]; do sleep 5; done' 52 | 53 | # create user in gitea. 54 | echo "Creating Gitea $RENOVATE_USERNAME user..." 55 | docker exec --user git "$gitea_container_name" gitea admin user create \ 56 | --admin \ 57 | --email "$RENOVATE_USERNAME@example.com" \ 58 | --username "$RENOVATE_USERNAME" \ 59 | --password "$RENOVATE_PASSWORD" 60 | curl \ 61 | --silent \ 62 | --show-error \ 63 | --fail-with-body \ 64 | -u "$RENOVATE_USERNAME:$RENOVATE_PASSWORD" \ 65 | -X 'PATCH' \ 66 | -H 'Accept: application/json' \ 67 | -H 'Content-Type: application/json' \ 68 | -d "{\"full_name\":\"$RENOVATE_NAME\"}" \ 69 | "$gitea_url/api/v1/user/settings" \ 70 | | jq \ 71 | > /dev/null 72 | 73 | # create the user personal access token. 74 | # see https://docs.gitea.io/en-us/api-usage/ 75 | # see https://docs.gitea.io/en-us/oauth2-provider/#scopes 76 | # see https://try.gitea.io/api/swagger#/user/userCreateToken 77 | echo "Creating Gitea $RENOVATE_USERNAME user personal access token..." 78 | curl \ 79 | --silent \ 80 | --show-error \ 81 | --fail-with-body \ 82 | -u "$RENOVATE_USERNAME:$RENOVATE_PASSWORD" \ 83 | -X POST \ 84 | -H "Content-Type: application/json" \ 85 | -d '{"name": "renovate", "scopes": ["read:user", "write:issue", "write:repository"]}' \ 86 | "$gitea_url/api/v1/users/$RENOVATE_USERNAME/tokens" \ 87 | | jq -r .sha1 \ 88 | >tmp/renovate-gitea-token.txt 89 | 90 | # try the token. 91 | echo "Trying the Gitea $RENOVATE_USERNAME user personal access token..." 92 | RENOVATE_TOKEN="$(cat tmp/renovate-gitea-token.txt)" 93 | export RENOVATE_TOKEN 94 | curl \ 95 | --silent \ 96 | --show-error \ 97 | --fail-with-body \ 98 | -H "Authorization: token $RENOVATE_TOKEN" \ 99 | -H 'Accept: application/json' \ 100 | "$gitea_url/api/v1/version" \ 101 | | jq \ 102 | > /dev/null 103 | 104 | # create remote repository in gitea. 105 | echo "Creating Gitea $RENOVATE_USERNAME test repository..." 106 | curl \ 107 | --silent \ 108 | --show-error \ 109 | --fail-with-body \ 110 | -u "$RENOVATE_USERNAME:$RENOVATE_PASSWORD" \ 111 | -X POST \ 112 | -H 'Accept: application/json' \ 113 | -H 'Content-Type: application/json' \ 114 | -d '{"name": "test"}' \ 115 | "$gitea_url/api/v1/user/repos" \ 116 | | jq \ 117 | > /dev/null 118 | 119 | # push the code to local gitea repository. 120 | # NB running renovate locally is not yet supported. 121 | # see https://github.com/renovatebot/renovate/issues/3609 122 | echo "Pushing local repository to Gitea $RENOVATE_USERNAME test repository..." 123 | git push --force "$GIT_PUSH_REPOSITORY" 124 | 125 | # see https://docs.renovatebot.com/modules/platform/gitea/ 126 | # see https://docs.renovatebot.com/self-hosted-configuration/#dryrun 127 | # see https://github.com/renovatebot/renovate/blob/main/docs/usage/examples/self-hosting.md 128 | # see https://github.com/renovatebot/renovate/tree/main/lib/modules/datasource 129 | # see https://github.com/renovatebot/renovate/tree/main/lib/modules/versioning 130 | RENOVATE_TOKEN="$(cat tmp/renovate-gitea-token.txt)" 131 | export RENOVATE_TOKEN 132 | # NB these can also be passed as raw positional arguments to docker run. 133 | export RENOVATE_REPOSITORIES="$RENOVATE_USERNAME/test" 134 | # see https://docs.github.com/en/rest/rate-limit#get-rate-limit-status-for-the-authenticated-user 135 | # see https://github.com/settings/tokens 136 | # NB this is only used for authentication. the token should not have any scope enabled. 137 | #export GITHUB_COM_TOKEN='TODO-YOUR-TOKEN' 138 | # let renovate create all the required pull requests. 139 | # see https://docs.renovatebot.com/configuration-options/#prhourlylimit 140 | # see https://docs.renovatebot.com/configuration-options/#prconcurrentlimit 141 | export RENOVATE_PR_HOURLY_LIMIT='0' 142 | export RENOVATE_PR_CONCURRENT_LIMIT='0' 143 | echo 'Running renovate...' 144 | # NB to capture the traffic using mitmproxy, start mitmweb in a different 145 | # shell, then enable the following if (i.e. true). 146 | docker_extra_args=() 147 | if false; then 148 | docker_extra_args+=( 149 | --env http_proxy=http://127.0.0.1:8080 150 | --env https_proxy=http://127.0.0.1:8080 151 | --env no_proxy= 152 | --env SSL_CERT_FILE=/usr/local/shared/ca-certificates/mitmproxy-ca.crt 153 | --volume "$HOME/.mitmproxy/mitmproxy-ca-cert.pem:/usr/local/shared/ca-certificates/mitmproxy-ca.crt:ro" 154 | ) 155 | fi 156 | # NB use --dry-run=lookup for not modifying the repository (e.g. for not 157 | # creating pull requests). 158 | docker run \ 159 | --rm \ 160 | --tty \ 161 | --interactive \ 162 | --net host \ 163 | --env GITHUB_COM_TOKEN \ 164 | --env RENOVATE_ENDPOINT \ 165 | --env RENOVATE_TOKEN \ 166 | --env RENOVATE_REPOSITORIES \ 167 | --env RENOVATE_PR_HOURLY_LIMIT \ 168 | --env RENOVATE_PR_CONCURRENT_LIMIT \ 169 | --env LOG_LEVEL=debug \ 170 | --env LOG_FORMAT=json \ 171 | "${docker_extra_args[@]}" \ 172 | "renovate/renovate:$renovate_version" \ 173 | --platform=gitea \ 174 | --git-url=endpoint \ 175 | >tmp/renovate-log.txt 176 | grep -E '^{' \ 177 | tmp/renovate-log.txt \ 178 | >tmp/renovate-log.json 179 | 180 | echo 'Getting results...' 181 | # extract the errors. 182 | jq 'select(.err)' tmp/renovate-log.json >tmp/renovate-errors.json 183 | # extract the result from the renovate log. 184 | jq 'select(.msg == "packageFiles with updates") | .config' tmp/renovate-log.json >tmp/renovate-result.json 185 | # extract all the dependencies. 186 | jq 'to_entries[].value[] | {packageFile,dep:.deps[]}' tmp/renovate-result.json >tmp/renovate-dependencies.json 187 | # extract the dependencies that have updates. 188 | jq 'select((.dep.updates | length) > 0)' tmp/renovate-dependencies.json >tmp/renovate-dependencies-updates.json 189 | 190 | # helpers. 191 | function show-title { 192 | echo 193 | echo '#' 194 | echo "# $1" 195 | echo '#' 196 | echo 197 | } 198 | 199 | # show errors. 200 | if [ "$(jq --slurp length tmp/renovate-errors.json)" -ne '0' ]; then 201 | show-title errors 202 | jq . tmp/renovate-errors.json 203 | fi 204 | 205 | # show dependencies. 206 | function show-dependencies { 207 | show-title "$1" 208 | ( 209 | printf 'packageFile\tdatasource\tdepName\tcurrentValue\tnewVersions\tskipReason\twarnings\n' 210 | jq \ 211 | -r \ 212 | '[ 213 | .packageFile, 214 | .dep.datasource, 215 | .dep.depName, 216 | .dep.currentValue, 217 | (.dep | select(.updates) | .updates | map(.newVersion) | join(" | ")), 218 | .dep.skipReason, 219 | (.dep | select(.warnings) | .warnings | map(.message) | join(" | ")) 220 | ] | @tsv' \ 221 | "$2" \ 222 | | sort 223 | ) | column -t -s "$(printf \\t)" 224 | } 225 | show-dependencies 'Dependencies' tmp/renovate-dependencies.json 226 | show-dependencies 'Dependencies Updates' tmp/renovate-dependencies-updates.json 227 | 228 | # show the gitea project. 229 | show-title "See PRs at $gitea_url/$RENOVATE_USERNAME/test/pulls (you can login as $RENOVATE_USERNAME:$RENOVATE_PASSWORD)" 230 | --------------------------------------------------------------------------------