fix: use proxmox nocloud network disk
This commit is contained in:
parent
816999a01f
commit
69e39bd812
|
|
@ -20,23 +20,61 @@ resource "null_resource" "stage_talos_image" {
|
||||||
for_each = var.talos_image_update_mode == "download" ? { for node in var.nodes : node.name => node } : {}
|
for_each = var.talos_image_update_mode == "download" ? { for node in var.nodes : node.name => node } : {}
|
||||||
|
|
||||||
triggers = {
|
triggers = {
|
||||||
node_name = each.value.name
|
node_name = each.value.name
|
||||||
proxmox_node = lookup(each.value, "proxmox_node", var.proxmox_node)
|
proxmox_node = lookup(each.value, "proxmox_node", var.proxmox_node)
|
||||||
image_url = local.talos_iso_download_url
|
image_url = local.talos_iso_download_url
|
||||||
storage = var.talos_image_storage
|
storage = var.talos_image_storage
|
||||||
talos_version = var.talos_version
|
talos_version = var.talos_version
|
||||||
filename = local.node_iso_filenames[each.key]
|
filename = local.node_iso_filenames[each.key]
|
||||||
network_config = sha256(local_file.metal_network_config[each.key].content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
provisioner "local-exec" {
|
provisioner "local-exec" {
|
||||||
interpreter = ["python", "-c"]
|
interpreter = ["python", "-c"]
|
||||||
command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--api-token', '${var.proxmox_api_token}', '--node', '${self.triggers.proxmox_node}', '--node-address', '${lookup(var.node_addresses, self.triggers.proxmox_node, self.triggers.proxmox_node)}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}', '--network-config', r'${local_file.metal_network_config[each.key].filename}'], check=True)"
|
command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--api-token', '${var.proxmox_api_token}', '--node', '${self.triggers.proxmox_node}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}'], check=True)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_file" "nocloud_network_config" {
|
||||||
|
for_each = { for node in var.nodes : node.name => node }
|
||||||
|
|
||||||
|
node_name = lookup(each.value, "proxmox_node", var.proxmox_node)
|
||||||
|
datastore_id = var.talos_image_storage
|
||||||
|
content_type = "snippets"
|
||||||
|
|
||||||
|
source_raw {
|
||||||
|
data = local_file.metal_network_config[each.key].content
|
||||||
|
file_name = "${each.key}-network-config.yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
depends_on = [local_file.metal_network_config]
|
depends_on = [local_file.metal_network_config]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_file" "nocloud_metadata" {
|
||||||
|
for_each = { for node in var.nodes : node.name => node }
|
||||||
|
|
||||||
|
node_name = lookup(each.value, "proxmox_node", var.proxmox_node)
|
||||||
|
datastore_id = var.talos_image_storage
|
||||||
|
content_type = "snippets"
|
||||||
|
|
||||||
|
source_raw {
|
||||||
|
data = "instance-id: ${each.key}\nlocal-hostname: ${each.key}\n"
|
||||||
|
file_name = "${each.key}-meta-data.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_file" "nocloud_user_data" {
|
||||||
|
for_each = { for node in var.nodes : node.name => node }
|
||||||
|
|
||||||
|
node_name = lookup(each.value, "proxmox_node", var.proxmox_node)
|
||||||
|
datastore_id = var.talos_image_storage
|
||||||
|
content_type = "snippets"
|
||||||
|
|
||||||
|
source_raw {
|
||||||
|
data = "#cloud-config\n"
|
||||||
|
file_name = "${each.key}-user-data.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Create all required network bridges
|
# Create all required network bridges
|
||||||
resource "proxmox_virtual_environment_network_linux_bridge" "cluster_bridge" {
|
resource "proxmox_virtual_environment_network_linux_bridge" "cluster_bridge" {
|
||||||
|
|
@ -122,10 +160,23 @@ resource "proxmox_virtual_environment_vm" "vm" {
|
||||||
file_id = each.value.iso_file
|
file_id = each.value.iso_file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Talos reads this Proxmox-generated cidata disk through its NoCloud platform.
|
||||||
|
initialization {
|
||||||
|
datastore_id = var.talos_image_storage
|
||||||
|
interface = "ide3"
|
||||||
|
type = "nocloud"
|
||||||
|
meta_data_file_id = proxmox_virtual_environment_file.nocloud_metadata[each.key].id
|
||||||
|
network_data_file_id = proxmox_virtual_environment_file.nocloud_network_config[each.key].id
|
||||||
|
user_data_file_id = proxmox_virtual_environment_file.nocloud_user_data[each.key].id
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure image staging and bridge setup complete before VMs
|
# Ensure image staging and bridge setup complete before VMs
|
||||||
depends_on = [
|
depends_on = [
|
||||||
proxmox_virtual_environment_network_linux_bridge.cluster_bridge,
|
proxmox_virtual_environment_network_linux_bridge.cluster_bridge,
|
||||||
null_resource.stage_talos_image,
|
null_resource.stage_talos_image,
|
||||||
|
proxmox_virtual_environment_file.nocloud_network_config,
|
||||||
|
proxmox_virtual_environment_file.nocloud_metadata,
|
||||||
|
proxmox_virtual_environment_file.nocloud_user_data,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
|
||||||
import gzip
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -11,9 +7,6 @@ import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def api_request(base_url, token, method, path, data=None):
|
def api_request(base_url, token, method, path, data=None):
|
||||||
url = base_url.rstrip("/") + path
|
url = base_url.rstrip("/") + path
|
||||||
req = urllib.request.Request(url, method=method)
|
req = urllib.request.Request(url, method=method)
|
||||||
|
|
@ -28,37 +21,23 @@ def api_request(base_url, token, method, path, data=None):
|
||||||
return json.loads(response.read().decode())
|
return json.loads(response.read().decode())
|
||||||
|
|
||||||
|
|
||||||
def ensure_image(base_url, token, node, node_address, storage, image_url, filename, network_config_path=None):
|
def ensure_image(base_url, token, node, storage, image_url, filename):
|
||||||
content_path = f"/api2/json/nodes/{node}/storage/{storage}/content"
|
content_path = f"/api2/json/nodes/{node}/storage/{storage}/content"
|
||||||
response = api_request(base_url, token, "GET", content_path)
|
response = api_request(base_url, token, "GET", content_path)
|
||||||
|
|
||||||
if network_config_path:
|
for item in response.get("data", []):
|
||||||
if any(item.get("volid", "").endswith(filename) for item in response.get("data", [])):
|
if item.get("volid", "").endswith(filename):
|
||||||
return filename, False
|
return filename, False
|
||||||
source_filename = f"source-{filename}"
|
|
||||||
else:
|
|
||||||
for item in response.get("data", []):
|
|
||||||
if item.get("volid", "").endswith(filename):
|
|
||||||
return filename, False
|
|
||||||
source_filename = filename
|
|
||||||
|
|
||||||
response = api_request(base_url, token, "GET", content_path)
|
download_path = f"/api2/json/nodes/{node}/storage/{storage}/download-url"
|
||||||
source_present = any(item.get("volid", "").endswith(source_filename) for item in response.get("data", []))
|
task = api_request(base_url, token, "POST", download_path, {
|
||||||
if not source_present:
|
"content": "iso",
|
||||||
download_path = f"/api2/json/nodes/{node}/storage/{storage}/download-url"
|
"filename": filename,
|
||||||
task = api_request(base_url, token, "POST", download_path, {
|
"url": image_url,
|
||||||
"content": "iso",
|
})
|
||||||
"filename": source_filename,
|
wait_for_task(base_url, token, node, task.get("data"))
|
||||||
"url": image_url,
|
if not wait_for_storage_content(base_url, token, node, storage, filename):
|
||||||
})
|
raise RuntimeError(f"download task finished but {filename} was not found in {storage}")
|
||||||
wait_for_task(base_url, token, node, task.get("data"))
|
|
||||||
if not wait_for_storage_content(base_url, token, node, storage, source_filename):
|
|
||||||
raise RuntimeError(f"download task finished but {source_filename} was not found in {storage}")
|
|
||||||
|
|
||||||
if network_config_path:
|
|
||||||
build_custom_iso(base_url, token, node_address, storage, source_filename, filename, network_config_path)
|
|
||||||
if not wait_for_storage_content(base_url, token, node, storage, filename):
|
|
||||||
raise RuntimeError(f"custom Talos ISO {filename} was not found in {storage} after baking")
|
|
||||||
|
|
||||||
response = api_request(base_url, token, "GET", content_path)
|
response = api_request(base_url, token, "GET", content_path)
|
||||||
for item in response.get("data", []):
|
for item in response.get("data", []):
|
||||||
|
|
@ -95,55 +74,18 @@ def wait_for_storage_content(base_url, token, node, storage, filename):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_ssh(node_address, script):
|
|
||||||
ssh = shutil.which("ssh")
|
|
||||||
if ssh is None:
|
|
||||||
raise RuntimeError("ssh is required to customize Talos ISO on the Proxmox host")
|
|
||||||
subprocess.run([ssh, f"root@{node_address}", script], check=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def build_custom_iso(base_url, token, node_address, storage, source_filename, output_filename, network_config_path):
|
|
||||||
network_config = Path(network_config_path).read_text(encoding="utf-8").strip()
|
|
||||||
installer_meta = base64.b64encode(gzip.compress(f"0xa={network_config}".encode("utf-8"), compresslevel=9)).decode("ascii")
|
|
||||||
|
|
||||||
script = (
|
|
||||||
"set -eu; "
|
|
||||||
f"SRC='/var/lib/vz/template/iso/{source_filename}'; "
|
|
||||||
f"OUT='/var/lib/vz/template/iso/{output_filename}'; "
|
|
||||||
"TMPDIR=$(mktemp -d); trap 'rm -rf \"$TMPDIR\"' EXIT; "
|
|
||||||
"xorriso -osirrox on -indev \"$SRC\" -extract / \"$TMPDIR/extract\" >/dev/null 2>&1; "
|
|
||||||
"for CFG in \"$TMPDIR/extract/isolinux/isolinux.cfg\" \"$TMPDIR/extract/boot/grub/grub.cfg\"; do "
|
|
||||||
" if [ -f \"$CFG\" ]; then "
|
|
||||||
f" sed -i '0,/---/s// talos.environment=INSTALLER_META_BASE64={installer_meta} ---/' \"$CFG\"; "
|
|
||||||
" fi; "
|
|
||||||
"done; "
|
|
||||||
"xorriso -as mkisofs "
|
|
||||||
"-o \"$OUT\" "
|
|
||||||
"-isohybrid-mbr \"$TMPDIR/extract/isolinux/isohdpfx.bin\" "
|
|
||||||
"-c isolinux/boot.cat "
|
|
||||||
"-b isolinux/isolinux.bin "
|
|
||||||
"-no-emul-boot -boot-load-size 4 -boot-info-table "
|
|
||||||
"-eltorito-alt-boot -e EFI/BOOT/BOOTX64.EFI -no-emul-boot -isohybrid-gpt-basdat "
|
|
||||||
"-V TALOS \"$TMPDIR/extract\" >/dev/null 2>&1"
|
|
||||||
)
|
|
||||||
run_ssh(node_address, script)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--api-url", required=True)
|
parser.add_argument("--api-url", required=True)
|
||||||
parser.add_argument("--api-token", required=True)
|
parser.add_argument("--api-token", required=True)
|
||||||
parser.add_argument("--node", required=True)
|
parser.add_argument("--node", required=True)
|
||||||
parser.add_argument("--node-address", required=True)
|
|
||||||
parser.add_argument("--storage", required=True)
|
parser.add_argument("--storage", required=True)
|
||||||
parser.add_argument("--image-url", required=True)
|
parser.add_argument("--image-url", required=True)
|
||||||
parser.add_argument("--filename", required=True)
|
parser.add_argument("--filename", required=True)
|
||||||
parser.add_argument("--network-config")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.node_address, args.storage, args.image_url, args.filename, args.network_config)
|
filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.storage, args.image_url, args.filename)
|
||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
sys.stderr.write(exc.read().decode())
|
sys.stderr.write(exc.read().decode())
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,32 @@
|
||||||
links:
|
version: 2
|
||||||
|
ethernets:
|
||||||
%{ for i, net in node.networks ~}
|
%{ for i, net in node.networks ~}
|
||||||
- name: eth${i}
|
eth${i}:
|
||||||
up: true
|
match:
|
||||||
%{ if net.vlan_id > 0 ~}
|
macaddress: "${net.mac_address}"
|
||||||
- name: eth${i}.${net.vlan_id}
|
dhcp4: false
|
||||||
logical: true
|
%{ if i == 0 && net.vlan_id == 0 ~}
|
||||||
up: true
|
addresses:
|
||||||
kind: vlan
|
- ${net.ip}/${split("/", net.cidr)[1]}
|
||||||
type: ether
|
gateway4: ${net.gateway}
|
||||||
parentName: eth${i}
|
nameservers:
|
||||||
vlan:
|
addresses:
|
||||||
vlanID: ${net.vlan_id}
|
%{ for dns in dns_servers ~}
|
||||||
vlanProtocol: 802.1q
|
- ${dns}
|
||||||
|
%{ endfor ~}
|
||||||
%{ endif ~}
|
%{ endif ~}
|
||||||
%{ endfor ~}
|
%{ endfor ~}
|
||||||
addresses:
|
%{ if node.networks[0].vlan_id > 0 ~}
|
||||||
- address: ${node.networks[0].ip}/${split("/", node.networks[0].cidr)[1]}
|
vlans:
|
||||||
linkName: %{ if node.networks[0].vlan_id > 0 }eth0.${node.networks[0].vlan_id}%{ else }eth0%{ endif }
|
eth0.${node.networks[0].vlan_id}:
|
||||||
family: inet4
|
id: ${node.networks[0].vlan_id}
|
||||||
scope: global
|
link: eth0
|
||||||
flags: permanent
|
addresses:
|
||||||
layer: platform
|
- ${node.networks[0].ip}/${split("/", node.networks[0].cidr)[1]}
|
||||||
routes:
|
gateway4: ${node.networks[0].gateway}
|
||||||
- family: inet4
|
nameservers:
|
||||||
gateway: ${node.networks[0].gateway}
|
addresses:
|
||||||
outLinkName: %{ if node.networks[0].vlan_id > 0 }eth0.${node.networks[0].vlan_id}%{ else }eth0%{ endif }
|
|
||||||
table: main
|
|
||||||
priority: 1024
|
|
||||||
scope: global
|
|
||||||
type: unicast
|
|
||||||
protocol: static
|
|
||||||
layer: platform
|
|
||||||
resolvers:
|
|
||||||
- dnsServers:
|
|
||||||
%{ for dns in dns_servers ~}
|
%{ for dns in dns_servers ~}
|
||||||
- ${dns}
|
- ${dns}
|
||||||
%{ endfor ~}
|
%{ endfor ~}
|
||||||
layer: platform
|
%{ endif ~}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue