diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3715bc8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = tab
+
+[*.yaml]
+indent_style = space
+indent_size = 4
+
+[{*.py,tools/merge}]
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
index ef4e642..19dd615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
-ipxe/
+.ipxe/
rendered/
-tftp/
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 182021a..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,40 +0,0 @@
-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"]
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..e781eef
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,2 @@
+dhcp:
+ tftpIp: 10.0.0.3
diff --git a/dhcp.yaml b/dhcp.yaml
deleted file mode 100644
index 183bc1f..0000000
--- a/dhcp.yaml
+++ /dev/null
@@ -1 +0,0 @@
-tftpIp: 10.0.0.3
diff --git a/generate.sh b/generate.sh
deleted file mode 100755
index 1c6d211..0000000
--- a/generate.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/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
diff --git a/nodes/_defaults.yaml b/nodes/_defaults.yaml
index 72530cc..725efc0 100644
--- a/nodes/_defaults.yaml
+++ b/nodes/_defaults.yaml
@@ -6,4 +6,3 @@ dns0: 1.1.1.1
dns1: 8.8.8.8
ntp: nl.pool.ntp.org
install: false
-upgradeIPXE: false
diff --git a/nodes/_schematic.yaml b/nodes/_schematic.yaml
index c766285..8cba538 100644
--- a/nodes/_schematic.yaml
+++ b/nodes/_schematic.yaml
@@ -1,5 +1,6 @@
customization:
- systemExtensions:
- officialExtensions:
- - siderolabs/iscsi-tools
- - siderolabs/util-linux-tools
+ systemExtensions:
+ officialExtensions:
+ - siderolabs/iscsi-tools
+ - siderolabs/util-linux-tools
+ - siderolabs/intel-ucode
diff --git a/nodes/vm/_defaults.yaml b/nodes/vm/_defaults.yaml
index 1066e5a..70bb7a5 100644
--- a/nodes/vm/_defaults.yaml
+++ b/nodes/vm/_defaults.yaml
@@ -1,3 +1,2 @@
netmask: 255.255.255.0
gateway: 192.168.1.1
-upgradeIPXE: ipxe.pxe
diff --git a/nodes/vm/vm.yaml b/nodes/vm/talos-vm.yaml
similarity index 73%
rename from nodes/vm/vm.yaml
rename to nodes/vm/talos-vm.yaml
index a3e9b55..f360796 100644
--- a/nodes/vm/vm.yaml
+++ b/nodes/vm/talos-vm.yaml
@@ -1,4 +1,4 @@
-serial: vm
+serial: talos-vm
interface: enp1s0
ip: 192.168.1.2
install: true
diff --git a/templates/boot.ipxe b/templates/boot.ipxe
index d8841b2..bd3c208 100644
--- a/templates/boot.ipxe
+++ b/templates/boot.ipxe
@@ -2,35 +2,24 @@
dhcp
+echo Starting ${serial}
+
: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
+goto node_${serial} || shell
# Default behavior (non install mode) is to exit iPXE script
-{{ range (datasource "nodes" | jsonArray) }}
+{{ range datasource "nodes" }}
{{- 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 }}
+kernel https://pxe.factory.talos.dev/image/{{ .schematicID }}/{{ .talosVersion }}/kernel-{{ .arch }} {{ $kernelArgs }}
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
diff --git a/templates/dnsmasq.conf b/templates/dnsmasq.conf
index 8bd1d9c..54b6340 100644
--- a/templates/dnsmasq.conf
+++ b/templates/dnsmasq.conf
@@ -1,4 +1,4 @@
-{{ $tftpIp := (ds "dhcp").tftpIp -}}
+{{ $tftpIp := (ds "config").dhcp.tftpIp -}}
enable-tftp
tftp-root=/tftproot
diff --git a/tools/merge.py b/tools/merge
similarity index 100%
rename from tools/merge.py
rename to tools/merge
diff --git a/tools/render b/tools/render
new file mode 100755
index 0000000..fccd78d
--- /dev/null
+++ b/tools/render
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT=$(git rev-parse --show-toplevel)
+RENDERED=${ROOT}/rendered
+TEMPLATES=${ROOT}/templates
+
+${ROOT}/tools/merge ./nodes > ${RENDERED}/nodes.json
+
+gomplate --input-dir ${TEMPLATES} --output-dir ${RENDERED} \
+ -d nodes=file://${RENDERED}/nodes.json \
+ -d config=${ROOT}/config.yaml \
diff --git a/tools/tftpd b/tools/tftpd
new file mode 100755
index 0000000..4b092e5
--- /dev/null
+++ b/tools/tftpd
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT=$(git rev-parse --show-toplevel)
+
+IPXE_VERSION=b41bda4413bf286d7b7a449bc05e1531da1eec2e
+IPXE_BIN=(bin/ipxe.pxe bin-x86_64-efi/ipxe.efi)
+
+IPXE_DIR=${ROOT}/.ipxe/ipxe-${IPXE_VERSION}
+
+function download_ipxe() {
+ base_dir=$(dirname ${IPXE_DIR})
+ # Download the iPXE source if needed
+ if [ ! -d "${IPXE_DIR}" ]; then
+ mkdir -p "${base_dir}"
+ curl -L https://github.com/ipxe/ipxe/archive/${IPXE_VERSION}.tar.gz | tar -xz -C "${base_dir}"
+ fi
+}
+
+function patch_ipxe() {
+ # Apply patches to iPXE source
+ cd "${IPXE_DIR}/src"
+ sed -i 's/^#undef[\t ]DOWNLOAD_PROTO_HTTPS.*$/#define DOWNLOAD_PROTO_HTTPS/g' config/general.h
+
+ cat > embed.ipxe << EOF
+#!ipxe
+
+dhcp
+chain boot.ipxe || shell
+EOF
+
+ cd - > /dev/null
+}
+
+function build_ipxe() {
+ cd "${IPXE_DIR}/src"
+ for bin in "${IPXE_BIN[@]}"; do
+ path=${IPXE_DIR}/src/${bin}
+ if [ ! -f "${path}" ]; then
+ make -j$(nproc) ${bin} EMBED=embed.ipxe
+ fi
+ done
+ cd - > /dev/null
+}
+
+function render() {
+ ${ROOT}/tools/render
+}
+
+function host_tftp() {
+ TFTP_DIR=$(mktemp --tmpdir -d tftp.XXX)
+ chmod 755 ${TFTP_DIR}
+ function cleanup() {
+ rm -rf ${TFTP_DIR}
+ }
+ trap cleanup EXIT
+
+ cp ${ROOT}/rendered/boot.ipxe ${TFTP_DIR}
+ for bin in "${IPXE_BIN[@]}"; do
+ path=${IPXE_DIR}/src/${bin}
+ cp ${path} ${TFTP_DIR}
+ done
+
+ echo "Starting tftpd"
+ sudo in.tftpd --verbosity 100 --permissive -L --secure ${TFTP_DIR}
+}
+
+download_ipxe
+patch_ipxe
+build_ipxe
+render
+host_tftp
diff --git a/tools/vm b/tools/vm
new file mode 100755
index 0000000..64b2124
--- /dev/null
+++ b/tools/vm
@@ -0,0 +1,154 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT=$(git rev-parse --show-toplevel)
+
+VM_NAME="talos-vm"
+VCPUS="2"
+RAM_MB="2048"
+DISK_GB="10"
+NETWORK=talos
+CONNECTION="qemu:///system"
+
+function define_network() {
+ config_file=$(mktemp)
+ cat > ${config_file} << EOF
+
+ ${NETWORK}
+
+
+
+
+
+
+
+
+
+
+
+EOF
+
+ function cleanup() {
+ rm ${config_file}
+ }
+ trap cleanup EXIT
+
+ if [[ $(virsh --connect="${CONNECTION}" net-list --all | grep -c "${NETWORK}") == "0" ]]; then
+ virsh --connect="${CONNECTION}" net-define "${config_file}"
+ virsh --connect="${CONNECTION}" net-start "${NETWORK}"
+ virsh --connect="${CONNECTION}" net-autostart "${NETWORK}"
+ fi
+
+ trap - EXIT
+ cleanup
+}
+
+
+function create() {
+ define_network
+
+ if [[ $(virsh --connect="${CONNECTION}" list --all | grep -c "${VM_NAME}") == "0" ]]; then
+ virt-install --connect="${CONNECTION}" --name="${VM_NAME}" --vcpus="${VCPUS}" --memory="${RAM_MB}" \
+ --os-variant="linux2022" \
+ --disk="size=${DISK_GB}" \
+ --pxe \
+ --sysinfo system.serial=${VM_NAME} \
+ --network network="${NETWORK}"
+ else
+ echo -n "VM already exists, start it with:
+ ${0} start
+"
+ exit -1
+ fi
+}
+
+function start() {
+ if [[ $(virsh --connect="${CONNECTION}" list --all | grep -c "${VM_NAME}") > "0" ]]; then
+ virsh --connect="${CONNECTION}" start ${VM_NAME}
+ virt-viewer --connect="${CONNECTION}" ${VM_NAME}
+ else
+ echo -n "VM doest not exists yet, create it with:
+ ${0} create
+"
+ exit -1
+ fi
+}
+
+function connect() {
+ if [[ $(virsh --connect="${CONNECTION}" list | grep -c "${VM_NAME}") > "0" ]]; then
+ virt-viewer --connect="${CONNECTION}" ${VM_NAME}
+ else
+ echo "VM is not running"
+ exit -1
+ fi
+}
+
+function stop() {
+ if [[ $(virsh --connect="${CONNECTION}" list | grep -c "${VM_NAME}") > "0" ]]; then
+ virsh --connect="${CONNECTION}" shutdown ${VM_NAME}
+ WAIT=240
+ for i in $(seq 0 1 ${WAIT}); do
+ echo -en "\rWaiting for VM to shutdown... (${i}/${WAIT})"
+
+ if [[ $(virsh --connect="${CONNECTION}" list | grep -c "${VM_NAME}") == "0" ]]; then
+ echo -e "\nVM successfully shutdown"
+ exit
+ fi
+
+ sleep 1
+ done
+
+ echo -e "\nDestroying VM"
+ virsh --connect="${CONNECTION}" destroy ${VM_NAME}
+ else
+ echo "VM is not running"
+ exit -1
+ fi
+}
+
+function delete() {
+ if [[ $(virsh --connect="${CONNECTION}" list --all | grep -c "${VM_NAME}") > "0" ]]; then
+ if [[ $(virsh --connect="${CONNECTION}" list | grep -c "${VM_NAME}") > "0" ]]; then
+ virsh --connect="${CONNECTION}" destroy "${VM_NAME}"
+ fi
+ virsh --connect="${CONNECTION}" undefine "${VM_NAME}" --remove-all-storage
+ else
+ echo "VM doest not exists"
+ exit -1
+ fi
+
+ if [[ $(virsh --connect="${CONNECTION}" net-list --all | grep -c "${NETWORK}") > "0" ]]; then
+ virsh --connect="${CONNECTION}" net-destroy "${NETWORK}"
+ virsh --connect="${CONNECTION}" net-undefine "${NETWORK}"
+ fi
+}
+
+function help() {
+ echo -n "Available commands:
+ start
+ stop
+ remove
+ connect
+"
+}
+
+COMMAND=${1:-}
+case ${COMMAND} in
+ create)
+ create Create the vm and perform first install
+ ;;
+ start)
+ start Start the vm
+ ;;
+ stop)
+ stop Stop the vm
+ ;;
+ delete)
+ delete Delete the vm
+ ;;
+ connect)
+ connect Connect to an already running vm
+ ;;
+ *)
+ help
+ ;;
+esac
diff --git a/vm/cluster-vm.xml b/vm/cluster-vm.xml
deleted file mode 100644
index 5806cd6..0000000
--- a/vm/cluster-vm.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
- cluster-vm
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vm/create.sh b/vm/create.sh
deleted file mode 100755
index ed31df2..0000000
--- a/vm/create.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/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}"
diff --git a/vm/destroy.sh b/vm/destroy.sh
deleted file mode 100755
index efac38b..0000000
--- a/vm/destroy.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/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}"
diff --git a/vm/helper.sh b/vm/helper.sh
deleted file mode 100644
index 6f88d97..0000000
--- a/vm/helper.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-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
diff --git a/vm/start.sh b/vm/start.sh
deleted file mode 100755
index 4023c85..0000000
--- a/vm/start.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/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}
diff --git a/vm/tftp.sh b/vm/tftp.sh
deleted file mode 100755
index c43358d..0000000
--- a/vm/tftp.sh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/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