Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
ipxe/
|
||||
rendered/
|
||||
tftp/
|
||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
||||
FROM docker.io/library/debian:stable AS builder-ipxe
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
liblzma-dev \
|
||||
genisoimage
|
||||
ARG IPXE_VERSION=b41bda4413bf286d7b7a449bc05e1531da1eec2e
|
||||
RUN curl -L https://github.com/ipxe/ipxe/archive/${IPXE_VERSION}.tar.gz | tar -xz
|
||||
WORKDIR /ipxe-${IPXE_VERSION}/src
|
||||
|
||||
# Enable HTTPS
|
||||
RUN sed -i 's/^#undef[\t ]DOWNLOAD_PROTO_HTTPS.*$/#define DOWNLOAD_PROTO_HTTPS/g' config/general.h
|
||||
|
||||
RUN mkdir /build
|
||||
RUN make -j$(nproc) bin/ipxe.pxe && cp bin/ipxe.pxe /build
|
||||
RUN make -j$(nproc) bin-x86_64-efi/ipxe.efi && cp bin-x86_64-efi/ipxe.efi /build
|
||||
|
||||
FROM docker.io/library/python:3.13-slim AS config-renderer
|
||||
COPY --from=docker.io/hairyhenderson/gomplate:v4.3 /gomplate /bin/gomplate
|
||||
COPY ./requirements.txt /requirements.txt
|
||||
RUN pip install -r /requirements.txt
|
||||
COPY ./generate.sh /generate.sh
|
||||
COPY ./tools /tools
|
||||
COPY ./nodes /nodes
|
||||
COPY ./templates /templates
|
||||
RUN ./generate.sh
|
||||
|
||||
FROM docker.io/library/alpine:3.22.2 AS runtime
|
||||
RUN apk add dnsmasq
|
||||
|
||||
COPY --from=builder-ipxe /build/ipxe.pxe /tftproot/
|
||||
COPY --from=builder-ipxe /build/ipxe.efi /tftproot/
|
||||
COPY --from=config-renderer /rendered/boot.ipxe /tftproot/
|
||||
COPY --from=config-renderer /rendered/dnsmasq.conf /dnsmasq.conf
|
||||
|
||||
EXPOSE 67/udp
|
||||
EXPOSE 69/udp
|
||||
|
||||
CMD ["dnsmasq", "--conf-file=/dnsmasq.conf", "--keep-in-foreground", "--user=root", "--log-facility=-", "--port=0"]
|
||||
5
generate.sh
Executable file
5
generate.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
|
||||
|
||||
${SCRIPT_DIR}/tools/merge.py ./nodes | gomplate -d nodes=stdin://nodes.json -d dhcp=${SCRIPT_DIR}/dhcp.yaml --input-dir ${SCRIPT_DIR}/templates --output-dir ${SCRIPT_DIR}/rendered
|
||||
9
nodes/_defaults.yaml
Normal file
9
nodes/_defaults.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
schematicID: !schematic "_schematic.yaml"
|
||||
arch: amd64
|
||||
talosVersion: v1.11.3
|
||||
kernelArgs: talos.platform=metal console=tty0 init_on_alloc=1 slab_nomerge pti=on consoleblank=0 nvme_core.io_timeout=4294967295 printk.devkmsg=on selinux=1 lockdown=confidentiality
|
||||
dns0: 1.1.1.1
|
||||
dns1: 8.8.8.8
|
||||
ntp: nl.pool.ntp.org
|
||||
install: false
|
||||
upgradeIPXE: false
|
||||
5
nodes/_schematic.yaml
Normal file
5
nodes/_schematic.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
customization:
|
||||
systemExtensions:
|
||||
officialExtensions:
|
||||
- siderolabs/iscsi-tools
|
||||
- siderolabs/util-linux-tools
|
||||
3
nodes/production/_defaults.yaml
Normal file
3
nodes/production/_defaults.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
netmask: 255.255.252.0
|
||||
gateway: 10.0.0.1
|
||||
install: true
|
||||
3
nodes/production/helios.yaml
Normal file
3
nodes/production/helios.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
serial: 5CZ7NX2
|
||||
interface: enp2s0
|
||||
ip: 10.0.0.202
|
||||
3
nodes/production/hyperion.yaml
Normal file
3
nodes/production/hyperion.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
serial: F3PKRH2
|
||||
interface: enp3s0
|
||||
ip: 10.0.0.201
|
||||
3
nodes/production/selene.yaml
Normal file
3
nodes/production/selene.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
serial: J33CHY2
|
||||
interface: enp2s0
|
||||
ip: 10.0.0.203
|
||||
3
nodes/vm/_defaults.yaml
Normal file
3
nodes/vm/_defaults.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
netmask: 255.255.255.0
|
||||
gateway: 192.168.1.1
|
||||
upgradeIPXE: ipxe.pxe
|
||||
4
nodes/vm/vm.yaml
Normal file
4
nodes/vm/vm.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
serial: vm
|
||||
interface: enp1s0
|
||||
ip: 192.168.1.2
|
||||
install: true
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
PyYAML==6.0.3
|
||||
requests==2.32.5
|
||||
36
templates/boot.ipxe
Normal file
36
templates/boot.ipxe
Normal file
@@ -0,0 +1,36 @@
|
||||
#!ipxe
|
||||
|
||||
dhcp
|
||||
|
||||
:start
|
||||
# Is a known serial is set, execute that
|
||||
# If an unknown serial is set, exit
|
||||
# If no serial is set, ask the user
|
||||
goto node_${serial} || goto manual
|
||||
# Default behavior (non install mode) is to exit iPXE script
|
||||
|
||||
{{ range (datasource "nodes" | jsonArray) }}
|
||||
{{- if .install }}
|
||||
# {{ .filename }}
|
||||
:node_{{ .serial }}
|
||||
{{- $ipArg := printf "ip=%s::%s:%s:%s:%s::%s:%s:%s" .ip .gateway .netmask .hostname .interface .dns0 .dns1 .ntp }}
|
||||
{{- $kernelArgs := printf "%s %s" $ipArg .kernelArgs }}
|
||||
imgfree
|
||||
kernel https://pxe.factory.talos.dev/image/{{ .schematicID }}/{{ .talosVersion }}/kernel-{{ .arch }} {{ $kernelArgs }} {{- if .upgradeIPXE }} || boot {{ .upgradeIPXE }} {{- end }}
|
||||
initrd https://pxe.factory.talos.dev/image/{{ .schematicID }}/{{ .talosVersion }}/initramfs-{{ .arch }}.xz
|
||||
boot
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
|
||||
:manual
|
||||
menu Select node
|
||||
{{ range (datasource "nodes" | jsonArray) }}
|
||||
item {{ .serial }} {{ .hostname }}
|
||||
{{ end }}
|
||||
choose selected || goto cancel
|
||||
goto node_${selected}
|
||||
|
||||
:cancel
|
||||
echo Type exit to restart script
|
||||
shell
|
||||
goto start
|
||||
40
templates/dnsmasq.conf
Normal file
40
templates/dnsmasq.conf
Normal file
@@ -0,0 +1,40 @@
|
||||
{{ $tftpIp := (ds "dhcp").tftpIp -}}
|
||||
|
||||
enable-tftp
|
||||
tftp-root=/tftproot
|
||||
tftp-single-port
|
||||
|
||||
dhcp-vendorclass=BIOS,PXEClient:Arch:00000
|
||||
dhcp-vendorclass=UEFI,PXEClient:Arch:00007
|
||||
dhcp-vendorclass=UEFI64,PXEClient:Arch:00009
|
||||
|
||||
# 1st stage: pxe rom boot on ipxe
|
||||
dhcp-boot=net:BIOS,ipxe.pxe,{{ $tftpIp }},{{ $tftpIp }}
|
||||
dhcp-boot=net:UEFI,ipxe.efi,{{ $tftpIp }},{{ $tftpIp }}
|
||||
dhcp-boot=net:UEFI64,ipxe.efi,{{ $tftpIp }},{{ $tftpIp }}
|
||||
|
||||
# Based on logic in https://gist.github.com/robinsmidsrod/4008017
|
||||
# iPXE sends a 175 option, checking suboptions
|
||||
dhcp-match=set:ipxe-http,175,19
|
||||
dhcp-match=set:ipxe-https,175,20
|
||||
dhcp-match=set:ipxe-menu,175,39
|
||||
|
||||
dhcp-match=set:ipxe-pxe,175,33
|
||||
dhcp-match=set:ipxe-bzimage,175,24
|
||||
dhcp-match=set:ipxe-iscsi,175,17
|
||||
|
||||
dhcp-match=set:ipxe-efi,175,36
|
||||
|
||||
# set ipxe-ok tag if we have correct combination
|
||||
tag-if=set:ipxe-ok,tag:ipxe-http,tag:ipxe-https
|
||||
|
||||
# these create option 43 cruft, which is required in proxy mode
|
||||
# TFTP IP is required on all dhcp-boot lines (unless dnsmasq itself acts as tftp server?)
|
||||
pxe-service=tag:!ipxe-ok,X86PC,PXE,undionly.kpxe,{{ $tftpIp }}
|
||||
pxe-service=tag:!ipxe-ok,IA32_EFI,PXE,snponlyx32.efi,{{ $tftpIp }}
|
||||
pxe-service=tag:!ipxe-ok,BC_EFI,PXE,snponly.efi,{{ $tftpIp }}
|
||||
pxe-service=tag:!ipxe-ok,X86-64_EFI,PXE,snponly.efi,{{ $tftpIp }}
|
||||
|
||||
# later match overrides previous, keep ipxe script last
|
||||
# server address must be non zero, but can be anything as long as iPXE script is not fetched over TFTP
|
||||
dhcp-boot=tag:ipxe-ok,boot.ipxe,,{{ $tftpIp }}
|
||||
96
tools/merge.py
Executable file
96
tools/merge.py
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Adapted from: https://enix.io/en/blog/pxe-talos/
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_schematic_id(schematic: str):
|
||||
"""Lookup the schematic id associated with a given schematic"""
|
||||
r = requests.post("https://factory.talos.dev/schematics", data=schematic)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
return data["id"]
|
||||
|
||||
|
||||
def schematic_constructor(directory: pathlib.Path):
|
||||
"""Load specified schematic file and get the assocatied schematic id"""
|
||||
|
||||
def constructor(loader: yaml.SafeLoader, node: yaml.nodes.ScalarNode):
|
||||
filename = str(loader.construct_scalar(node))
|
||||
try:
|
||||
schematic = directory.joinpath(filename).read_text()
|
||||
return get_schematic_id(schematic)
|
||||
except Exception:
|
||||
raise yaml.MarkedYAMLError("Failed to load schematic", node.start_mark)
|
||||
|
||||
return constructor
|
||||
|
||||
|
||||
def get_loader(directory: pathlib.Path):
|
||||
"""Add special constructors to yaml loader"""
|
||||
loader = yaml.SafeLoader
|
||||
loader.add_constructor("!schematic", schematic_constructor(directory))
|
||||
|
||||
return loader
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_defaults(directory: pathlib.Path, root: pathlib.Path):
|
||||
"""Compute the defaults from the provided directory and parents."""
|
||||
try:
|
||||
with open(directory.joinpath("_defaults.yaml")) as fyaml:
|
||||
yml_data = yaml.load(fyaml, Loader=get_loader(directory))
|
||||
except OSError:
|
||||
yml_data = {}
|
||||
|
||||
# Stop recursion when reaching root directory
|
||||
if directory != root:
|
||||
return get_defaults(directory.parent, root) | yml_data
|
||||
else:
|
||||
return yml_data
|
||||
|
||||
|
||||
def walk_files(root: pathlib.Path):
|
||||
"""Get all files that do not start with and underscore"""
|
||||
for dirpath, _dirnames, filenames in root.walk():
|
||||
for fn in filenames:
|
||||
if not fn.startswith("_"):
|
||||
yield dirpath.joinpath(fn)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("directory", type=pathlib.Path)
|
||||
parser.add_argument("-f", "--filter")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = []
|
||||
for fullname in walk_files(args.directory):
|
||||
filename = (
|
||||
str(fullname.relative_to(args.directory).parent) + "/" + fullname.stem
|
||||
)
|
||||
|
||||
if args.filter is not None and not filename.startswith(args.filter):
|
||||
continue
|
||||
|
||||
with open(fullname) as fyaml:
|
||||
yml_data = yaml.load(fyaml, Loader=get_loader(fullname.parent))
|
||||
yml_data = get_defaults(fullname.parent, args.directory) | yml_data
|
||||
yml_data["hostname"] = fullname.stem
|
||||
yml_data["filename"] = filename
|
||||
data.append(yml_data)
|
||||
|
||||
# Dump everything to json
|
||||
print(json.dumps(data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
vm/cluster-vm.xml
Normal file
13
vm/cluster-vm.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<network xmlns:dnsmasq='http://libvirt.org/schemas/network/dnsmasq/1.0'>
|
||||
<name>cluster-vm</name>
|
||||
<bridge name="cluster0" stp="on" delay="0"/>
|
||||
<forward mode='nat'>
|
||||
<nat/>
|
||||
</forward>
|
||||
<ip address="192.168.1.1" netmask="255.255.255.0">
|
||||
<dhcp>
|
||||
<range start="192.168.1.2" end="192.168.1.254"/>
|
||||
<bootp file='boot.ipxe'/>
|
||||
</dhcp>
|
||||
</ip>
|
||||
</network>
|
||||
15
vm/create.sh
Executable file
15
vm/create.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
|
||||
source ${SCRIPT_DIR}/helper.sh
|
||||
|
||||
if [[ $(virsh --connect="${CONNECTION}" net-list --all | grep -c "${NETWORK}") == "0" ]]; then
|
||||
virsh --connect="${CONNECTION}" net-define "${SCRIPT_DIR}/${NETWORK}.xml"
|
||||
virsh --connect="${CONNECTION}" net-start "${NETWORK}"
|
||||
virsh --connect="${CONNECTION}" net-autostart "${NETWORK}"
|
||||
fi
|
||||
|
||||
virt-install --connect="${CONNECTION}" --name="${VM_NAME}" --vcpus="${VCPUS}" --memory="${RAM_MB}" \
|
||||
--os-variant="linux2022" \
|
||||
--disk="size=${DISK_GB}" \
|
||||
--pxe \
|
||||
--network network="${NETWORK}"
|
||||
8
vm/destroy.sh
Executable file
8
vm/destroy.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
|
||||
source ${SCRIPT_DIR}/helper.sh
|
||||
|
||||
virsh --connect="${CONNECTION}" destroy "${VM_NAME}"
|
||||
virsh --connect="${CONNECTION}" undefine "${VM_NAME}" --remove-all-storage
|
||||
virsh --connect="${CONNECTION}" net-destroy "${NETWORK}"
|
||||
virsh --connect="${CONNECTION}" net-undefine "${NETWORK}"
|
||||
10
vm/helper.sh
Normal file
10
vm/helper.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
set -euxo pipefail
|
||||
VM_NAME="test"
|
||||
VCPUS="2"
|
||||
RAM_MB="2048"
|
||||
DISK_GB="10"
|
||||
NETWORK=cluster-vm
|
||||
CONNECTION="qemu:///system"
|
||||
|
||||
IPXE_VERSION=b41bda4413bf286d7b7a449bc05e1531da1eec2e
|
||||
IPXE_BIN=bin/ipxe.pxe
|
||||
7
vm/start.sh
Executable file
7
vm/start.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
|
||||
source ${SCRIPT_DIR}/helper.sh
|
||||
|
||||
virsh --connect="${CONNECTION}" start ${VM_NAME}
|
||||
virt-viewer --connect="${CONNECTION}" ${VM_NAME}
|
||||
virsh --connect="${CONNECTION}" shutdown ${VM_NAME}
|
||||
26
vm/tftp.sh
Executable file
26
vm/tftp.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
|
||||
source ${SCRIPT_DIR}/helper.sh
|
||||
|
||||
TFTP_DIR=${SCRIPT_DIR}/../tftp
|
||||
rm -rf "${TFTP_DIR}"
|
||||
mkdir -p "${TFTP_DIR}"
|
||||
|
||||
IPXE_DIR=${SCRIPT_DIR}/../ipxe
|
||||
IPXE_FILE=${IPXE_DIR}/ipxe-${IPXE_VERSION}/src/${IPXE_BIN}
|
||||
if [ ! -f "${IPXE_FILE}" ]; then
|
||||
mkdir -p "${IPXE_DIR}"
|
||||
rm -rf "${IPXE_DIR}/ipxe-${IPXE_VERSION}"
|
||||
curl -L https://github.com/ipxe/ipxe/archive/${IPXE_VERSION}.tar.gz | tar -xz -C "${IPXE_DIR}"
|
||||
cd "${IPXE_DIR}/ipxe-${IPXE_VERSION}/src"
|
||||
sed -i 's/^#undef[\t ]DOWNLOAD_PROTO_HTTPS.*$/#define DOWNLOAD_PROTO_HTTPS/g' config/general.h
|
||||
make -j$(nproc) ${IPXE_BIN}
|
||||
cd -
|
||||
fi
|
||||
|
||||
${SCRIPT_DIR}/../generate.sh
|
||||
|
||||
cp ${SCRIPT_DIR}/../rendered/boot.ipxe ${TFTP_DIR}
|
||||
cp ${IPXE_FILE} ${TFTP_DIR}
|
||||
|
||||
sudo in.tftpd -L --secure ./tftp
|
||||
Reference in New Issue
Block a user