fix: bake talos bootstrap network into iso
This commit is contained in:
parent
bb38c261af
commit
d61225a9d3
|
|
@ -39,7 +39,15 @@ locals {
|
||||||
|
|
||||||
talos_iso_download_url = "https://factory.talos.dev/image/${var.talos_factory_schematic_id}/${var.talos_version}/nocloud-${var.talos_image_architecture}.iso"
|
talos_iso_download_url = "https://factory.talos.dev/image/${var.talos_factory_schematic_id}/${var.talos_version}/nocloud-${var.talos_image_architecture}.iso"
|
||||||
|
|
||||||
image_target_nodes = toset(distinct([for node in var.nodes : lookup(node, "proxmox_node", var.proxmox_node)]))
|
image_target_nodes = toset([for node in var.nodes : node.name])
|
||||||
|
|
||||||
|
node_iso_filenames = {
|
||||||
|
for node in var.nodes : node.name => "talos-${trimprefix(var.talos_version, "v")}-${substr(var.talos_factory_schematic_id, 0, 12)}-${node.name}-${var.talos_image_architecture}.iso"
|
||||||
|
}
|
||||||
|
|
||||||
|
node_iso_files = {
|
||||||
|
for node in var.nodes : node.name => format("%s:iso/%s", var.talos_image_storage, local.node_iso_filenames[node.name])
|
||||||
|
}
|
||||||
|
|
||||||
# --- VM Resource Transformation (No changes needed below this line) ---
|
# --- VM Resource Transformation (No changes needed below this line) ---
|
||||||
all_nodes_transformed = {
|
all_nodes_transformed = {
|
||||||
|
|
@ -53,7 +61,7 @@ locals {
|
||||||
disk_size = node.disk_size
|
disk_size = node.disk_size
|
||||||
disk_storage = var.disk_storage
|
disk_storage = var.disk_storage
|
||||||
tags = lookup(node, "tags", [node.role])
|
tags = lookup(node, "tags", [node.role])
|
||||||
iso_file = var.talos_iso_file
|
iso_file = local.node_iso_files[node.name]
|
||||||
additional_disk_size = lookup(node, "additional_disk_size", null)
|
additional_disk_size = lookup(node, "additional_disk_size", null)
|
||||||
additional_disk_storage = lookup(node, "additional_disk_size", null) != null ? var.additional_disk_storage : null
|
additional_disk_storage = lookup(node, "additional_disk_size", null) != null ? var.additional_disk_storage : null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,28 @@ provider "proxmox" {
|
||||||
api_token = var.proxmox_api_token
|
api_token = var.proxmox_api_token
|
||||||
insecure = true
|
insecure = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "null_resource" "stage_talos_image" {
|
resource "null_resource" "stage_talos_image" {
|
||||||
for_each = var.talos_image_update_mode == "download" ? local.image_target_nodes : []
|
for_each = var.talos_image_update_mode == "download" ? { for node in var.nodes : node.name => node } : {}
|
||||||
|
|
||||||
triggers = {
|
triggers = {
|
||||||
node_name = each.value
|
node_name = each.value.name
|
||||||
image_url = local.talos_iso_download_url
|
proxmox_node = lookup(each.value, "proxmox_node", var.proxmox_node)
|
||||||
storage = var.talos_image_storage
|
image_url = local.talos_iso_download_url
|
||||||
talos_version = var.talos_version
|
storage = var.talos_image_storage
|
||||||
filename = "talos-${trimprefix(var.talos_version, "v")}-${substr(var.talos_factory_schematic_id, 0, 12)}-${var.talos_image_architecture}.iso"
|
talos_version = var.talos_version
|
||||||
|
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', '${each.value}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.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}', '--network-config', r'${local_file.metal_network_config[each.key].filename}'], check=True)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
depends_on = [local_file.metal_network_config]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# 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" {
|
||||||
for_each = local.unique_bridges
|
for_each = local.unique_bridges
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
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):
|
||||||
|
|
@ -23,20 +29,30 @@ 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, storage, image_url, filename):
|
def ensure_image(base_url, token, node, storage, image_url, filename, network_config_path=None):
|
||||||
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)
|
||||||
for item in response.get("data", []):
|
for item in response.get("data", []):
|
||||||
if item.get("volid", "").endswith(filename):
|
if item.get("volid", "").endswith(filename):
|
||||||
return filename, False
|
return filename, False
|
||||||
|
|
||||||
download_path = f"/api2/json/nodes/{node}/storage/{storage}/download-url"
|
source_filename = filename
|
||||||
task = api_request(base_url, token, "POST", download_path, {
|
if network_config_path:
|
||||||
"content": "iso",
|
source_filename = f"base-{filename}"
|
||||||
"filename": filename,
|
|
||||||
"url": image_url,
|
response = api_request(base_url, token, "GET", content_path)
|
||||||
})
|
source_present = any(item.get("volid", "").endswith(source_filename) for item in response.get("data", []))
|
||||||
wait_for_task(base_url, token, node, task.get("data"))
|
if not source_present:
|
||||||
|
download_path = f"/api2/json/nodes/{node}/storage/{storage}/download-url"
|
||||||
|
task = api_request(base_url, token, "POST", download_path, {
|
||||||
|
"content": "iso",
|
||||||
|
"filename": source_filename,
|
||||||
|
"url": image_url,
|
||||||
|
})
|
||||||
|
wait_for_task(base_url, token, node, task.get("data"))
|
||||||
|
|
||||||
|
if network_config_path:
|
||||||
|
build_custom_iso(base_url, token, node, storage, source_filename, filename, network_config_path)
|
||||||
|
|
||||||
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", []):
|
||||||
|
|
@ -63,6 +79,42 @@ def wait_for_task(base_url, token, node, upid):
|
||||||
raise RuntimeError("timed out waiting for ISO download task to finish")
|
raise RuntimeError("timed out waiting for ISO download task to finish")
|
||||||
|
|
||||||
|
|
||||||
|
def build_custom_iso(base_url, token, node, storage, source_filename, output_filename, network_config_path):
|
||||||
|
storage_path = f"/var/lib/vz/template/iso/{source_filename}"
|
||||||
|
output_path = f"/var/lib/vz/template/iso/{output_filename}"
|
||||||
|
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")
|
||||||
|
|
||||||
|
if shutil.which("docker") is None:
|
||||||
|
raise RuntimeError("docker is required to build Talos ISO with embedded META")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
temp_path = Path(temp_dir)
|
||||||
|
script_path = temp_path / "build.sh"
|
||||||
|
script_path.write_text(
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"set -eu\n"
|
||||||
|
"apk add --no-cache xorriso >/dev/null\n"
|
||||||
|
"cp /input/source.iso /work/source.iso\n"
|
||||||
|
"xorriso -indev /work/source.iso -outdev /output/result.iso -boot_image any replay \\\n"
|
||||||
|
f" -append_partition 2 0x0 /dev/null \\\n"
|
||||||
|
f" -boot_image any keep \\\n"
|
||||||
|
f" -map /input/cmdline /cmdline\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
cmdline_path = temp_path / "cmdline"
|
||||||
|
cmdline_path.write_text(f"talos.environment=INSTALLER_META_BASE64={installer_meta}\n", encoding="utf-8")
|
||||||
|
subprocess.run([
|
||||||
|
"docker", "run", "--rm",
|
||||||
|
"-v", f"{script_path}:/input/build.sh:ro",
|
||||||
|
"-v", f"{cmdline_path}:/input/cmdline:ro",
|
||||||
|
"-v", f"{Path(storage_path).resolve()}:/input/source.iso:ro",
|
||||||
|
"-v", f"{temp_path}:/work",
|
||||||
|
"-v", f"{Path(output_path).resolve().parent}:/output",
|
||||||
|
"alpine:3.20", "sh", "/input/build.sh",
|
||||||
|
], check=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--api-url", required=True)
|
parser.add_argument("--api-url", required=True)
|
||||||
|
|
@ -71,10 +123,11 @@ def main():
|
||||||
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.storage, args.image_url, args.filename)
|
filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.storage, args.image_url, args.filename, args.network_config)
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,20 @@ resource "local_file" "talconfig" {
|
||||||
cluster_svc_nets = var.cluster_svc_nets
|
cluster_svc_nets = var.cluster_svc_nets
|
||||||
cni_name = var.cni_name
|
cni_name = var.cni_name
|
||||||
|
|
||||||
# --- Pass the proxy config ---
|
|
||||||
image_cache_proxy = var.image_cache_proxy
|
image_cache_proxy = var.image_cache_proxy
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "local_file" "metal_network_config" {
|
||||||
|
for_each = { for node in var.nodes : node.name => node }
|
||||||
|
|
||||||
|
filename = "${path.module}/../generated/metal-network/${each.key}.yaml"
|
||||||
|
content = templatefile("${path.module}/templates/talos-metal-network.yaml.tpl", {
|
||||||
|
node = each.value
|
||||||
|
dns_servers = var.dns_servers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
# (The output block is already correct from the last fix)
|
# (The output block is already correct from the last fix)
|
||||||
output "node_organization" {
|
output "node_organization" {
|
||||||
description = "How your nodes are organized for Talos configuration"
|
description = "How your nodes are organized for Talos configuration"
|
||||||
|
|
|
||||||
39
terraform/templates/talos-metal-network.yaml.tpl
Normal file
39
terraform/templates/talos-metal-network.yaml.tpl
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
links:
|
||||||
|
%{ for i, net in node.networks ~}
|
||||||
|
- name: eth${i}
|
||||||
|
up: true
|
||||||
|
%{ if net.vlan_id > 0 ~}
|
||||||
|
- name: eth${i}.${net.vlan_id}
|
||||||
|
logical: true
|
||||||
|
up: true
|
||||||
|
kind: vlan
|
||||||
|
type: ether
|
||||||
|
parentName: eth${i}
|
||||||
|
vlan:
|
||||||
|
vlanID: ${net.vlan_id}
|
||||||
|
vlanProtocol: 802.1q
|
||||||
|
%{ endif ~}
|
||||||
|
%{ endfor ~}
|
||||||
|
addresses:
|
||||||
|
- address: ${node.networks[0].ip}/${split("/", node.networks[0].cidr)[1]}
|
||||||
|
linkName: %{ if node.networks[0].vlan_id > 0 }eth0.${node.networks[0].vlan_id}%{ else }eth0%{ endif }
|
||||||
|
family: inet4
|
||||||
|
scope: global
|
||||||
|
flags: permanent
|
||||||
|
layer: platform
|
||||||
|
routes:
|
||||||
|
- family: inet4
|
||||||
|
gateway: ${node.networks[0].gateway}
|
||||||
|
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 ~}
|
||||||
|
- ${dns}
|
||||||
|
%{ endfor ~}
|
||||||
|
layer: platform
|
||||||
Loading…
Reference in a new issue