feat: initial release of the scripts collection
Deploy / deploy (push) Successful in 17s
Test / test (push) Successful in 53s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-10 04:00:00 +00:00
commit 788cf0571f
27 changed files with 19257 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
# Architecture
How the scripts collection is put together. Every script, helper and arrow below
exists in the repository; nothing is aspirational.
## Overview
There is no shared library. Each script is one file that can be fetched and run on its
own, and the six of them share a skeleton rather than a module: the same command
runner, the same progress reporting, the same state checks before every action, and the
same shape of a run.
```mermaid
flowchart TD
Start[invocation] --> Args[parse_args reads ARGV]
Args --> Version{version or help?}
Version -->|yes| Print[print and exit]
Version -->|no| Root[check_root, detect_os]
Root --> Sections[sections run in order]
Sections --> Each[each section checks state first]
Each --> Done{changed?}
Done -->|no| Report[report what is already in place]
Done -->|yes| Act[act, then verify what was done]
Act --> Report
Report --> Summary[print_summary on stderr]
```
A run is idempotent because of that loop: a section reads the state of the system,
compares it with the desired one, and acts only on the difference, so a second run
reports everything as already in place and changes nothing.
## The seven scripts
| Script | Owns | Deliberately does not |
|---|---|---|
| `network-diag.pl` | Latency, loss, DNS timing, MTU discovery, dual-stack reachability, listening ports, and the grade of the connection | Touch any configuration; it only measures |
| `system-diag.pl` | Reading CPU, memory, disk, network, GPU, services, security and performance, and grading the result | Change anything on the host; it is read-only |
| `server-setup.pl` | Base packages, firewall (with the SSH rule verified before the service starts), SELinux, Podman, automatic updates | Install the services themselves; that is the operator's, and the deploy scripts' |
| `workstation-setup.pl` | A Fedora desktop: Brave, the official Go toolchain, Rust, GoLand, Flatpak applications, firewall, SELinux | Anything on a server distribution; it targets Fedora Workstation |
| `sglang-deploy.pl` | The SGLang deployment: the container engine, the systemd unit, nginx with TLS, the API key, the firewall rule and the SELinux boolean | The host's own setup, which `server-setup.pl` does first |
| `system-optimise.pl` | Old kernels (the running one and one fallback always stay), journals, temporary files, core dumps, and the package audit | Touch an rpm-ostree system, which it refuses |
## Data flow: a forked section collection
`system-diag.pl` is the one script whose shape is not linear. Its sections are
independent and dominated by waiting, so each runs in its own child process, writes its
result into a private scratch directory as a Perl literal, and the parent reads the
results back after reaping. The runner keeps stdout and stderr apart in that same
directory, which is why the file names carry the process id.
```mermaid
sequenceDiagram
participant Parent
participant Child as Section child
participant Disk as Scratch directory
Parent->>Child: fork, one per requested section
Child->>Child: collect one section
Child->>Disk: write the result as a Perl literal
Parent->>Parent: waitpid, report the section as it lands
Parent->>Disk: read_literal for each child
Parent->>Parent: compute the grade and render the report
Parent->>Disk: remove the scratch directory
```
Only the process that created the scratch directory removes it: a forked child inherits
the `END` block, so the cleanup is guarded by a parent pid comparison. A child that
cannot write its result is reported as a missing section, and the grade drops that
component rather than inventing a value for it.
## Conventions every script follows
- **Builtins only.** No module beyond the interpreter is loaded, because the systems
this targets package modules separately. What Perl has no builtin for is written out:
the command runner, RPM's version comparison, the JSON decoders and the encoder, the
IPv4 and IPv6 arithmetic, `sockaddr` packing, a `which`, and argument parsing.
- **External binaries as transport.** `dnf`, `rpm`, `curl`, `openssl`, `podman`,
`systemctl`, `journalctl`, `find`, `stat`, `sha256sum`, `tar`, `gpg`, `lspci` and the
rest are driven as argument lists through `run`, which keeps stdout and stderr apart,
forces `LANG=C` and `LC_ALL=C` for parseable output, and turns a timeout, a missing
binary and a failed exec into exit codes 124, 127 and 126 rather than exceptions.
- **One optional module.** `Time::HiRes` is loaded inside an `eval` in the scripts that
time something. Where it is absent the affected figures are reported as not measured
and the grade drops that component, rather than the script failing.
- **Atomic writes for system configuration.** A file that a boot depends on is written
to a temporary name, flushed, given its mode and owner, and renamed over the target.
Perl has no `fsync`, so that barrier is delegated to `sync` where the binary exists.
- **Errors carry what the tool said.** A failure reproduces the errno, the reason and
the path, so a message can be searched for as it stands.
## The SGLang deployment
`sglang-deploy.pl` is the only script that deploys a service, and its engine runs as a
container. That is a consequence of the material rather than a preference: SGLang
publishes no ROCm wheel, and its AMD install paths are the project's container images
or a source build against a full ROCm toolchain that Fedora carries only partly and
that CentOS Stream and openEuler cannot carry at all.
```mermaid
flowchart LR
Client[client] -->|443| Nginx[nginx on the host, TLS]
Nginx -->|loopback, plain HTTP| Engine[SGLang in a Podman container]
Engine -->|device nodes| GPU[/dev/kfd, /dev/dri]
Engine -->|bind mount| Cache[state directory, model cache]
Unit[systemd unit] -->|podman run| Engine
EnvFile[EnvironmentFile, mode 0600] -->|API key| Unit
```
The host therefore needs the amdgpu kernel driver and its device nodes, never a ROCm
userland. The image tag is resolved from the card's family (mi30x for an MI300 or
MI325, mi35x for an MI350 or MI355) and from the host's ROCm, because the container's
ROCm userland must not be newer than the host's kernel driver. A Radeon 8060S or 8050S
(gfx1151) has no stable tag anywhere; AMD's dated development builds are resolved
instead, and `--image` pins one. Radeon cards need `SGLANG_USE_AITER=false` and
`SGLANG_ROCM_FUSED_DECODE_MLA=false` in the unit, which the script writes for them and
never for an Instinct host.
## Dependencies
Nothing outside the interpreter, and nothing that has to be installed beyond the tools
each script's own dependency section installs. The non-obvious ones and their reasons:
- `podman` for `sglang-deploy.pl`, because the engine is a container.
- `lspci` for the GPU family, and `rocm-smi` in `system-diag.pl` for AMD memory and
utilisation figures.
- `sha256sum` in `workstation-setup.pl`, because Perl's builtins have no hash.
- `sync` for the write barrier after an atomic write.
- `getent` or `host` in `network-diag.pl` as the resolver transport, since `getent` on
FreeBSD implements neither of the `ahosts` databases.