solution: src/NETSetup.sln project: NETSetup testProject: NETSetup.Tests language: csharp testCommand: dotnet test src/NETSetup.sln --nologo --verbosity minimal ---
NETSetup
Version: 0.9.0.268
Branch: develop
Release Date: 04/16/2026 19:32:17
Install (Windows) — Chocolatey
Primary install path on Windows. Use this unless you have a reason not to.
choco source add --name=netsetup --source=http://netsetupprod.osisa.com:8080/chocolatey
choco install netsetup -y
Upgrade an existing installation:
choco upgrade netsetup -y
Direct download (advanced / fallback)
For environments without Chocolatey or for building an install ISO locally:
NETSetup install ISO is no longer published as a release asset. To build it: download NETSetup.exe above, run NETSetup.exe create-iso (see Usage below). Flash resulting NETSetup.iso to USB.
NEWS
2026-04-15: Proxmox host self-update via GitHub Releases
netsetup update on a Proxmox host now upgrades itself end-to-end without leaving
the binary stale. The flow:
-
First-boot is USB-only.
netsetup-first-boot.shcopies the linux-x64 binary from the SYSTEM partition (labelSYSTEMon USB #1) andcp -ppreserves its mtime. No Dropbox / Pages fallback. Noself-update.sh. NoExecStartPre— the systemd unit simply runs/NETSetup/NETSetup update. -
Self-update via GitHub Releases.
UpdateMethods.EnsureLatestBinaryOrReexecruns as the first step of everynetsetup updatecycle. It decrypts an embedded fine-grained PAT (Contents:Read onosisa/netsetuponly, AES-256-CBC viaosisa.Security.Contracts.EncryptionHelpersostrings(1)only sees a base64 blob), GETs/repos/osisa/netsetup/releases/latest, parses the response withParseReleaseInfo, and compares versions withCompareSemVer. When the remote SemVer is strictly greater, it downloads the asset by id, swaps the binary atomically (mv -f),chmod 755, spawns the new binary asNETSetup update, waits, and `Environment.Exit`s with the child’s exit code. -
SemVer comparator with build-counter precedence. Same
major.minor.patch? The trailing dot-segment of the pre-release tail (the GitVersion build counter, e.g.249in0.9.0-proxmoxvms.249) is compared numerically — channel rename in CI does not invert the ordering. Per SemVer 2.0 a missing pre-release still outranks any present one (1.0.0 > 1.0.0-x). -
Post-install ran step-by-step in C#.
EnsureProxmoxPostInstallno longer writes a giant bash script. Each side-effect (disable enterprise repos, writepve-no-subscription.sources, install nag-removal hook, configure banners) is a discrete NETCommand orIFileOperatorcall so failures stay scoped. -
Customer VM provisioning (e.g. Nextcloud).
EnsureCustomerVms.Runis wired intoRunProxmoxUpdateafterapt full-upgrade. Machines whose serial format is<VMID>@<HostSerial>and whose OS is NixOS-backed (or Win10/11/Server) get provisioned on the local Proxmox host. TheOfflineCompanytest fixture has a Nextcloud server hosted on the Liberator (100@lib); testProxmoxWithUSBStickTests.Liberatorend-to-end confirms post-install ran, the customer VM was created, and Nextcloud’s HTTPS endpoint serves on its configured IP. -
GitHub Actions publishes the linux-x64 binary as a release asset on every push to
main/develop(gh release upload v$SemVer NETSetup --clobber), so the self-update path always has a fresh build to pull.
2025-07-03
Updated the Flow to include the future Flow for Liberators.
Purpose
NETSetup allows users to automatically (or automagically) setup and configure any number of machines (servers, client computers, laptops) in a given organisation from OS installation, to server configuration, to domain joining of client computers, to software installation, all according to a given NETSetup configuration file.
In other words: NETSetup translates an enterprise structure to a network configuration and sets up the network accordingly.
In a future revision a new machine will receive its NETSetup automatically during its OOBE experience ("Just plug it in!"™).
Step by Step Process
@startuml
'Actors
actor Customer
actor Agent as "Agent (public)"
actor Ticketing as "Ticketing (public)"
actor Accounting as "Accounting (private)"
actor NETSetup as "NETSetup (private)"
actor Supplier as "Supplier (Alltron)"
actor Provider as "Provider (Dropbox)"
actor Computer
actor SWVendor as "Software Vendor (Microsoft)"
actor HWVendor as "Hardware Vendor (HP)"
'Contact
Customer -> Agent: [[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/NETSetup/050%20Kontaktaufnahme/Kontaktaufnahme%20V1.1.adoc callAgent() -> new order]]
Agent -> Ticketing: [[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/NETSetup/050%20Kontaktaufnahme/Ticketverwaltung%20in%20Freshdesk.adoc createNETSetupTicket(order) -> new ticket]]
Ticketing -> Customer: notifyCustomer(ticket)
'Delta
Agent -> Customer: [[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/NETSetup/100%20Offerte/Offertprozess.adoc#ist-soll-analyse requestCurrentMode(ticket)]] -> ensureConfig(ticket.customer.currentMode)[[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/Templates/TemplateCompany.adoc Template Company]] [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup.Tests/TestInfrastructure/OfflineCompany.cs Example Code]]
Customer -> Agent: responseCurrentMode(ticket) -> updateConfig(updatedCurrentMode)
Agent -> Customer: requestFutureMode(updatedCurrentMode.config)
Customer -> Agent: responseFutureMode(updatedCurrentMode.config) -> futureMode.config
'Quote
Agent -> Accounting: [[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/NETSetup/100%20Offerte/Offertprozess.adoc#offerterstellung createQuote(updatedCurrentMode.config, futureMode.config) -> new quote]]
Accounting -> Agent: returnQuote(quote)
Agent -> Customer: sendQuote(quote)
Customer -> Agent: responseQuote(quote)
'Config
Agent -> Supplier: [[https://github.com/osisa/NETSetup/blob/feature/proxmoxvms/docs/NETSetup/230%20Beschaffung/Beschaffung.adoc createOrder(quote) -> new order]]
Supplier -> Agent: return order.Success
Agent -> NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup.Tests/Customers/TranslateCompanyToConfig.cs createConfigs(ticket, futureMode.config)]] -> new config[]
note over Agent, NETSetup: someConfig[] = { {HASTAG}ticket.json, ... }
NETSetup -> Provider: [[https://www.dropbox.com/home/NETSetup/BOOT/Config save config() ]]
Provider -> NETSetup: return save.Success
NETSetup -> Agent: return save.Success
newpage Old Prerequisite Steps
Supplier -> Agent: return order.HardwareHashes
Agent -> NETSetup: TODO: updateConfigs(ticket, hardwareHashes)
NETSetup -> Provider: TODO: updateConfigs(ticket, hardwareHashes)
Provider -> NETSetup: return update.Success
NETSetup -> Agent: return update.Success
Supplier -> Customer: [[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/300%20Auslieferung/Auslieferung.adoc deliverOrder(setup)]]
Agent -> NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/input/NETSetupInstallUSB.adoc#NS-INSTALL-USB-CREATE-STICK Create NETSetup USB Stick]]
NETSetup -> Agent: NETSetup ready USB Stick
'Installation
loop For every computer
Agent -> Computer: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/input/NETSetupInstall.adoc#NS-INSTALL-USB-USE-STICK plugIn(Power, USB Stick, Display, Keyboard)]]
Agent -> Computer: turnOn()
Agent -> Computer: hold(F9 or other key to get into Boot Menu)
Agent -> Computer: select(USB Stick)
newpage NETSetup Setup Process - Boot Phase 1: USB Boot into WinPE
== PHASE 1: UEFI Firmware + WinPE (runs from USB stick) ==
note over Computer: UEFI firmware is running.\n**Proxmox installs require TWO USB sticks PRE-FLASHED WITH RUFUS (each >= 16 GB):**\n USB #1 (NETSetup/WinPE, Rufus: Partitionsschema GPT, Zielsystem UEFI, NTFS):\n Partition 1: FAT32 ESP — Windows Boot Manager + WinPE boot files\n Partition 2: NTFS (label=DATA) — WinPE boot.wim, NETSetup\n USB #2 (Proxmox installer, Rufus: DD image mode from proxmox.iso):\n Unmodified Proxmox hybrid ISO layout (ESP + iso9660).\n NETSetup does NOT touch USB #2 — it is booted as-is by the agent\n after WinPE finishes writing the answer.toml to USB #1.\n**Agent boots USB #1 first; after WinPE prep + reboot, agent boots USB #2.**
Computer -> Computer: UEFI firmware loads Windows Boot Manager from USB ESP (Secure Boot OK)
alt Can boot NTFS Partition
Computer -> Computer: Boots WinPE from NTFS Partition on USB
else Lacks NTFS Driver
Computer -> Computer: Boots FAT32 NTFS Driver from USB
Computer -> Computer: Boots WinPE from NTFS Partition on USB
end
note over Computer: WinPE is now running from USB RAM disk.\nProcess: startnet.cmd -> autoexecute.ps1 -> NETSetup.exe
Computer -> Computer: WinPE runs startnet.cmd
Computer -> Computer: startnet.cmd searches every drive for autoexecute.ps1
Computer -> Computer: cd to drive and run autoexecute.ps1
Computer -> NETSetup: autoexecute.ps1 runs [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/CLI/Commands/InstallCommand.cs NETSetup.exe install]]
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/CLI/Commands/InstallCommand.cs InstallCommand.cs]]\nhost = NETSetup.GetSystemHost()\nhost.OperatingSystem, host.ExitIfNotElevated()\nconfig = ConfigMethods.GetConfig(host, services, ticket, hostname)\nBranches on host.IsOnWinPE() -> [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/MainWinPE.cs MainWinPE.InstallOperatingSystem(cli, config, host)]]
NETSetup -> Computer: host = GetSystemHost() — ISystemHost with Hardware, Logger, OperatingSystem
NETSetup -> Computer: logs NETSetup Version, host.OperatingSystem
NETSetup -> Computer: host.ExitIfNotElevated()
NETSetup -> Computer: config = ConfigMethods.GetConfig(host, services, ticket, hostname)
opt ConfigMethods.GetConfig()
NETSetup -> Computer: host.Hardware.SerialNumber
NETSetup -> Computer: online = CheckOnline(timeout 10s)
alt online
NETSetup -> Computer: check for online map file
note over Agent, NETSetup: foreach machine in config a map file. Nomenclature: SerialNumber#Ticket.json
NETSetup -> Computer: download map file
NETSetup -> Computer: check for single matching map file
end
NETSetup -> Computer: if no map file found online -> check local map files as fallback (covers USB stick with embedded map file)
NETSetup -> Computer: if no disks or not online -> try to install drivers
NETSetup -> Computer: if nothing is found, but there are configs locally -> UI select which
end
NETSetup -> Computer: computer = config.IdentifyHost(host).CastTo<Computer>()
alt operatingSystem is Windows (target)
newpage NETSetup Setup Process - Windows Install Path
== PHASE 1 continued: WinPE prepares Windows installation (DISM /Apply-Image) ==
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/MainWinPE.cs MainWinPE]] Windows branch\nsetup.exe replaced with DISM /Apply-Image (setup.exe fails on Win11 25H2).
NETSetup -> Computer: unattended = [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/S4WindowsUnattended.cs GetDismUnattendedFileSubPath(host, config, computer)]]
note over NETSetup: **Image resolution BEFORE wiping install disk:**\nbootRoot = Path.GetFullPath("/") — boot medium root (startnet.cmd cd'd here).\nif <bootRoot>\\NETSetup\\Images\\<OS>\\ exists → prebuilt offline ISO path, no download.\nelse if !isOnline → ExitFailure.\nelse → mark imagesNetsetup = W:\\NETSetup (download after partitioning).\nThe broken pre-clear copy to Z:\\ was removed — Z: lives on the install disk and\ngets wiped by Clear() a few lines below, so anything staged there would be lost.
NETSetup -> Computer: installationDisk.Clear(GPT) + AddPartitions(S: ESP 1000 MB, W: remaining)
opt image was NOT prebuilt on boot medium
NETSetup -> Computer: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/S2Images.cs S2Images.EnsureImageElseExit(services, host, computer, isOnline, W:\\NETSetup)]]\nDownloads <OS>.7z from Dropbox + sha256 sidecar, extracts to W:\\NETSetup\\Images\\<OS>\\.
end
NETSetup -> Computer: wimPath = imagesNetsetup\\Images\\<OS>\\sources\\install.wim (verified with FileExists)
NETSetup -> Computer: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/DismCommand.cs DismCommand.ApplyImage(wimPath, imageName, W:)]] — 60 min timeout
NETSetup -> Computer: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/BcdbootCommand.cs bcdboot W:\\Windows /s S: /f UEFI]] — bcdboot from applied image, not WinPE's
NETSetup -> Computer: Copy unattend.xml → W:\\Windows\\Panther\\unattend.xml
NETSetup -> Computer: Copy NETSetup folder → W:\\NETSetup (excludes Images, Logs)
NETSetup -> Computer: Reboot()
== PHASE 2: Windows Setup (runs from target disk) ==
note over Computer: Windows installer is running from target disk.\nNo USB needed anymore. WinPE is gone.
Computer -> Computer: UEFI boots Windows Setup from installationDisk
Computer -> Computer: Windows auto setup via unattend.xml
Computer -> Computer: Unattend adds scheduled task: run NETSetup on first login
Computer -> Computer: Reboot()
== PHASE 3: Windows + NETSetup (runs from target disk) ==
note over Computer: Full Windows is running.\nScheduled task launches NETSetup.exe install.\nInstallCommand detects host.IsOnWindows() && host.HasNETSetup()\n-> [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/3-NETSetupWindows/Stage3Windows.cs Stage3Windows.InstallNETSetupWindows()]]
Computer -> Computer: Boots Windows from installationDisk
Computer -> NETSetup: Scheduled Task runs NETSetup.exe install
NETSetup -> Computer: computer = config.IdentifyHost(host).CastTo<Computer>()
NETSetup -> Computer: CheckOnline(), EnsureOnlineIfNeededAskForWifi()
NETSetup -> Computer: PowerSettings: Disable screen timeout and sleep (powercfg)
NETSetup -> Computer: InstallChocolatey() (needed for HP updates)
NETSetup -> Computer: TryGetDrivers() from remote source + TryInstallDrivers()
opt computer is on Proxmox
NETSetup -> Computer: Install VirtIO drivers via pnputil + disable WU driver search
NETSetup -> Computer: Install QEMU Guest Agent
end
opt computer is HP (host.IsOnHP())
NETSetup -> Computer: HPUpdateHelper.UpdateHPMachine() — install HP Support Assistant via choco, run silent driver/firmware/BIOS update -> Reboot
end
alt computer is WindowsServer
NETSetup -> Computer: SetIPv4Configuration(staticIP)
NETSetup -> Computer: InstallADForest() -> Reboot
NETSetup -> Computer: ConfigureDNSServer()
NETSetup -> Computer: InstallDHCPServer() -> Reboot
NETSetup -> Computer: DisableDHCPServer()
NETSetup -> Computer: Enable Remote Desktop
NETSetup -> Computer: WinFirewall configuration
NETSetup -> Computer: Enable Wake-on-LAN
NETSetup -> Computer: Set Wallpaper
NETSetup -> Computer: net start netlogon
NETSetup -> Computer: dcdiag /test:dns + dcdiag
NETSetup -> Computer: Create AD Users + Groups
NETSetup -> Computer: Deactivate Default Administrator
NETSetup -> Computer: Finished DC Setup -> Reboot
NETSetup -> Computer: RunUpdate (choco upgrade, software install, Windows Update) -> Reboot
else computer is WindowsComputer (Client)
NETSetup -> Computer: Enable FullDomainDNSRegistration
NETSetup -> Computer: Set all networks to Private
NETSetup -> Computer: Enable Remote Desktop
NETSetup -> Computer: WinFirewall configuration
NETSetup -> Computer: Enable Wake-on-LAN
NETSetup -> Computer: DomainJoin(config.Domain) -> Reboot
NETSetup -> Computer: Set Wallpaper
NETSetup -> Computer: gpupdate /force
NETSetup -> Computer: RunUpdate (choco upgrade, software install, Windows Update) -> Reboot
end
else operatingSystem is Linux (target)
newpage NETSetup Setup Process - Linux/Proxmox: WinPE prepares USB
== PHASE 1 continued: WinPE prepares USB stick for Linux boot ==
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/2-WinPE/MainWinPE.cs MainWinPE]] Linux branch.\nStill running in WinPE from USB RAM disk.\n**Target disk is NOT touched. Everything stays on USB #1.**
alt operatingSystem is Proxmox
note over NETSetup: **New architecture (single-USB prep):**\nNETSetup does NOT touch USB #2 at all.\nUSB #2 is a plain Rufus-flashed Proxmox installer stick that the agent\nbrought with them. NETSetup only modifies USB #1 by adding a small FAT32\nSYSTEM partition that holds the generated answer.toml.\nproxmox-fetch-answer will scan *all* attached partitions (including USB #1)\nby label, so answer.toml living on USB #1 still reaches the installer running\nfrom USB #2.
NETSetup -> Computer: FindBootDiskNumber(host) — resolve USB #1 disk number from AppContext.BaseDirectory drive letter
NETSetup -> Computer: UsbLinuxPartitionManager.EnsureUsbDataPartition(usbDiskNumber, 100 MB, label=SYSTEM, fs=fat32, assignDriveLetter=true)
note over Computer: **On USB #1 (the booted NETSetup stick):**\n 1. Find largest partition (NTFS DATA partition 2)\n 2. Shrink it via diskpart (current - 100 MB)\n 3. Create new primary partition in freed space\n 4. Format FAT32 with label SYSTEM\n 5. Add-PartitionAccessPath -AssignDriveLetter -> first free letter\nIdempotent: if SYSTEM already exists, re-uses it and just re-assigns a letter.
NETSetup -> Computer: netSetupConfig.ToProxmoxAutoInstall(host, liberator, setStaticNetworkConfiguration: true).WriteTomlFile({letter}:\answer.toml)
note over Computer: answer.toml lives at {assignedLetter}:\answer.toml on USB #1 SYSTEM (FAT32).\nPath + full TOML content are logged via LogDebugToFile("WinPE_Proxmox_AnswerToml").
NETSetup -> Agent: UserInteraction.AskRebootNow("You will need to boot from Proxmox USB Stick now...")
note over Agent: **Agent must now reboot and pick USB #2 (Proxmox) from the boot menu**\n(NETSetup cannot force this — USB #1 is still the default boot device).\nUSB #1 stays plugged in so proxmox-fetch-answer can find SYSTEM:/answer.toml.
else other Linux (NixOS, etc.)
note over Computer: VoidPE stays on USB NTFS (not overwritten).\nWinPE writes linux config to SYSTEM partition.
NETSetup -> Computer: Write linux config file to SYSTEM:\config
end
NETSetup -> Computer: ExitReboot()
note over Computer: **USB #1 layout after WinPE prep (Proxmox case):**\n Partition 1: FAT32 ESP (unchanged)\n Partition 2: NTFS DATA (shrunk by 100 MB)\n Partition 3: FAT32 SYSTEM — answer.toml\n**USB #2 is completely unchanged** (still a plain Rufus-flashed Proxmox ISO).\nTarget disk is completely untouched.
newpage NETSetup Setup Process - Boot Phase 2: Agent boots Proxmox USB
== PHASE 2: Agent reboots and selects USB #2 (Proxmox) from boot menu ==
Agent -> Computer: Power-cycle; hold F9 (or equivalent) to open UEFI boot menu
Agent -> Computer: Select USB #2 "Proxmox" entry (NOT USB #1)
note over Computer: **USB #1 stays plugged in** — proxmox-fetch-answer will scan all attached\npartitions (by label) and find SYSTEM:answer.toml on USB #1.\nSecure Boot must be OFF (Proxmox GRUBX64.EFI is not signed for SB).\nThe agent already disabled SB in BIOS before deployment.
Computer -> Computer: UEFI loads Proxmox shim BOOTX64.EFI from USB #2 ESP (unmodified Rufus flash)
Computer -> Computer: Proxmox shim loads GRUBX64.EFI, which loads the stock Proxmox GRUB.CFG from USB #2
alt operatingSystem is Proxmox
Computer -> Computer: Stock Proxmox GRUB menu appears — agent selects "Install Proxmox VE (Automated)"
note over Computer: **No custom GRUB.CFG** — USB #2 is a plain Rufus-flashed Proxmox ISO.\nAgent picks the stock "Automated" entry manually, which passes\nproxmox-start-auto-installer to the kernel.
Computer -> Computer: GRUB loads Proxmox kernel+initrd from USB #2 iso9660 partition
newpage NETSetup Setup Process - Phase 2 continued: Proxmox auto-installer
== PHASE 2 continued: Proxmox auto-installer ==
Computer -> Computer: Kernel boots, normal Proxmox /init runs
Computer -> Computer: /init mounts squashfs (pve-base.squashfs, pve-installer.squashfs)
Computer -> Computer: Creates overlayfs, switch_root -> /sbin/unconfigured.sh
Computer -> Computer: proxmox-start-auto-installer triggers proxmox-auto-installer
Computer -> Computer: proxmox-fetch-answer scans all attached partitions for answer.toml
note over Computer: proxmox-fetch-answer uses blkid to scan every attached partition and reads\nthe first one containing answer.toml.\nFinds **USB #1 partition 3 (SYSTEM, FAT32, written by WinPE)**.\nUSB #1 must still be plugged in at boot time — agent keeps both sticks attached\nuntil the installer has read the answer.
Computer -> Computer: Auto-installer partitions TARGET DISK, installs Proxmox
note over Computer: Target disk touched for the FIRST TIME here.\nAuto-installer: sgdisk, mkfs, LVM, debootstrap.\nConfigures network, hostname, root password from answer.toml.\nBoth USB sticks stay completely intact.
Computer -> Computer: Reboot into installed Proxmox
else other Linux (VoidPE path)
Computer -> Computer: GRUB selects "NETSetup VoidPE" entry
Computer -> Computer: GRUB loads VoidPE kernel+initrd from LINUX partition
note over Computer: VoidPE boots from USB LINUX partition.\ndracut live module finds squashfs on LABEL=LINUX.\nOverlayfs rootfs. /etc/rc.local runs NETSetup.
Computer -> NETSetup: NETSetup.exe install (Stage4LinuxPE)
NETSetup -> Computer: Partition target drive, format, unpack image
NETSetup -> Computer: Update boot entry (GRUB/EFI), reboot into installed Linux
end
newpage NETSetup Setup Process - Phase 3: Proxmox Host Creates VMs
== PHASE 3: Proxmox Host + NETSetup (runs ON installed Proxmox) ==
note over Computer: Proxmox VE is now installed and running.\n[[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Resources/netsetup-first-boot.sh netsetup-first-boot.sh]] (USB-only install, no network fallback):\n 0. **Log-exists gate** — if /NETSetup/first-boot.log exists → exit (idempotent)\n 1. Restore Windows Boot Manager on USB ESP (bootmgfw.efi.bak → BOOTX64.EFI)\n 2. Mount SYSTEM partition (label=SYSTEM, FAT32 on USB #1)\n 3. cp -p NETSetup binary from /mnt/system/NETSetup/NETSetup → /NETSetup/NETSetup (preserves mtime)\n 4. cp -r config files from /mnt/system/NETSetup/Config → /NETSetup/Config\n 5. Install systemd service + timer (OnBootSec=2min, OnUnitActiveSec=6h, Persistent=true)\n 6. systemctl enable netsetup.timer (**NOT --now** — fires only after reboot)\n 7. **systemctl reboot** (scheduled +5s so first-boot unit exits cleanly)\n**No inline `netsetup update`** — network flaky during first-boot (DNS/403 on GitHub).\nBinary handles self-update via GitHub Releases API on every update cycle (see 3a).
Computer -> Computer: First-boot service runs (from Proxmox ISO, ordering=fully-up)
Computer -> Computer: Restore USB ESP boot manager (so USB is reusable)
Computer -> Computer: Mount SYSTEM partition → cp NETSetup binary + Config locally
Computer -> Computer: systemctl enable netsetup.timer (timer armed, NOT fired)
Computer -> Computer: **Reboot** — clean boot, full network, then timer fires
note over Computer: **After reboot**, systemd timer fires 2 min in (Persistent=true catches missed runs).\nOn **every subsequent boot** the timer also re-fires `netsetup update`.
Computer -> NETSetup: systemd timer → ExecStart=/NETSetup/NETSetup update
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/CLI/Commands/UpdateCommands.cs UpdateCommands.cs]] → [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Helpers/UpdateMethods.cs UpdateMethods.RunUpdate]]\nDetermineUpdateStage(host) → host.IsProxmoxHost() → RunProxmoxUpdate.\nEvery step is a discrete NETCommand call so individual failures stay scoped\nand surface in the C# log (no monolithic shell scripts).
== PHASE 3a: Self-Update via GitHub Releases ==
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Helpers/UpdateMethods.cs EnsureLatestBinaryOrReexec]] — runs FIRST, before anything else:\n 1. Decrypt embedded GitHub PAT via osisa.Security.Contracts.EncryptionHelper\n (AES-256-CBC, PBKDF2-SHA1, baked-in ciphertext invisible to strings(1))\n 2. curl GET /repos/osisa/netsetup/releases/latest with Bearer token\n 3. ParseReleaseInfo → (version, assetId)\n 4. CompareSemVer(local, remote) — same major.minor.patch ⇒ trailing build counter wins\n 5. Remote newer → curl /releases/assets/{id} (Accept: application/octet-stream),\n mv -f → /NETSetup/NETSetup, chmod 755, spawn child `NETSetup update`,\n Environment.Exit(child.IsSuccess ? 0 : 1) — re-execs into the new binary
NETSetup -> Computer: Decrypt embedded PAT (Contents:Read on osisa/netsetup only)
NETSetup -> Computer: curl /releases/latest → ParseReleaseInfo
alt remoteSemVer > localSemVer
NETSetup -> Computer: Download asset by id → mv -f → chmod 755
NETSetup -> NETSetup: Spawn `/NETSetup/NETSetup update`, wait, Environment.Exit
note over NETSetup: Re-execed into the new binary; rest of this cycle runs there.
else equal or older
NETSetup -> Computer: Continue with current binary
end
== PHASE 3b: EnsureProxmoxPostInstall (once-only, LogFileExists-gated) ==
note over NETSetup: **Log-file gate** mirrors Windows Stage3 pattern:\n `if (!host.Logger.LogFileExists("ProxmoxPostInstall"))` → run + LogInformationToFile → **reboot + Environment.Exit(0)**\n → next boot the log exists → skip → continue with 3c.\nEach helper = one discrete NETCommand or IFileOperator call:\n - DisableEnterpriseListRepos (PVE 8 .list — sed in-place)\n - DisableEnterpriseDeb822Repos (PVE 9 .sources — fileop read+write)\n - EnsureNoSubscriptionRepo (pveversion → write .list or .sources)\n - InstallNagRemovalHook (write hook + apt post-invoke + chmod + run once)\n - ConfigureBanners (clear /etc/motd + systemctl disable pvebanner + write /etc/issue with detected IP)
alt /NETSetup/Logs/ProxmoxPostInstall.log missing (FIRST run)
NETSetup -> Computer: EnsureProxmoxPostInstall (repos, nag, banners)
NETSetup -> Computer: LogInformationToFile("ProxmoxPostInstall", ...) — writes gate marker
NETSetup -> Computer: systemctl reboot + Environment.Exit(0) — rest of cycle skipped
note over Computer: **Next boot** the timer re-fires `netsetup update`.\nlog exists → post-install skipped → continues with 3c-3e below.
else log exists (subsequent runs)
NETSetup -> Computer: log "skipping post-install, continuing update" — falls through to 3c
end
== PHASE 3c: apt update + full-upgrade ==
NETSetup -> Computer: apt update (only succeeds AFTER enterprise repos disabled in 3b)
NETSetup -> Computer: env DEBIAN_FRONTEND=noninteractive apt full-upgrade -y
== PHASE 3d: EnsureCustomerVms ==
note over NETSetup: [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Helpers/EnsureCustomerVms.cs EnsureCustomerVms.Run]] orchestrates VM provisioning per host:\n 1. orderedVms = [[https://github.com/osisa/NETSetup/tree/feature/proxmoxvms/src/NETSetup/Stages/5-NETSetupLinux/CreateVirtualMachines.cs GetOrderedVmsForHost]](config, host.SerialNumber)\n — IsForHost matches serial format `<VMID>@<HostSerial>`\n — VirtualHyperVTestBase.EnsureVirtualMachineSerialNumber rewires every hosted VM's\n suffix to the real BIOS serial of the test host before publishing\n 2. Classify each machine: Nix (OS.NixOS / OS.Nix*) | Windows (Win10/11/Server) | Skip\n 3. If any Nix → EnsureNixosIsoOnProxmoxHost (sha256-driven 7z sync from Dropbox)\n 4. Per-VM dispatch with fault isolation:\n - Nix → ProvisionNixosVm.Run (NixHostTemplate via NixTemplateFactory)\n - Windows → ProvisionWindowsVmOnProxmox.Run (offline production ISO + answer.toml)
NETSetup -> Computer: EnsureCustomerVms.Run(host, config, services)
loop For each customer VM (DC first via WindowsServer ordering)
alt Nix-backed (e.g. Nextcloud → NixGenericTemplate, NixNextcloud → NixNextcloudTemplate)
NETSetup -> Computer: ProvisionNixosVm.Run — emits flake.nix + hardware.nix + default.nix, qm create, install.sh over SSH
else Windows
NETSetup -> Computer: ProvisionWindowsVmOnProxmox.Run — MainISO.CreateOfflineProductionISO, qm create, attach
else Skip
NETSetup -> Computer: log "OS not classified for provisioning" — continue
end
end
== PHASE 3e: btrfs scrub ==
NETSetup -> Computer: For each mounted btrfs filesystem → btrfs scrub start -B
note over Computer: systemd timer re-fires netsetup.service every 6h **and on every boot** (OnBootSec=2min).\nPhases 3c/3d/3e run every cycle (apt, VMs, scrub are inherently idempotent).\nPhase 3b runs ONCE (log-gated) and reboots on first run.\nPhase 3a swaps binary only when remote SemVer is strictly greater, logs outcome either way.
end
end
newpage Approval and Billing
Agent -> Customer: [[https://github.com/osisa/NETSetup/tree/develop/docs/NETSetup/400%20Abnahme config.CreateApprovalForm())]]
Customer -> Agent: fillOut(config.approvalForm)
Agent -> Accounting: [[https://github.com/osisa/NETSetup/blob/develop/docs/Infobl%C3%A4tter/Software/Sage/Sage%20Start/Softwareinfoblatt%20Sage%20Start.adoc commissionBill(config)]]
Accounting -> Agent: returnBill(config.bill)
Agent -> Customer: sendBill(config.bill)
Customer -> Accounting: payBill(config.bill)
Accounting -> Agent: notify(config.bill.paid)
Agent -> Ticketing: closeTicket()
@enduml
NETSetup: Install using USB Stick
0. Requirements for Installation using NETSetup USB Stick
Read the entire Installation Instructions once from start to end before doing anything.
Target machine’s serial number has to be associated to a specific configuration. To do this goto the NETSetup.Tests Project and copy a customer file (e.g. Schnell.cs and at the bottom fill in all the details.The tests are supposed to be run in order to ensure the functionality of the configuration.One (by default ignored) test will upload the config files to the remote source, e.g. Dropbox).The most important methods are the ServerIP Range and the CreateCompany! (ALTERNATIVE to 1. for single Computers) Take #123.json from Dropbox and set the SerialNumber inside the desired type of Computer and ensure the {SerialNumber}#123.map File both exist.Copy them after creating the USB Stick in the NETSetup/Config Folder.
-
Config has to be online of customer
-
Physical access to the machine, which has internet cable, power, display and a keyboard connected.
-
Press power button and hold the button labeled ESC until you see a menu
-
Choose BIOS Setup / Computer Setup with arrow keys and press Enter
-
Go through all Settings using the keys while looking for and setting the following settings (if you dont find the name, skip). In the end goto File > Save Changes and Exit and confirm yes and shutdown Machine with power button after reboot:
-
Secure Boot Configuration > Legacy Support / CSM / MBR Boot Disabled
-
USB / Removable Media Boot enabled
-
Secure Boot Configuration > Fast Boot Disabled
-
For Liberator VTx (Virtualization) Enabled
-
For Liberator Secure Boot Configuration > Secure Boot Disabled
-
For Liberator Boot Order > Push all entries with "USB" to the bottom
-
-
A USB Stick of at least 16 GB (This USB Stick will be formatted and all data on it will be lost).
-
For Proxmox installs you also need a SECOND USB Stick of at least 16 GB flashed with the Proxmox ISO using Rufus (same procedure as below, but select the Proxmox ISO instead of NETSetup.iso and use Rufus’s default "ISO Image mode" when prompted). Both sticks must be plugged into the target machine during installation — see Using the Install USB Stick (Proxmox).
-
Goto your existing Personal Computer and login with an Administrator
-
Precondition: Chocolatey
-
Press Win and X and open Terminal (Administrator) and confirm
-
write the following into the box and press Enter:
choco install rufus -y
-
Download
NETSetup.exefrom the release page (optional: verify hash viaGet-FileHash NETSetup.exe— compare to value below).
D1B899C2517BDF5D96716783CF9BFE89662A11763B4F0AF1A5E22B371C516530
-
Open an elevated PowerShell in the folder where
NETSetup.exesits and build the ISO:
.\NETSetup.exe create-iso
The ISO is written to C:\NETSetup\Iso\NETSetup.iso. Use that file in Rufus in the next section.
1. Making the USB Stick
-
Plug in your USB Stick to your computer
-
Press Win and R and type "rufus" and press enter and confirm
-
Select your USB Stick from the list at the top "Laufwerk" (Drive):
-
Click the "AUSWAHL" (SELECT) button and select the "NETSetup.iso" file from your Downloads folder.
-
Make sure that the following Items are set as expected:
-
"Partitionsschema" is set to "GPT"
-
"Zielsystem" is set to "UEFI (ohne CSM)"
-
"Dateisystem" is set to "NTFS"
-
-
Click the "START" button.
-
Rufus might show you a warning. Click "OK" to confirm that all existing data on the stick will be deleted.
-
Wait for the green progress bar at the bottom of the program to fill. This is a good time to get a cup of coffee.
-
Rufus will show the text "FERTIG" (FINISHED) in the filled progress bar once it is done. You can unplug and use the NETSetup USB Stick now, and you can close rufus.
2. Using the Install USB Stick
-
Plug the USB Stick in the target machine on the back side (directly into the motherboard and not a front header USB).
-
Power on the machine and hold down the F9 key (or a different one depending on your machine to enter the boot menu, refer to your manufacturers manual) until you enter the boot menu.
-
Use the arrow keys to select USB. If you have multiple USB shows up, pick the topmost occurence and press enter
-
If it asks you to select something, type the answer using the keyboard and press enter.
-
Wait for the installation to finish, it reboots several times
For Liberator (or other linux based system) enter the Boot Menu 2 times and boot the disk and not the USB Stick
Note: If you’re setting up a new Windows Server Domain/AD, wait for the server to finish all its tasks before installing the clients. ~30min should be enough for reasonably up-to-date hardware. Connect the new machines to the LAN and boot them the same way with the USB Stick from before or a different one that has been made the same way
Workaround for Liberator Use this to setup machines on Liberator
2a. Using the Install USB Stick — Proxmox (TWO-STICK PROCEDURE)
Proxmox installations require TWO USB sticks because NETSetup and Proxmox each need their own bootable media. NETSetup runs in WinPE from stick #1, prepares an answer file on a small FAT32 partition it adds to stick #1, and then tells you to reboot into stick #2. The stock Proxmox installer on stick #2 boots up, reads the answer file off stick #1 (by volume label), and installs Proxmox automatically.
Both sticks are created in advance with Rufus — NETSetup does not write anything to stick #2.
-
Prepare Stick #1 (NETSetup) following section 1 above with
NETSetup.iso. -
Prepare Stick #2 (Proxmox) the same way:
-
Plug in the second USB stick
-
Run Rufus
-
Select the second stick as the drive
-
Click "AUSWAHL" (SELECT) and pick your
proxmox-ve_*.iso -
Accept Rufus’s defaults (it will prompt to write in "ISO Image mode" — accept)
-
Click "START" and confirm data loss
-
Wait for "FERTIG"
-
-
Plug BOTH sticks into the target machine. Both must stay plugged in for the entire installation.
-
Power on and hold F9 (or the manufacturer’s boot-menu key) to open the UEFI boot menu.
-
Select Stick #1 (NETSetup) — NOT the Proxmox one yet.
-
NETSetup/WinPE runs, detects that the target OS is Proxmox, shrinks stick #1’s NTFS data partition by ~100 MB, creates a new FAT32 partition labeled
SYSTEM, writes the generatedanswer.tomlto it, and then prompts:You will need to boot from Proxmox USB Stick now... (Y/N)Press Y to reboot. Both sticks must still be plugged in.
-
When the machine reboots, hold F9 again to open the boot menu a second time.
-
This time select Stick #2 (Proxmox) — NOT NETSetup.
-
The stock Proxmox GRUB menu appears. Pick the "Install Proxmox VE (Automated)" entry.
-
Proxmox boots,
proxmox-fetch-answerscans all attached partitions, findsanswer.tomlon stick #1’sSYSTEMpartition, and runs the unattended installer. Both sticks stay plugged in until the installer finishes reading the answer. -
After Proxmox is installed and has rebooted into the installed system, you can unplug both USB sticks.
Troubleshooting Proxmox 2-stick flow:
-
"proxmox-fetch-answer: no answer file found" → You pulled stick #1 too early. Keep both sticks plugged in until the installer has loaded past the partition scan.
-
Stick #2 doesn’t appear in the boot menu → Secure Boot is still on; disable it (see [BIOS-SETTINGS]). Proxmox’s shim is fine but the GRUB payload inside the hybrid ISO is not SB-signed for every firmware.
-
Machine boots stick #1 again after the reboot → You didn’t hold F9. There’s no way for NETSetup to force UEFI to change boot order mid-install; you must pick stick #2 manually.
3. USB Stick Troubleshooting
-
I cant boot the USB Stick. Check that UEFI Boot is enabled and NOT CSM/MBR BOOT!!!
-
I get an error while creating the USB stick:
=> Ensure that the USB stick is working and that it has enough space.
-
I cannot enter the boot menu:
=> <<BIOS-SETTINGS,Disable "Fast Boot">> in the BIOS. Consult Google if you are unsure on how to do this.
-
The NETSetup fails unexpectedly:
=> Ensure that you have the correct version of NETSetup.iso file (check the SHA-256 hash).
=> Make sure you follow the directions in <<Using the USB Stick>> correctly and ensure that the target machine's serial number is correctly registered in the config.
-
Windows Server installation fails:
=> Windows Server installation REQUIRES to be connected to an ethernet environment (a router is enough, it doesn't have to be a complete internet-activated environment with multiple machines). If there is no response on the ethernet port while installing, Windows Server will not install and setup ethernet and it will fail.
Executable (Advanced Functions)
SHA-256 Hash of NETSetup.exe:
D1B899C2517BDF5D96716783CF9BFE89662A11763B4F0AF1A5E22B371C516530
Usage
NETSetup.exe can install and configure aspects of a given machine. During a regular NETSetup installation it need not be downloaded separately. The executable is provided here for advanced users.
Use NETSetup.exe [command] --help to get help for a specific command.
Installation & Configuration
install[--ticket <ticket>] [--hostname <hostname>]-
Installs and configures the machine according to its NETSetup configuration. Looks up the config by the machine’s serial number. On WinPE/LinuxPE it runs the full OS installation pipeline. On an installed OS it configures software, domain, etc. Optional
--ticketand--hostnameparameters override automatic config detection. update-
Updates an installed machine’s software. Runs Chocolatey upgrade-all, ensures all config-specified packages with exact versions are installed, and refreshes Group Policy if domain-joined. Requires elevation.
create-iso[--offline] [--output <path>] [--ticket <ticket>] [--hostname <hostname>] [--images <path>] [--drivers <path>]-
Creates a NETSetup install ISO with the config baked in. Use
--offlineto include OS images and drivers for offline installation. On Proxmox hosts, copies the ISO to the queried ISO storage. create-binaries[--output <path>]-
Publishes NETSetup binaries (win-x64
NETSetup.exe, linux-x64NETSetup, linux-musl-x64NETSetup.musl) intooutput/artifacts/and writes SHA256 adoc files. No ISO. Used by the deployment workflow.
File Synchronization
ensure-file--file <file> --target <directory>-
Ensures that the file exists in the target directory. If not present, it will be downloaded from the remote source. The
--filepath is relative to the remote NETSetup boot directory. ensure-folder--source <folder> --target <directory>-
Ensures that the folder with all its contents exists in the target directory. If not present, it will be downloaded from the remote source.
Network
set-ipv4--ip <ip> --netmask <netmask> --gateway <gateway> --dns <dns>-
Sets the machine’s IPv4 address, netmask, gateway, and DNS.
connect-wifi[--ssid <ssid>] [--password <password>]-
Connects to a WiFi network with the given SSID and password.
check-online-
Checks whether the machine has internet connectivity.
check-website-online--url <url> [--timeout-minutes <minutes>]-
Checks whether a specific URL is reachable. Optional timeout in minutes.
enable-dhcp-server-
Enables the DHCP server on this machine.
disable-dhcp-server-
Disables the DHCP server on this machine.
Hardware Inventory
scan-hardware[--output <path>]-
Probe local machine via CIM, dump JSON. Output defaults to
{LogsRoot}/hwscan-<timestamp>.json.LogsRoot=appsettings.jsonLoggingOptions.LogsPathOverrideelse/NETSetup/Logs. If--outputis a directory, default filename appended; if a file, used as-is. scan-network[--hosts <csv>] [--range <cidr|a-b>] [--user <u>] [--password <p>] [--output <path>] [--no-self]-
Probe current machine + remote Windows hosts via PowerShell
Invoke-Command.--hosts= comma list of IPs/hostnames.--range= CIDR (10.0.0.0/24) or inclusive range (10.0.0.10-10.0.0.50); both flags may be combined. Credentials optional; omit for current Kerberos creds.--no-selfskips the scanning machine. Unreachable hosts logged as failures but do not abort; result JSON includes per-host status. Output path same rules asscan-hardware.
Wake-on-LAN
wake-machine--mac-address <mac> [--broadcast <ip>] [--port <port>]-
Sends a Wake-on-LAN magic packet to wake a single machine by its MAC address.
wake-machines--file-path <path> [--broadcast <ip>] [--port <port>]-
Sends Wake-on-LAN magic packets to multiple machines listed in a file.
enable-wol--interface-name <name>-
Enables Wake-on-LAN on the specified network interface.
query-wol-status--interface-name <name>-
Queries the Wake-on-LAN status of the specified network interface.
Hashing & Verification
get-sha2-256--file <file>-
Gets the SHA-256 hash of a file.
get-sha2-384--file <file>-
Gets the SHA-384 hash of a file.
get-sha2-512--file <file>-
Gets the SHA-512 hash of a file.
get-md5--file <file>-
Gets the MD5 hash of a file.
get-crc32-le--file <file>-
Gets the CRC-32 hash (little-endian) of a file.
get-crc32-be--file <file>-
Gets the CRC-32 hash (big-endian) of a file.
get-xxh-32--file <file>-
Gets the xxHash-32 hash of a file.
get-xxh-64--file <file>-
Gets the xxHash-64 hash of a file.
get-xxh3-64--file <file>-
Gets the xxHash3-64 hash of a file.
get-xxh3-128--file <file>-
Gets the xxHash3-128 hash of a file.
check-sha2-256--file <file> --hash <checksum>-
Verifies the SHA-256 hash of a file matches the expected checksum.
check-sha2-512--file <file> --hash <checksum>-
Verifies the SHA-512 hash of a file matches the expected checksum.
check-xxh3-128--file <file> --hash <checksum>-
Verifies the xxHash3-128 hash of a file matches the expected checksum.
System Information
get-id-
Gets the machine’s BIOS serial number (SMBIOS).
get-serial-number-
Gets the machine’s serial number.
get-os-
Gets the detected operating system.
get-win-key-
Gets the Windows product key from the BIOS.
info-
Displays NETSetup version and build information.
Disk Management
list-disk-
Lists all disks on the machine.
list-usb-
Lists all USB drives on the machine.
select-disk[--index <index>]-
Selects a disk by index for installation.
Encryption
encrypt--text <text>-
Encrypts a text string using the NETSetup encryption key (for use in config files).
decrypt--text <text>-
Decrypts a NETSetup-encrypted text string.
Serving
serve-file--file <file> [--port <port>]-
Serves a single file over HTTP. Default port: 80.
serve-folder[--path <path>] [--port <port>]-
Serves a folder over HTTP. Default path: current directory. Default port: 80.
AI Assistant
assistant[--prompt <prompt>] [--model <model>]-
Connects to the Claude API for NETSetup-related questions. Without
--prompt, enters interactive REPL mode. With--prompt, sends a single question and exits. Uses the machine’s NETSetup config as context. Optional--modeloverrides the default model.
Debug / Development
example-log-output-
Outputs example log messages at various log levels (for testing log configuration).
example-log-output-to-files-
Outputs example log messages to log files (for testing file log configuration).
Examples
.\NETSetup.exe set-ipv4 --ip 192.168.1.123 --netmask 255.255.255.0 --gateway 192.168.1.1 --dns 8.8.8.8
.\NETSetup.exe get-sha2-256 --file c:\temp\document.docx
.\NETSetup.exe connect-wifi --ssid swisscom --password abcd-1234-efgh-5678-ijkl
.\NETSetup.exe install
.\NETSetup.exe update
.\NETSetup.exe create-iso --offline --ticket 12345 --hostname DC01
.\NETSetup.exe assistant --prompt "What software is configured for this machine?"
.\NETSetup.exe serve-folder --path C:\NETSetup --port 8080
.\NETSetup.exe wake-machine --mac-address 00:15:5D:AB:CD:EF --broadcast 192.168.1.255
Developer Info
When you push changes to the NETSetup repository, the corresponding Git Action tests, builds, and updates the executable and the ISO-Image automatically.
The entrypoint for the executable is CLI/NETSetup.cs/Main(). There it adds all the commands to the application.
The Install command is in CLI/Commands/InstallCommand.cs
You can create an offline ISO with "__main__.cs > [TestMethod] CreateOffline"
If Offline Install ensure Image, Drivers and Config are copied onto USB Stick. If singular computer ensure the Config and MapFile as described in Requirements
Linux PE Notes
Void Linux PE (UnixPE)
Void Linux musl serves as the "Linux Pre-Execution Environment" (UnixPE) in NETSetup. It boots from a squashfs image on the target disk’s ESP partition and runs NETSetup to install Linux-based operating systems (Proxmox, NixOS, etc.).
Architecture Overview
USB Stick (WinPE)
│
├─ NETSetup.exe install (Phase 1: WinPE)
│ ├─ Partitions target disk: GPT, 4GB FAT32 ESP (label=SYSTEM)
│ ├─ Copies linux/ folder → Z:\ (GRUB + Void squashfs)
│ ├─ Copies NETSetup binary → Z:\NETSetup\NETSetup.musl
│ ├─ Copies config + OS image (e.g. proxmox.iso) → Z:\NETSetup\
│ └─ Reboots (USB can be removed)
│
Target Disk ESP (label=SYSTEM)
│
├─ /EFI/BOOT/BOOTX64.EFI ← GRUB bootloader
├─ /boot/vmlinuz ← Void Linux kernel
├─ /boot/initrd ← Void Linux initramfs
├─ /boot/grub/grub.cfg → grub_void.cfg
├─ /LiveOS/squashfs.img ← Void Linux rootfs (compressed)
│ └─ LiveOS/ext3fs.img ← Void Linux rootfs (ext3, ~3GB)
│ └─ / (full Void musl system)
│ └─ /etc/rc.local ← entrypoint: searches for autoexecute.sh
├─ /NETSetup/NETSetup.musl ← NETSetup binary (linux-musl-x64)
├─ /NETSetup/Config/*.json ← customer config
└─ /NETSetup/Images/{OS}/ ← OS installer ISO
Boot Chain
-
UEFI firmware boots GRUB from ESP (
/EFI/BOOT/BOOTX64.EFI) -
GRUB loads
grub_void.cfg→ boots "UnixPE" menu entry -
Kernel + initrd boot, dracut live module:
-
Finds
squashfs.imgon partitionLABEL=SYSTEM -
Mounts squashfs (read-only) → loop-mounts
ext3fs.img -
Creates overlayfs (rw tmpfs on top of ro rootfs)
-
Pivots root to overlayfs
-
-
Void Linux init runs →
/etc/rc.localexecutes -
rc.localscans all partitions forautoexecute.sh -
Finds it on the SYSTEM partition → runs it
-
autoexecute.shlaunchesNETSetup.musl install -
NETSetup Stage 4 (LinuxPE) installs the target OS via GRUB chainload
Disk Layout on Target (after Phase 1)
/dev/sdX (or nvme0n1)
├─ Partition 1: FAT32 ESP, 4GB, label=SYSTEM
│ ├─ EFI/BOOT/BOOTX64.EFI
│ ├─ boot/ (vmlinuz, initrd, grub/)
│ ├─ LiveOS/squashfs.img
│ ├─ NETSetup/ (binary, config, images)
│ └─ answer.toml (for Proxmox auto-install)
└─ Partition 2: (created by Stage 4) raw ISO partition for OS installer
squashfs.img Structure
The squashfs contains a single file: LiveOS/ext3fs.img — a ~3GB ext3 filesystem with the full Void Linux musl rootfs.
Key files inside:
| Path | Purpose |
|---|---|
|
Boot entrypoint — scans drives for |
|
NTFS mount support (needed to read USB sticks) |
|
C++ standard library — must match .NET runtime requirements |
How to Modify the Void PE Image
Prerequisites
-
WSL (Ubuntu) with
squashfs-toolsinstalled:sudo apt install squashfs-tools -
The squashfs.img lives at:
src/NETSetup/linux/LiveOS/squashfs.img -
Helper scripts in the same folder:
open-void-pe.sh,close-void-pe.sh,edit-netsetup-call.sh
Open (unsquash + mount)
From WSL Ubuntu, cd to the LiveOS/ folder:
cd /mnt/c/_gh/main/netsetup/src/NETSetup/linux/LiveOS
# Step 1: unsquash (creates void/LiveOS/ext3fs.img)
sudo unsquashfs -f -d void squashfs.img
# Step 2: copy ext3fs.img to Linux FS (loop mount doesn't work on /mnt/c)
sudo mkdir -p /mnt/void
sudo cp void/LiveOS/ext3fs.img /tmp/ext3fs.img
sudo mount -o loop /tmp/ext3fs.img /mnt/void
|
Note
|
Loop-mounting on /mnt/c (Windows FS via 9p/DrvFs) does not work.
You MUST copy ext3fs.img to the native Linux filesystem (e.g. /tmp/) before mounting.
|
Modify files
# Edit rc.local
sudo nano /mnt/void/etc/rc.local
# Or browse the filesystem
ls /mnt/void/usr/lib/libstdc++*
Chroot (for package management)
|
Important
|
You CANNOT chroot from glibc Ubuntu into musl Void — the binaries will not run. Use static xbps instead: |
# Download static xbps (runs on any Linux x86_64)
cd /tmp
wget https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz
tar xf xbps-static-latest.x86_64-musl.tar.xz
# Upgrade all packages in the mounted rootfs
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Su
# Install a specific package
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -S <package-name>
# Query installed packages
sudo /tmp/usr/bin/xbps-query.static -r /mnt/void -l
Alternatively, if you have a real Void Linux musl system (VM, live USB, etc.), you can chroot /mnt/void directly and use normal xbps-install -Su.
Close (unmount + repack)
# Step 1: unmount
sudo umount /mnt/void
# Step 2: copy modified image back
sudo cp /tmp/ext3fs.img void/LiveOS/ext3fs.img
# Step 3: fsck
sudo e2fsck -p -f void/LiveOS/ext3fs.img
# Step 4: verify
dumpe2fs -h void/LiveOS/ext3fs.img | grep "Filesystem state"
# Step 5: repack squashfs (xz compression)
sudo rm squashfs.img
sudo mksquashfs void squashfs.img -comp xz -noappend
# Step 6: cleanup
sudo rm -rf void
sudo rm /tmp/ext3fs.img
How to Create a New Void PE Image from Scratch
When upgrading to a new Void Linux version or starting clean.
Step 1: Download Void Linux musl ROOTFS
cd /tmp
# Get the latest musl rootfs tarball from:
# https://voidlinux.org/download/ → "Base Tarballs" → x86_64-musl
wget https://repo-default.voidlinux.org/live/current/void-x86_64-musl-ROOTFS-<DATE>.tar.xz
Step 2: Create ext3fs.img
# Create a 3GB ext3 image (adjust size as needed)
dd if=/dev/zero of=/tmp/ext3fs.img bs=1M count=3072
mkfs.ext3 /tmp/ext3fs.img
# Mount it
sudo mkdir -p /mnt/void
sudo mount -o loop /tmp/ext3fs.img /mnt/void
# Extract rootfs
sudo tar xpf void-x86_64-musl-ROOTFS-*.tar.xz -C /mnt/void
Step 3: Install required packages
# Copy DNS config so xbps can resolve repos
sudo cp /etc/resolv.conf /mnt/void/etc/resolv.conf
# Use static xbps to install packages
cd /tmp
wget https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz
tar xf xbps-static-latest.x86_64-musl.tar.xz
# Update package database and install required packages
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Syu
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -S \
ntfs-3g \
parted \
dosfstools \
e2fsprogs \
grub-x86_64-efi \
lsblk \
udev \
kmod \
dracut \
wifi-firmware \
wpa_supplicant \
dhclient
Step 4: Configure rc.local
Copy the rc.local from the existing image or write a new one.
The rc.local must:
-
Scan all partitions (
lsblk) forautoexecute.sh -
Mount each partition (handle NTFS via
ntfs-3g, other viamount -t) -
When found, copy to
/root/autoexecute.sh,chmod +x, andexecit
See the current rc.local in the existing image for the full implementation.
Step 5: Extract kernel + initrd
# The kernel and initrd for GRUB must come from this rootfs
sudo cp /mnt/void/boot/vmlinuz-<version> /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/vmlinuz
sudo cp /mnt/void/boot/initramfs-<version>.img /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/initrd
|
Important
|
The kernel (vmlinuz) and initrd in linux/boot/ must match the kernel installed in the squashfs rootfs.
If you update the kernel inside the image, you MUST also update these files.
|
Step 6: Pack into squashfs
sudo umount /mnt/void
# Create squashfs directory structure
mkdir -p /tmp/void-squash/LiveOS
mv /tmp/ext3fs.img /tmp/void-squash/LiveOS/ext3fs.img
# Pack
sudo mksquashfs /tmp/void-squash /mnt/c/_gh/main/netsetup/src/NETSetup/linux/LiveOS/squashfs.img -comp xz -noappend
# Cleanup
sudo rm -rf /tmp/void-squash
Updating libstdc++ (fixing .NET 10 compatibility)
The current image has libstdc++.so.6.0.28 (GCC 10) which causes:
Error relocating NETSetup.musl: _ZSt28__throw_bad_array_new_lengthv: symbol not found
Fix: Update packages in existing image
Follow "How to Modify the Void PE Image" above, then:
# Upgrade all packages (pulls in GCC 12+ libstdc++)
sudo /tmp/usr/bin/xbps-install.static -r /mnt/void -Su
# Verify libstdc++ version (should be 6.0.32 or newer)
ls -la /mnt/void/usr/lib/libstdc++.so*
Then close/repack the image.
Updating the kernel + initrd
After a full system upgrade, the kernel version may change.
You need to update the boot files in linux/boot/:
# Check which kernel version is installed
ls /mnt/void/boot/vmlinuz-*
ls /mnt/void/boot/initramfs-*.img
# Copy to the boot folder
sudo cp /mnt/void/boot/vmlinuz-<new-version> /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/vmlinuz
sudo cp /mnt/void/boot/initramfs-<new-version>.img /mnt/c/_gh/main/netsetup/src/NETSetup/linux/boot/initrd
GRUB Configuration
grub.cfg (main)
Loads filesystem modules and sources grub_void.cfg:
insmod usbms
insmod part_gpt
insmod fat
insmod iso9660
insmod ntfs
insmod linux
search --file --no-floppy --set=voidlive "/boot/grub/grub_void.cfg"
source "(${voidlive})/boot/grub/grub_void.cfg"
grub_void.cfg (Void Linux boot entry)
menuentry "UnixPE" {
linux (${voidlive})/boot/vmlinuz \
root=live:LABEL=SYSTEM ro init=/sbin/init \
rd.live.overlay.overlayfs=1 lockdown=none
initrd (${voidlive})/boot/initrd
}
Key kernel parameters:
-
root=live:LABEL=SYSTEM— dracut live module finds squashfs on partition labeled SYSTEM -
rd.live.overlay.overlayfs=1— use overlayfs (writable tmpfs layer on top of read-only rootfs) -
lockdown=none— allow unsigned modules
NETSetup adds installer entry at runtime
Stage 4 (LinuxPE) appends a second GRUB menu entry to grub_void.cfg that boots the OS installer ISO from partition 2 via rdinit= wrapper.
See BootISOGrub2.cs and the process diagram in docs/NETSetup.puml.
rc.local Boot Flow
/etc/rc.local
│
├─ Lock file check (/var/lock/autoexecute.lock)
├─ Scan all partitions (lsblk: sd*, nvme*, hd*, sr*)
│ ├─ Skip: already mounted, no filesystem, swap
│ ├─ Mount: NTFS via ntfs-3g, others via mount -t $fstype
│ └─ Search for autoexecute.sh on each partition
│
├─ Found autoexecute.sh:
│ ├─ Copy to /root/autoexecute.sh (strip \r)
│ ├─ chmod +x
│ └─ exec → NETSetup.musl install
│
└─ Already has /root/autoexecute.sh:
└─ Re-exec if not already running
File Inventory
| File | Purpose | Tracked in git? |
|---|---|---|
|
GRUB EFI bootloader |
Yes |
|
Void Linux kernel |
Yes (LFS recommended) |
|
Void Linux initramfs |
Yes (LFS recommended) |
|
GRUB main config |
Yes |
|
Void Linux boot menu |
Yes |
|
GRUB background |
Yes |
|
Void Linux rootfs (compressed) |
Yes (LFS recommended) |
|
Extracted squashfs (working dir) |
No (.gitignore) |
|
Helper: unsquash + mount |
Yes |
|
Helper: unmount + repack |
Yes |
|
Helper: edit rc.local |
Yes |
Troubleshooting
_ZSt28__throw_bad_array_new_lengthv: symbol not found
libstdc is too old for .NET 10. See <<Updating libstdc (fixing .NET 10 compatibility)>>.
Loop mount fails on /mnt/c
WSL’s DrvFs (9p) does not support loop devices. Copy ext3fs.img to native Linux FS (/tmp/) first.
Cannot chroot from Ubuntu into Void
Ubuntu uses glibc, Void uses musl — binaries are incompatible.
Use static xbps (xbps-install.static -r /mnt/void) or a real Void Linux system.
Kernel panic on boot
Kernel (vmlinuz) and initrd in linux/boot/ don’t match the rootfs.
After upgrading the kernel inside the squashfs, copy the new vmlinuz + initrd to linux/boot/.
osisa NETSetup Master
where what
What is NETSetup
NETSetup is a collection of processes and tools to run any aspect of an IT Company. It provides processes to onboard employees handling support cases, provide a process to get information from what the customer needs, to offering, ordering, implementing and supporting whole network infrastructures.
Purpose of this document
This is the starting root of all NETSetup o.s.i.s.a. documentations. Working through this documentation will provide any user of NETSetup with sufficent information to run the NETSetup proccess.
Pre-requisites to work with this documentation
-
Chocolatey Package Manager is installed.
-
Visual Studio Code is installed.
Overall description of the complete NETSetup process landscape (master documentation)
Technologies to be used in NETSetup v3.0
These technologies are to be used in NETSetup 3.0:
-
Fileserver, Mailserver, Firewall, Webserver via Proxmox
-
File storage will use IPFS (interplanetary filesystem)
-
Name resolution will use IPNS (interplanetary nameserver)
-
Every business has a database with OrbitDB (Database with IPFS), local backups should exist
-
Software distribution will happen with our own "IPIS" (interplanetary Installation server)
-
This will also include our NETBase
-
When all this works, we should be able to "get rid" of our financial accounnting software because this will happen automatically
-
Eventually, the automated accounting will use wallets instead of bank accounts
-
-
Our business uses three main Repositories:
-
NETSetup: This repo is concerned with installing an OS / getting a computer up and running
-
NETBase: This repo contains all our business logic
-
Laufentaler: This repo contains the currency we intend to use in the future
Further read
Step by step guide on how to install Windows using osisa NETSetup HERE (gh Pages) and HERE (gh Pages source) and HERE (old documentation)
How to register a device in Autopilot
How to install KMULine
How To install Dropbox
How To update Alltron product data
How To assure the correct set up of a new development machine
How To create new GitHub Repository
addition from release 0.6.0
Osisa NETSetup Diagram
Explanation of terms:
Config: Whole customer configuration (unique per customer)
Setup: Specific Version of a customer hardware/workstation setup
Ensure Item: Either locate existing item or create new item
@startuml
!pragma teoz true
'Actors
'Customer Box
box Customer #lightyellow
actor Customer
end box
'Osisa Box
box osisa #lightblue
actor "Agent (public)" as Agent
actor "Ticketing (public)" as Ticketing
actor "Accounting (private)" as Accounting
actor "NETSetup (private)" as NETSetup
end box
'Suppliers Box
box Suppliers #lightgreen
actor "Supplier (Alltron)" as Supplier
actor "Software Vendor (Microsoft)" as SWVendor
actor "Hardware Vendor (HP)" as HWVendor
end box
'Variables
'Contact and Ticket
!$callAgent = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/050%20Kontaktaufnahme/Kontaktaufnahme%20V1.1.adoc callAgent() -> new order]]"
!$createTicket = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/050%20Kontaktaufnahme/Ticketverwaltung%20in%20Freshdesk.adoc createNETSetupTicket(order) -> new ticket]]"
!$notifyCustomer = "notifyCustomer(ticket)"
'Quote Process
!$requestCurrentMode = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/100%20Offerte/Offertprozess.adoc#ist-soll-analyse requestCurrentMode(ticket) -> ensure(ticket.customer) config<currentMode> ]]"
!$responseCurrentMode = "responseCurrentMode(ticket) -> update config<updatedCurrentMode>"
!$requestFutureMode = "requestFutureMode(config<updatedCurrentMode>)"
!$responseFutureMode = "responseFutureMode(config<updatedCurrentMode>) -> config<futureMode>"
!$createQuote = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/100%20Offerte/Offertprozess.adoc#offerterstellung createQuote(config<updatedCurrentMode>, config<futureMode>) -> new quote]]"
!$returnQuote = "returnQuote(quote)"
!$sendQuote = "sendQuote(quote)"
!$responseQuote = "responseQuote(quote)"
'Order Process
!$createOrder = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/230%20Beschaffung/Beschaffung.adoc createOrder(quote) -> new order]]"
!$confirmOrder = "confirmOrder(order)"
!$updateCustomer = "updateCustomer(customer)"
!$registerCustomer = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/250%20Setup/Kundenerfassung%20in%20NETSetup-Datenbank.adoc registerCustomer(databaseEntry<newCustomer>) -> new customer]]"
!$createSetup = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/250%20Setup/Softwareinfoblatt%20NETSetup-Datenbank.adoc createSetup(config<futureMode>) -> new setup]]"
!$returnSetup = "returnSetup(setup)"
!$deliverOrder = "deliverOrder(setup)"
'Setup Process Server
!$plugInServer = "plugInServer(setup)"
!$configureRaid = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/250%20Setup/Installation%20Server.adoc#festplatten-installieren-und-raid-konfigurieren configureRaid()]]"
!$setUpServer = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/250%20Setup/Installation%20Server.adoc#aufsetzen-per-netsetup setUpServer(setup)]]"
!$configureServer = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/250%20Setup/Installation%20Server.adoc#konfiguration configureServer()]]"
!$setUpFirewall = "firewallSetup()"
!$setUpAccessPoints = "accesspointsSetup()"
'Setup Process Client
!$plugInClient = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup.adoc plugInHardware(setup)]]"
!$registerHash = "registerHardwareHash(setup)"
!$rebootClient = "reboot(setup)"
!$requestLogin = "requestLogin()"
!$login = "login()"
!$osSetup = "[[https://github.com/osisa/NETSetup/tree/develop/NETSetup.WinPEBoot TargetOSSetup(setup)]]"
!$domainJoin = "domainJoin(setup)"
'Last Steps
!$backupCheck = "targetBackupCheck()"
!$backupRecoveryTest = "targetBackupRecoveryTest()"
!$backupSelfTest = "targetBackupSelfTest()"
!$requestCablingProtocol = "networkCablingMeasurementRequest(Protocol)"
!$returnCablingProtocol = "return(Protocol)"
!$validateCablingProtocol = "ProtocolValidate()"
!$setDeliveryDate = "setTicketDeliveryDate(setup.deliveryDate)"
!$deliverMaterial = "[[https://github.com/osisa/NETSetup/blob/develop/docs/NETSetup/300%20Auslieferung/Auslieferung.adoc deliverOrder]](setup and [[https://github.com/osisa/NETSetup/tree/develop/docs/NETSetup/400%20Abnahme setup.approvalForm]], training material and awareness test)"
!$fillOutMaterial = "fillOut(setup.approvalForm and awareness test)"
'Billing and Ending
!$comissionBill = "[[https://github.com/osisa/NETSetup/blob/develop/docs/Infobl%C3%A4tter/Software/Sage/Sage%20Start/Softwareinfoblatt%20Sage%20Start.adoc commissionBill(setup)]]"
!$returnBill = "returnBill(setup.bill)"
!$sendBill = "sendBill(setup.bill)"
!$payBill = "payBill(setup.bill)"
!$notifyAccounting = "notify(setup.bill.paid())"
!$closeTicket = "closeTicket()"
'Netsetup Diagram for Client
== Netsetup Diagram for Client ==
'Contact and Ticket
Customer -> Agent : $callAgent
Agent -> Ticketing : $createTicket
Ticketing -> Customer : $notifyCustomer
'Quote Process
Agent -> Customer : $requestCurrentMode
Customer -> Agent : $responseCurrentMode
group If customer not\nSatisfied: repeat
Agent -> Customer : $requestFutureMode
Customer -> Agent : $responseFutureMode
Agent -> Accounting : $createQuote
Accounting -> Agent : $returnQuote
Agent -> Customer : $sendQuote
Customer -> Agent : $responseQuote
end
'Order Process
Agent -> Supplier : $createOrder
Supplier -> Agent : $confirmOrder
alt if customer is already registered:
Agent -> NETSetup : $updateCustomer
else else
Agent -> NETSetup: $registerCustomer
end
Agent -> NETSetup : $createSetup
NETSetup -> Agent : $returnSetup
Supplier -> Customer : $deliverOrder
'Setup Process Client
loop for n clients do
Agent -> Agent: $plugInClient
Agent -> SWVendor : $registerHash
Agent -> SWVendor : $rebootClient
SWVendor -> Agent: $requestLogin
Agent -> SWVendor : $login
SWVendor -[#red]> Agent: $osSetup
Agent -> Agent : $domainJoin
end
'Last Steps
Agent -> Agent : $backupCheck
Agent -> Agent : $backupRecoveryTest
Agent -> Agent : $backupSelfTest
Agent -> Customer : $requestCablingProtocol
Customer -> Agent : $returnCablingProtocol
Agent -> Agent : $validateCablingProtocol
Agent -> Ticketing : $setDeliveryDate
Agent -> Customer : $deliverMaterial
Customer -> Agent : $fillOutMaterial
'Billing and Ending
Agent -> Accounting : $comissionBill
Accounting -> Agent : $returnBill
Agent -> Customer : $sendBill
Customer -> Accounting : $payBill
Accounting -> Agent : $notifyAccounting
Agent -> Ticketing : $closeTicket
'NETSetup Diagram for Network (Client + Server)
== Diagram for Network (Client + Server) ==
'Contact and Ticket
Customer -> Agent : $callAgent
Agent -> Ticketing : $createTicket
Ticketing -> Customer : $notifyCustomer
'Quote Process
Agent -> Customer : $requestCurrentMode
Customer -> Agent : $responseCurrentMode
group If customer not\nSatisfied: repeat
Agent -> Customer : $requestFutureMode
Customer -> Agent : $responseFutureMode
Agent -> Accounting : $createQuote
Accounting -> Agent : $returnQuote
Agent -> Customer : $sendQuote
Customer -> Agent : $responseQuote
end
'Order Process
Agent -> Supplier : $createOrder
Supplier -> Agent : $confirmOrder
alt if customer is already registered:
Agent -> NETSetup : $updateCustomer
else else
Agent -> NETSetup: $registerCustomer
end
Agent -> NETSetup : $createSetup
NETSetup -> Agent : $returnSetup
Supplier -> Customer : $deliverOrder
'Setup Process Server
Agent -> Agent : $plugInServer
Agent -> Agent : $configureRaid
Agent -> NETSetup : $setUpServer
Agent -> Agent : $configureServer
Agent -> Agent : $setUpFirewall
Agent -> Agent : $setUpAccessPoints
'Setup Process Client
loop for n clients do
Agent -> Agent: $plugInClient
Agent -> SWVendor : $registerHash
Agent -> SWVendor : $rebootClient
SWVendor -> Agent: $requestLogin
Agent -> SWVendor : $login
SWVendor -[#red]> Agent: $osSetup
Agent -> Agent : $domainJoin
end
'Last Steps
Agent -> Agent : $backupCheck
Agent -> Agent : $backupRecoveryTest
Agent -> Agent : $backupSelfTest
Agent -> Customer : $requestCablingProtocol
Customer -> Agent : $returnCablingProtocol
Agent -> Agent : $validateCablingProtocol
Agent -> Ticketing : $setDeliveryDate
Agent -> Customer : $deliverMaterial
Customer -> Agent : $fillOutMaterial
'Billing and Ending
Agent -> Accounting : $comissionBill
Accounting -> Agent : $returnBill
Agent -> Customer : $sendBill
Customer -> Accounting : $payBill
Accounting -> Agent : $notifyAccounting
Agent -> Ticketing : $closeTicket
@enduml
@enduml
Steps required to setup a fresh development machine
---
setup using netsetup
(you might need mermaid-cli aka mmdc, npm install -g @mermaid-js/mermaid-cli, and add "%USERPROFILE%\AppData\Roaming\npm" to path)
set-executionpolicy -executionpolicy bypass -scope localmachine -force
Set-ExecutionPolicy Bypass -Scope Process -Force; iex New-Object System.Net.WebClient).DownloadString('http://netsetupprod.osisa.com:8080/install.ps1'
choco source add --name=netsetup --source=http://netsetupprod.osisa.com:8080/chocolatey
cinst -y firefox git git-lfs vscode chocolateygui 7zip dotnetcore dotnet dropbox googlechrome infoniqaonestart2023_00 kmuline netsetupdb notepadplusplus office365netsetup teamviewer teams vlc windirstat asciidocfx putty ruby
2-machine firewall rdp wallpaper vpnToConfiguredVPNServer(connection to domain controller)
install-language en-us
set-winuilanguageoverride en-us
$LanguageList = Get-WinUserLanguageList ; $LanguageList.Add("en-US") ; $firstLang = $LanguageList[0].LanguageTag ; $LanguageList[0] = $LanguageList[-1] ; $LanguageList[-1] = $firstLang ; Set-WinUserLanguageList $LanguageList -force
set-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "dontdisplaylastusername" -Value 1
set-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "PromptOnSecureDesktop" -Value 0
set-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "shutdownwithoutlogin" -Value 0
set-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "ConsentPromptBehaviorAdmin" -Value "0"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "LocalAccountTokenFilterPolicy" -Value 1
Get-NetConnectionProfile | ForEach-Object { Set-NetConnectionProfile -NetworkCategory Private -Name $.Name Set-NetConnectionProfile -NetworkDiscoveryEnabled true -Name $.Name }
[System.Environment]::SetEnvironmentVariable('DOTNET_ENVIRONMENT','Development',[System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('MSBuildEnableWorkloadResolver','true',[System.EnvironmentVariableTarget]::Machine)
[System.Environment]::SetEnvironmentVariable('MSDOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLE','false',[System.EnvironmentVariableTarget]::Machine)
git config --global --add safe.directory /git/
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
reboot
domain join
login as your user (must be an admin)
3-user
dotnet nuget remove source github
dotnet nuget add source --name github "https://nuget.pkg.github.com/osisa/index.json" -u soeren.maske@osisa.com -p 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 --store-password-in-clear-text
"./nuget.exe setapikey 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 -Source github"
dotnet tool install --global GitVersion.Tool
dotnet tool install --global gpr
dotnet tool install --global netbase.build --prerelease --no-cache
dotnet tool update --global netbase.build --prerelease --no-cache
gem install asciidoctor
gem install asciidoctor-diagram
asciidoctor -r asciidoctor-diagram -a diagrams docs/ghpages.adoc
Windows SDK
add newest win to path, eg C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64
Windows ADK Windows EDK
Install-Module -Name OpenSSL
systemmanagement.contracts
-> isoftware
-> icomputer
-> iuser
usermanagement.contracts
-> irole
-> igroup
-> imembership
-> idomain
human.contracts
-> icontact
-> inaturalperson
-> iperson
localization.contracts
-> ilocale
-> itimezone
enterprise.contracts
-> icompany
-> iorganizationalunit
-> iemployee
-> icustomer
-> isupplier
-> idevice
-> itask
-> iservice
netsetup.contracts
-> inetsetupsoftware [new: packagename]
-> inetsetupcontact [new: naturalperson]
-> inetsetupcustomer [new: conclusionDate, documentationLink]
-> inetsetupmanageduser [new: associated computers]
-> inetsetupdomain [new: groups replaced with functionality groups, computer list, contact list replaces person list]
-> iconfig has domain, contacts, software
BSP
funcionality = accounting
accounting.softwareversion[0] = infoniqa v2023
accounting.softwareversion[1] = office 365
accounting.membership[0] = remo.oser [role: head]
Wake-on-LAN (WOL) Integration with NETSetup
The NETSetup process can integrate with Wake-on-LAN (WOL) to remotely trigger machine setup or maintenance tasks. This is particularly useful for automated deployments.
The following outlines how WOL can be configured and utilized:
Prerequisites
Your network and hardware must support WOL. This typically involves:
-
BIOS/UEFI settings enabling WOL for the network adapter.
-
Network interface card (NIC) supporting WOL.
-
Proper network configuration to allow WOL packets.
Configuring WOL on Linux Targets
For Linux-based systems that netsetup might be deploying or managing, WOL can be configured as follows:
Required Packages
Install the necessary packages:
sudo apt-get update
sudo apt-get install build-essential libelf-dev ethtool
Identifying Network Interfaces
Network interfaces are often named enp1s0 and enp2s0 on systems like H4. Check your system’s interfaces with:
ip link
Checking and Enabling WOL
-
Check if WOL is enabled for an interface (e.g.,
enp1s0) using:
ethtool enp1s0 | grep Wake
-
Alternatively, check all interfaces:
grep . /sys/class/net/*/device/power/wakeup 2>/dev/null
-
Temporarily enable WOL for an interface:
sudo ethtool -s enp1s0 wol g
Persistent WOL Configuration
To enable WOL persistently across reboots, create a systemd service:
# /etc/systemd/system/wol@.service
[Unit]
Description=Wake-on-LAN for %i
Requires=network.target
# After=network.target
After=network-online.target
ExecStart=/bin/sh -c "ethtool -s %i wol g" Type=oneshot
WantedBy=multi-user.target
After creating the service file, enable and start it for your network interface (e.g., enp1s0):
sudo systemctl enable wol@enp1s0.service
sudo systemctl start wol@enp1s0.service
Note: The netsetup command itself may have options or configurations to trigger WOL or manage these settings on target machines. Further details on netsetup specific WOL commands would be found in its dedicated command-line documentation.
Proxmox VM provisioning during update
Feature ship. netsetup update on Proxmox host now spin customer VMs auto.
Two flow. Pick by Machine.OperatingSystem:
-
NixOS flow — os match
NixNextcloud,NixIPFS,NixOS. Boot genericnixos.iso(qemu-guest-agent on, master pubkey bake in). Wait guest-agent report ipv4. Pick template viaNixTemplateFactory.ForMachine(machine). Emit flake/default/hardware nix to/NETSetup/generated-nix/<vm>.scpconfig to VM/nixos-config/,sshinstall.sh -h <name> -d <disk>via master key. -
Windows flow — os match
Windows10/Windows11/WindowsServer201x/202x. Build per-VM offlineNETSetup-{ticket}-{host}.isovia existing create-iso pipeline. Upload to Proxmox iso storage. Stealth hardware iff Win11. Boot VM from ISO, unattended install run normal.
Paths / keys / cadence:
-
Proxmox iso store:
/var/lib/vz/template/iso/(bothnixos.isoandNETSetup-*.iso). -
Generated nix configs:
/NETSetup/generated-nix/<vm>. Rewrite each update cycle. -
Master SSH key: pub bake into nixos.iso at
src/NETSetup/nixos/netsetup-master.pub. Priv extract at runtime to/NETSetup/.ssh/netsetup-master.key(mode 0600). -
Dropbox source of truth for nixos.iso:
NETSetup/ISOs/nixos.iso.7z+nixos.iso.sha256.EnsureNixosIsoOnProxmoxHostcheck sha, download+7z-extract if miss/mismatch. -
Cadence: every
netsetup updatecall hit this path. Idempotent (skip existing VMs, skip ISO sync when sha match). -
Fail mode: per-VM error log + continue. Aggregate throw at end list all fail VM name.
Entry point: UpdateMethods.RunProxmoxUpdate append EnsureCustomerVms.Run(host, config, services) after RunBtrfsScrub.
VM order: DC first, then others (CreateVirtualMachines.GetOrderedVmsForHost).
Sequence diagram:
@startuml NETSetupProvisionVms
title Proxmox VM Provisioning During `netsetup update`
actor Admin
participant "netsetup update\n(on Proxmox host)" as NS
participant "UpdateMethods\nRunProxmoxUpdate" as UM
participant "EnsureCustomerVms" as ECV
participant "EnsureNixosIso" as EI
participant "Dropbox\nNETSetup/ISOs" as DB
participant "Proxmox Host\n(corsinvest API)" as PH
participant "NixOS VM\n(generic nixos.iso)" as NIX
participant "Windows VM" as WIN
Admin -> NS: netsetup update
NS -> UM: RunProxmoxUpdate(host, config)
UM -> UM: apt update + full-upgrade
UM -> UM: btrfs scrub
UM -> ECV: EnsureCustomerVms(host, config)
group Nix path (any nix VM in config)
ECV -> EI: EnsureNixosIsoOnProxmoxHost
EI -> DB: GET nixos.iso.sha256
EI -> EI: compare to local iso sha256
alt mismatch or missing
EI -> DB: GET nixos.iso.7z
EI -> EI: verify sha256, 7z extract
EI -> PH: place nixos.iso at\n/var/lib/vz/template/iso/
end
end
loop for each VM (DC first)
alt IsNixBacked(os)
ECV -> PH: CreateVirtualMachine(nixos.iso)
PH -> NIX: boot from nixos.iso\n(qemu-guest-agent enabled)
ECV -> PH: qm guest network-get-interfaces\n(poll 2s, deadline 5m)
PH --> ECV: ipv4
ECV -> ECV: NixTemplateFactory.ForMachine\nemit flake.nix + default.nix + hardware.nix\nto /NETSetup/generated-nix/<vm>
ECV -> NIX: scp -i netsetup-master.key\n-r <genDir> root@ip:/nixos-config/
ECV -> NIX: ssh root@ip\ninstall.sh -h <name> -d <disk>
NIX --> ECV: install OK, reboot
else Windows
ECV -> ECV: per-VM offline create-iso\nNETSetup-<ticket>-<host>.iso
ECV -> PH: upload ISO to\n/var/lib/vz/template/iso/
ECV -> PH: CreateVirtualMachine(NETSetup-ISO,\nstealth hw iff Win11)
PH -> WIN: boot from NETSetup ISO\n(unattended install)
else other
ECV -> ECV: log + skip
end
ECV -> ECV: collect per-VM outcome
end
alt any failure
ECV --> UM: throw NETSetupException\n(aggregate failed VM names)
else all success
ECV --> UM: OK
end
UM --> NS: update complete
NS --> Admin: exit 0
@enduml
In Development
nixos-vm-provisioning
GOAL: Proxmox host running NETSetup provisions customer VMs per NETSetupConfig during netsetup update. NixOS-backed VMs (Nextcloud, IPFS, generic NixOS) boot generic nixos.iso, NETSetup SSHes in using master key, scps C#-generated flake + host config, runs install.sh remotely. Windows VMs use existing per-VM offline create-iso pipe. Result: fully hands-off VM deploy from a single netsetup update on the Proxmox host.
Decisions:
-
ISO artifact:
nixos.isoon Proxmox ISO storage. Dropbox holdsnixos.iso.7z+nixos.iso.sha256(ArchiveSync + SevenZipCommand pattern — see Serenacompressed_image_driver_sync). -
Bootstrap SSH: single master NETSetup keypair. Pubkey committed at
src/NETSetup/nixos/netsetup-master.pub, baked intoiso/iso.nixrootauthorized_keys. Privkey shipped with NETSetup binary undersrc/NETSetup/Resources/netsetup-master.key(embedded resource, extracted to/NETSetup/.ssh/netsetup-master.keychmod 0600 on use). -
OS trigger:
Machine.OperatingSystem.Name.StartsWith("Nix")→ nix-backed.OS.NixOS(contracts) routes toNixHostTemplate. New NETSetup-local OS constantsNixNextcloud,NixIPFSdefined inNETSetup.Config.Nix.NetsetupOS(NOT in contracts lib). -
Templates: abstract
NixHostTemplatebase + concreteNixNextcloudTemplate,NixIpfsTemplate. Each emits three files as strings:flake.nix,hosts/<name>/default.nix,hardware.nix. Content derived fromsrc/NETSetup/nixos/hosts/default-nextcloud/default.nixanddefault-ipfs/default.nix. -
Disk: plain btrfs (no LUKS). Default
/dev/sda; VirtIO VMs use/dev/vda.NixHostTemplatetakes device param. -
Entry point: extend
UpdateMethods.RunProxmoxUpdate(appendEnsureCustomerVms(host, config)at end).InstallCommand.InstallStage.ProxmoxVMsstays a stub. -
New commands live in
NETSetup/NETCommand/— no changes to sharedosisa.NETCommand.NixRemoteInstallCommandcomposesLinuxCommandBaseforssh/scp.QmGuestCommandwraps Proxmox-APInodes/<node>/qemu/<vmid>/agentvia corsinvest client (reuse existingProxmox/Hostwrappers fromosisa.SystemManagement.Proxmox). -
No
System.IO: useAbsolutePath+PhysicalFileOperator. No rawProcess: use NETCommand builders. -
VM identification: reuse
CreateVirtualMachines.IsForHost+GetOrderedVmsForHost(DC-first ordering preserved). -
Local emission path for generated nix configs:
AbsolutePath("/NETSetup/generated-nix") / vmName(Linux Proxmox host), wiped + rewritten each update cycle.-
✓ Add NETSetup-local
OSconstants and nix-detection extension: createsrc/NETSetup/Config/Nix/NetsetupOS.cswith public static readonlyOperatingSystem NixNextcloud = OperatingSystem.From("NixNextcloud", OperatingSystemFamily.Linux_GNU)andNixIPFS = OperatingSystem.From("NixIPFS", OperatingSystemFamily.Linux_GNU). Createsrc/NETSetup/Extensions/NixOsExtensions.cswithpublic static bool IsNixBacked(this OperatingSystem os)returning true whenos == OS.NixOSORos.Name.StartsWith("Nix", StringComparison.OrdinalIgnoreCase). Assertions: -
NetsetupOS.NixNextcloud.Name.Should().Be(SomeNixNextcloudName)
-
NetsetupOS.NixNextcloud.Family.Should().Be(OperatingSystemFamily.Linux_GNU)
-
NetsetupOS.NixIPFS.Name.Should().Be(SomeNixIpfsName)
-
OS.NixOS.IsNixBacked().Should().BeTrue()
-
NetsetupOS.NixNextcloud.IsNixBacked().Should().BeTrue()
-
NetsetupOS.NixIPFS.IsNixBacked().Should().BeTrue()
-
OS.Windows11.IsNixBacked().Should().BeFalse()
-
OS.Proxmox.IsNixBacked().Should().BeFalse()
-
OS.Unknown.IsNixBacked().Should().BeFalse()
-
✓ Extend
src/NETSetup/nixos/iso/iso.nixwith qemu-guest-agent + master pubkey: addservices.qemuGuest.enable = true;andusers.users.root.openssh.authorizedKeys.keys = [ (lib.fileContents ../netsetup-master.pub) ];(createsrc/NETSetup/nixos/netsetup-master.pubplaceholder committed with a real ed25519 pubkey). Keepservices.openssh.enable = true, ensurePermitRootLogin = "prohibit-password"(or"yes"during bootstrap). Preserve all existing options (volumeID, contents, install-nixos.sh script, supportedFilesystems). Assertions: -
isoNixContent.Should().Contain("services.qemuGuest.enable = true")
-
isoNixContent.Should().Contain("users.users.root.openssh.authorizedKeys.keys")
-
isoNixContent.Should().Contain("netsetup-master.pub")
-
isoNixContent.Should().Contain("services.openssh.enable = true")
-
pubFileExists.Should().BeTrue()
-
pubFileContent.Should().StartWith("ssh-ed25519 ")
-
✓ Create
build-nixos-imageskill + standalone script: add.claude/skills/build-nixos-image/SKILL.md(description: "Build NixOS installer ISO, 7z, sha256, upload to Dropbox") plussrc/NETSetup/nixos/scripts/build-nixos-image.shthat (1)cdto nixos dir,git add -A(flakes see only tracked), (2)nix build .#iso --out-link /tmp/nixos-iso-result, (3) resolve ISO file via symlink, (4)7z a -mx=9 /tmp/nixos.iso.7z <iso>, (5)sha256sum /tmp/nixos.iso.7z > /tmp/nixos.iso.sha256, (6)cpboth to"$DROPBOX/NETSetup/ISOs/". Script must be idempotent, detect missing nix/7z and error clearly, and exit non-zero on any step failure. Skill instructs running under WSL distronixos(modeled afterupdate-nixos-imageskill). Assertions: -
skillFileExists.Should().BeTrue()
-
skillContent.Should().Contain("build-nixos-image")
-
skillContent.Should().Contain("WSL")
-
scriptFileExists.Should().BeTrue()
-
scriptContent.Should().Contain("nix build .#iso")
-
scriptContent.Should().Contain("7z a")
-
scriptContent.Should().Contain("sha256sum")
-
scriptContent.Should().Contain("set -euo pipefail")
-
scriptIsExecutable.Should().BeTrue() (file mode check)
-
✓ Implement C# Nix templates under
src/NETSetup/Config/Nix/Templates/: abstractNixHostTemplatewith propertiesHostname(MachineName),DiskDevice(string, default/dev/sda),UserName(string, defaultadmin),TimeZone(string, defaultUTC),AuthorizedKeys(IReadOnlyList<string>) and virtual methodsGenerateFlakeNix(),GenerateHardwareNix(), abstractGenerateDefaultNix(). ConcreteNixNextcloudTemplateandNixIpfsTemplateoverrideGenerateDefaultNix()with content ported fromsrc/NETSetup/nixos/hosts/default-nextcloud/default.nixanddefault-ipfs/default.nix(literal string). FactoryNixTemplateFactory.ForMachine(Machine machine)returns concrete template based onmachine.OperatingSystem:NixNextcloud→ NixNextcloudTemplate,NixIPFS→ NixIpfsTemplate,NixOS→NixHostTemplategeneric subclassNixGenericTemplate, other → throwsNETSetupException. Assertions: -
nextcloudTemplate.GenerateDefaultNix().Should().Contain("services.nextcloud.enable = true")
-
nextcloudTemplate.GenerateDefaultNix().Should().Contain(SomeHostname)
-
nextcloudTemplate.GenerateFlakeNix().Should().Contain("description")
-
nextcloudTemplate.GenerateHardwareNix().Should().Contain(SomeDiskDevice)
-
ipfsTemplate.GenerateDefaultNix().Should().Contain("services.ipfs.enable = true")
-
NixTemplateFactory.ForMachine(nextcloudMachine).Should().BeOfType<NixNextcloudTemplate>()
-
NixTemplateFactory.ForMachine(ipfsMachine).Should().BeOfType<NixIpfsTemplate>()
-
NixTemplateFactory.ForMachine(nixosMachine).Should().BeOfType<NixGenericTemplate>()
-
act.Should().Throw<NETSetupException>() for non-nix machine
-
template.GenerateDefaultNix().Should().Contain(SomeAuthorizedKey)
-
✓ Implement Proxmox guest-agent IP discovery:
src/NETSetup/Extensions/QmGuestAgentExtensions.cswithpublic static IPAddress DiscoverVmIpViaQmAgent(this Host host, int vmId, TimeSpan deadline, ILogger logger). Uses corsinvest_client.Nodes[<node>].Qemu[vmId].Agent.NetworkGetInterfaces.NetworkGetInterfaces()(or equivalent), parses JSONdata.result[].ip-addresses[]filteringip-address-type == "ipv4", excluding127.0.0.0/8and169.254.0.0/16. Polls every 2s until first match or deadline. ThrowsNETSetupExceptionwithvmIdin message on timeout. Assertions: -
result.Should().Be(IPAddress.Parse(SomeValidIp))
-
result.AddressFamily.Should().Be(AddressFamily.InterNetwork)
-
(loopback-only scenario) act.Should().Throw<NETSetupException>().Where(e ⇒ e.Message.Contains(SomeVmId.ToString()))
-
(link-local-only scenario) act.Should().Throw<NETSetupException>()
-
(timeout with no response) act.Should().Throw<NETSetupException>().Where(e ⇒ e.Message.Contains("timeout"))
-
(multiple ipv4) result.Should().Be(IPAddress.Parse(SomeFirstNonLoopbackIp))
-
✓ Implement
NixRemoteInstallCommandundersrc/NETSetup/NETCommand/NixRemoteInstallCommand.csusing NETCommand builder pattern. Builder exposesWithIp(IPAddress),WithHostname(string),WithMasterKey(AbsolutePath keyPath),WithConfigDir(AbsolutePath localGeneratedDir),WithDiskDevice(string)(default/dev/sda),CopyHostSshKeys(bool).Execute()runs: (1)ssh-keyscantarget → known_hosts, (2)scp -r <localDir>/* root@<ip>:/nixos-config/, (3)ssh root@<ip> "/nixos-config/scripts/install.sh -h <name> -d <device>"with 30min timeout. Each phase is aLinuxCommandBaseinternally. ReturnsNixRemoteInstallResultwithSuccess,StandardOutput,StandardError,Phase(KeyScan/Scp/Install) on failure. Assertions: -
builder.WithIp(SomeIp).WithHostname(SomeHostname).WithMasterKey(SomeKeyPath).WithConfigDir(SomeConfigDir).Build().Should().NotBeNull()
-
act.Should().Throw<ArgumentException>() when WithIp not called
-
act.Should().Throw<ArgumentException>() when WithHostname not called
-
command.RenderedScpArgs.Should().Contain("root@" + SomeIp)
-
command.RenderedScpArgs.Should().Contain("/nixos-config/")
-
command.RenderedSshArgs.Should().Contain("install.sh -h " + SomeHostname)
-
command.RenderedSshArgs.Should().Contain("-d " + SomeDiskDevice)
-
command.RenderedSshArgs.Should().Contain("-i " + SomeKeyPath)
-
result.Phase.Should().Be(InstallPhase.Install) on successful full run
-
✓ Implement
EnsureNixosIsoOnProxmoxHost(ISystemHost host, IFileProvider dropbox): checks/var/lib/vz/template/iso/nixos.isoexistence AND sha256 match against DropboxNETSetup/ISOs/nixos.iso.sha256. If missing or mismatch: downloadsnixos.iso.7zto temp, verifies sha256, extracts viaSevenZipCommandto/var/lib/vz/template/iso/nixos.iso, logs action. Idempotent — returns silently if match. Lives atsrc/NETSetup/Helpers/EnsureNixosIso.cs. Assertions: -
act.Should().NotThrow() when ISO present + hash matches
-
result.Action.Should().Be(EnsureAction.Downloaded) when missing
-
result.Action.Should().Be(EnsureAction.Replaced) when hash mismatch
-
result.Action.Should().Be(EnsureAction.UpToDate) when already current
-
act.Should().Throw<NETSetupException>() when Dropbox sha256 file missing
-
act.Should().Throw<NETSetupException>() when downloaded archive sha256 fails verification
-
isoPath.FileExists().Should().BeTrue() after successful run
-
✓ Implement
ProvisionNixosVm(Host proxmoxHost, Machine machine, NETSetupConfig config, ISystemHost sysHost)insrc/NETSetup/Stages/5-NETSetupLinux/ProvisionNixosVm.cs: (1) buildVirtualMachineentity fromMachine(name, id from serial prefix, CPU/RAM/disk from machine hardware,IsoName = "nixos.iso"), (2) skip if VM already exists with matching serial description, (3) callproxmoxHost.CreateVirtualMachine(vm, autoStart: true), (4)DiscoverVmIpViaQmAgent(vm.Id, TimeSpan.FromMinutes(5)), (5) pick template viaNixTemplateFactory.ForMachine(machine), emit files toAbsolutePath("/NETSetup/generated-nix") / machine.Name, (6) runNixRemoteInstallCommandbuilder with generated dir + master key path, (7) log result, throwNETSetupExceptionon failure. Assertions: -
(new VM scenario) proxmoxHost.Received().CreateVirtualMachine(Arg.Is<VirtualMachine>(vm ⇒ vm.IsoName == SomeNixosIsoName))
-
(existing VM scenario) proxmoxHost.DidNotReceive().CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
result.VmIp.Should().Be(IPAddress.Parse(SomeDiscoveredIp))
-
result.GeneratedConfigDir.Should().Be(SomeExpectedGeneratedDir)
-
act.Should().Throw<NETSetupException>() when DiscoverVmIpViaQmAgent times out
-
act.Should().Throw<NETSetupException>() when NixRemoteInstall fails
-
generatedDir.DirectoryExists().Should().BeTrue() after successful emission
-
(generatedDir / "flake.nix").FileExists().Should().BeTrue()
-
(generatedDir / "hosts" / SomeHostname / "default.nix").FileExists().Should().BeTrue()
-
(generatedDir / "hardware.nix").FileExists().Should().BeTrue()
-
✓ Implement
ProvisionWindowsVmOnProxmox(Host proxmoxHost, Machine machine, NETSetupConfig config, ISystemHost sysHost, IServiceProvider services)insrc/NETSetup/Stages/5-NETSetupLinux/ProvisionWindowsVmOnProxmox.cs: (1) skip if VM exists, (2) derive ticket + hostname frommachine.Name+ config, (3) invoke existingCreateIsoCommandoffline path (MainISO.CreateOfflineProductionISO+CopyToProxmoxStorageIfApplicable) to produceNETSetup-{ticket}-{host}.isoin/var/lib/vz/template/iso/, (4) buildVirtualMachinewith that ISO as main, stealth hardware iffShouldUseStealthHardware(machine), (5)proxmoxHost.CreateVirtualMachine(vm, autoStart: true). Reuses existingCreateVirtualMachines.ShouldUseStealthHardware. Assertions: -
(new Windows 11 VM) proxmoxHost.Received().CreateVirtualMachine(Arg.Is<VirtualMachine>(vm ⇒ vm.IsoName.StartsWith("NETSetup-")))
-
(WindowsServer VM) proxmoxHost.Received().CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
(existing VM) proxmoxHost.DidNotReceive().CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
producedIsoPath.Should().Be(ProxmoxDefaultIsoStoragePath + "/NETSetup-" + SomeTicket + "-" + SomeHostname + ".iso")
-
result.UsedStealthHardware.Should().BeTrue() when machine is Windows 11
-
result.UsedStealthHardware.Should().BeFalse() when machine is WindowsServer
-
act.Should().Throw<NETSetupException>() when offline ISO creation fails
-
✓ Wire VM provisioning into
UpdateMethods.RunProxmoxUpdate: add new methodEnsureCustomerVms(ISystemHost host, NETSetupConfig config, IServiceProvider services)called at end ofRunProxmoxUpdate(afterRunBtrfsScrub). Method: (1) connect to localProxmoxvia DI, (2)GetHostAsync(host.MachineName), (3) callEnsureNixosIsoOnProxmoxHostiff any nix-backed machine targets this host, (4)GetOrderedVmsForHost(config, host.SerialNumber)— iterate with DC first, (5) dispatch:IsNixBacked→ProvisionNixosVm, else Windows →ProvisionWindowsVmOnProxmox, other → log skip. Errors per-VM are logged + continue (one bad VM must not block others). Final aggregate throws if any VM failed. Assertions: -
(no VMs for host) proxmoxHost.DidNotReceive().CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
(mixed nix+windows VMs) proxmoxHost.Received(SomeExpectedCount).CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
(DC-first ordering) firstCreatedVmSerial.Should().StartWith(SomeDcVmIdPrefix)
-
(all nix, no windows) ensureNixosIsoCalled.Should().BeTrue()
-
(only windows, no nix) ensureNixosIsoCalled.Should().BeFalse()
-
(one VM fails, others succeed) proxmoxHost.Received(SomeTotalCount).CreateVirtualMachine(Arg.Any<VirtualMachine>())
-
(any failure) act.Should().Throw<NETSetupException>().Where(e ⇒ e.Message.Contains(SomeFailedVmName))
-
(all success) act.Should().NotThrow()
-
✓ Documentation: add section "Proxmox VM provisioning during update" to
netsetup.adoc(after existing Proxmox sections) describing the two flows (NixOS SSH-deploy + Windows ISO-deploy), required keys/paths, update cadence. Add PUML sequence diagramsrc/NETSetup/docs/NETSetupProvisionVms.pumlshowing: netsetup-update → EnsureNixosIso → CreateVirtualMachine → qm guest-agent → scp config → ssh install.sh → reboot. Include innetsetup.adocviainclude::docs/NETSetupProvisionVms.puml[]. All narrative in caveman full per CLAUDE.md. Assertions: -
adocContent.Should().Contain("Proxmox VM provisioning")
-
adocContent.Should().Contain("nixos.iso")
-
adocContent.Should().Contain("NETSetupProvisionVms.puml")
-
pumlFileExists.Should().BeTrue()
-
pumlContent.Should().Contain("@startuml")
-
pumlContent.Should().Contain("EnsureNixosIso")
-
pumlContent.Should().Contain("qm guest")
-
pumlContent.Should().Contain("scp")
-
pumlContent.Should().Contain("install.sh")
-