From b9424ff9440210f10e3f019be55ebbb3fd3af952 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 4 Jun 2025 22:02:58 +0200 Subject: [PATCH] Added PXE Scripts and Typst Script --- netboot-files/angestoepselt.ipxe | 74 ++ netboot-files/menu.ipxe | 126 +++ preseed/preseed.cfg | 12 +- setup.sh | 2 +- tools/angestoepselt_typst/.envrc | 1 + tools/angestoepselt_typst/.python-version | 1 + tools/angestoepselt_typst/Dockerfile | 44 + tools/angestoepselt_typst/Makefile | 2 + tools/angestoepselt_typst/flake.lock | 27 + tools/angestoepselt_typst/flake.nix | 34 + tools/angestoepselt_typst/pyproject.toml | 22 + tools/angestoepselt_typst/src/__init__.py | 105 +++ tools/angestoepselt_typst/src/logo_stack.png | Bin 0 -> 5017 bytes tools/angestoepselt_typst/src/main.typ | 55 ++ tools/angestoepselt_typst/typst/computer.json | 8 + tools/angestoepselt_typst/typst/main.py | 27 + tools/angestoepselt_typst/uv.lock | 765 ++++++++++++++++++ 17 files changed, 1293 insertions(+), 12 deletions(-) create mode 100644 netboot-files/angestoepselt.ipxe create mode 100644 netboot-files/menu.ipxe create mode 100644 tools/angestoepselt_typst/.envrc create mode 100644 tools/angestoepselt_typst/.python-version create mode 100644 tools/angestoepselt_typst/Dockerfile create mode 100644 tools/angestoepselt_typst/Makefile create mode 100644 tools/angestoepselt_typst/flake.lock create mode 100644 tools/angestoepselt_typst/flake.nix create mode 100644 tools/angestoepselt_typst/pyproject.toml create mode 100644 tools/angestoepselt_typst/src/__init__.py create mode 100644 tools/angestoepselt_typst/src/logo_stack.png create mode 100644 tools/angestoepselt_typst/src/main.typ create mode 100644 tools/angestoepselt_typst/typst/computer.json create mode 100755 tools/angestoepselt_typst/typst/main.py create mode 100644 tools/angestoepselt_typst/uv.lock diff --git a/netboot-files/angestoepselt.ipxe b/netboot-files/angestoepselt.ipxe new file mode 100644 index 0000000..2325e6c --- /dev/null +++ b/netboot-files/angestoepselt.ipxe @@ -0,0 +1,74 @@ +#!ipxe + +set timeout 10000 + +:menu +menu Network boot options for ${uuid} +item --key a default Try to boot (a)ll network adapters in turn +item +#item --gap -- --- Detected network adapters --- +#set i:int8 0 +#:loop +#ifopen net${i} && item --key ${i} net${i} net(${i}): ${netX/mac} - ${netX/bustype} ${netX/busloc:busdevfn} ${pci/${netX/busloc}.0.2}:${pci/${netX/busloc}.2.2} ${netX/chip} ; ifclose +#inc i +#iseq ${i} 10 || goto loop +item +item --gap -- --- Alternatives --- +item --key c config Open (c)onfiguration +item --key r reboot (R)eboot computer +item --key s shell Drop to iPXE (s)hell +item --key x exit E(x)it and continue BIOS boot order +item --key a angestoepselt (a)ngestoepselt Iso Boot +choose --timeout ${timeout} selected && goto select || goto default +goto menu + +:select +isset ${${selected}/mac} && goto nic || goto label + +:angestoepselt +imgfree +set mirror http://deb.debian.org +set dir debian/dists/bookworm/main/installer-amd64/current/images/netboot/debian-installer/amd64 +set mirrorcfg mirror/suite=bookworm +set install_params auto=true priority=critical preseed/url=http://10.200.4.12:8000/preseed.cfg +echo kernel ${mirror}/${dir}/linux ${install_params} ${mirrorcfg} -- quiet ${params} initrd=initrd.magic ${cmdline} +kernel ${mirror}/${dir}/linux ${install_params} ${mirrorcfg} -- quiet ${params} initrd=initrd.magic ${cmdline} +initrd ${mirror}/${dir}/initrd.gz +boot + +:nic +autoboot ${selected} && goto exit || +echo Booting '${selected}' failed, exiting iPXE... +goto exit + +:label +goto ${selected} || +echo The label '${selected}' could not be found, returning to menu... +sleep 2 +goto restart + +:default +autoboot && goto exit || +echo Booting failed, exiting iPXE... +goto exit + +:config +config +goto restart + +:shell +shell +goto restart + +:restart +set timeout 0 +goto menu + +:reboot +reboot + +:exit +echo Continuing BIOS boot order... +sleep 1 +exit + diff --git a/netboot-files/menu.ipxe b/netboot-files/menu.ipxe new file mode 100644 index 0000000..5377b96 --- /dev/null +++ b/netboot-files/menu.ipxe @@ -0,0 +1,126 @@ +#!ipxe + +:start +isset ${arch} && goto skip_arch_detect || +cpuid --ext 29 && set arch x86_64 || set arch i386 +iseq ${buildarch} arm64 && set arch arm64 || +:skip_arch_detect +chain --autofree boot.cfg || +echo Attempting to retrieve latest upstream version number... +chain --timeout 5000 https://boot.netboot.xyz/version.ipxe || +ntp 0.pool.ntp.org || +iseq ${cls} serial && goto ignore_cls || +set cls:hex 1b:5b:4a # ANSI clear screen sequence - "^[[J" +set cls ${cls:string} +:ignore_cls + + +isset ${menu} && goto ${menu} || +isset ${ip} || dhcp + +:main_menu +clear menu +set space:hex 20:20 +set space ${space:string} +isset ${next-server} && menu ${site_name} v${version} - next-server: ${next-server} || menu ${site_name} + + +item --gap Angestoepselt: +item angestoepselt ${space} Boot angestoepselt ISO +item angestoepselt2 ${space} Eigenes IPXE +item --gap Default: +item local ${space} Boot from local hdd +item --gap Distributions: +iseq ${menu_linux} 1 && item linux ${space} Linux Network Installs (64-bit) || +iseq ${menu_linux_i386} 1 && item linux-i386 ${space} Linux Network Installs (32-bit) || +iseq ${menu_linux_arm} 1 && item linux-arm ${space} Linux Network Installs (arm64) || +iseq ${menu_live} 1 && item live ${space} Live CDs || +iseq ${menu_live_arm} 1 && item live-arm ${space} Live CDs || +iseq ${menu_bsd} 1 && item bsd ${space} BSD Installs || +iseq ${menu_unix} 1 && item unix ${space} Unix Network Installs || +iseq ${menu_freedos} 1 && item freedos ${space} FreeDOS || +iseq ${menu_windows} 1 && item windows ${space} Windows || +item --gap Tools: +iseq ${menu_utils} 1 && iseq ${platform} efi && item utils-efi ${space} Utilities (UEFI) || +iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} x86_64 && item utils-pcbios-64 ${space} Utilities (64-bit) || +iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} i386 && item utils-pcbios-32 ${space} Utilities (32-bit) || +iseq ${menu_utils_arm} 1 && item utils-arm ${space} Utilities (arm64) || +item change_arch ${space} Architecture: ${arch} +item shell ${space} iPXE shell +item netinfo ${space} Network card info +iseq ${menu_pci} 1 && item lspci ${space} PCI Device List || +item about ${space} About netboot.xyz +item --gap Signature Checks: +item sig_check ${space} netboot.xyz [ enabled: ${sigs_enabled} ] +isset ${github_user} && item --gap Custom Github Menu: || +isset ${github_user} && item custom-github ${space} ${github_user}'s Custom Menu || +isset ${custom_url} && item --gap Custom URL Menu: || +isset ${custom_url} && item custom-url ${space} Custom URL Menu || +isset ${menu} && set timeout 0 || set timeout ${boot_timeout} +choose --timeout ${timeout} --default ${menu} menu || goto local +echo ${cls} +goto ${menu} || +iseq ${sigs_enabled} true && goto verify_sigs || goto change_menu + +:verify_sigs +imgverify ${menu}.ipxe ${sigs}${menu}.ipxe.sig || goto error +goto change_menu + +:change_menu +chain ${menu}.ipxe || goto error +goto main_menu + +:error +echo Error occurred, press any key to return to menu ... +prompt +goto main_menu + +:angestoepselt +imgfree +set mirror http://deb.debian.org +set dir debian/dists/bookworm/main/installer-amd64/current/images/netboot/debian-installer/amd64 +set mirrorcfg mirror/suite=bookworm +echo kernel ${mirror}/${dir}/linux ${install_params} ${mirrorcfg} -- quiet ${params} initrd=initrd.magic ${cmdline} +kernel ${mirror}/${dir}/linux ${install_params} ${mirrorcfg} -- quiet ${params} initrd=initrd.magic ${cmdline} +initrd ${mirror}/${dir}/initrd.gz +boot + +:angestoepselt2 +chain angestoepselt.ipxe || goto error +goto linux_menu + +:local +echo Booting from local disks ... +exit 1 + +:shell +echo Type "exit" to return to menu. +set menu main_menu +shell +goto main_menu + +:change_arch +iseq ${arch} x86_64 && set arch i386 && set menu_linux_i386 1 && set menu_linux 0 && goto main_menu || +iseq ${arch} i386 && set arch x86_64 && set menu_linux_i386 0 && set menu_linux 1 && goto main_menu || +goto main_menu + +:sig_check +iseq ${sigs_enabled} true && set sigs_enabled false || set sigs_enabled true +goto main_menu + +:about +chain https://boot.netboot.xyz/about.ipxe || chain about.ipxe +goto main_menu + +:custom-github +chain https://raw.githubusercontent.com/${github_user}/netboot.xyz-custom/master/custom.ipxe || goto error +goto main_menu + +:custom-url +chain ${custom_url}/custom.ipxe || goto error +goto main_menu + +:custom-user +chain custom/custom.ipxe +goto main_menu + diff --git a/preseed/preseed.cfg b/preseed/preseed.cfg index c53ccd2..a653b24 100755 --- a/preseed/preseed.cfg +++ b/preseed/preseed.cfg @@ -100,14 +100,4 @@ d-i netcfg/choose_interface select auto d-i netcfg/hostname string computerspende d-i preseed/late_command string \ - in-target --pass-stdout bash -c "echo 'computerspende ALL=NOPASSWD:ALL' > /etc/sudoers.d/computerspende"; \ - cp /cdrom/files/late_command.sh /target/home/computerspende/late_command.sh; \ - in-target bash -c 'mkdir -p /home/computerspende/.config/autostart'; \ - in-target bash -c 'echo "[Desktop Entry]" > /home/computerspende/.config/autostart/post_hardware.desktop'; \ - in-target bash -c 'echo "Type=Application" >> /home/computerspende/.config/autostart/post_hardware.desktop'; \ - in-target bash -c 'echo "Terminal=true" >> /home/computerspende/.config/autostart/post_hardware.desktop'; \ - in-target bash -c 'echo "Exec=gnome-terminal -- /home/computerspende/post_hardware.sh" >> /home/username/.config/autostart/post_hardware.desktop'; \ - in-target bash -c 'echo "Name=Post Hardware Setup" >> /home/computerspende/.config/autostart/post_hardware.desktop'; \ - in-target bash -c 'chown computerspende:computerspende /home/computerspende/post_hardware.sh'; \ - in-target bash -c 'chown -R computerspende:computerspende /home/computerspende/.config' - + in-target --pass-stdout bash -c "echo 'computerspende ALL=NOPASSWD:ALL' > /etc/sudoers.d/computerspende"; diff --git a/setup.sh b/setup.sh index 57a979b..9c0c132 100644 --- a/setup.sh +++ b/setup.sh @@ -3,7 +3,7 @@ # Change to the script's directory cd "$(dirname "$0")" -ISO_URL="https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd/debian-12.9.0-amd64-DVD-1.iso" +ISO_URL="https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.11.0-amd64-netinst.iso" IMAGE_DIR="images" ISO_NAME="$IMAGE_DIR/debian-server.iso" SOURCE_DIR="source" diff --git a/tools/angestoepselt_typst/.envrc b/tools/angestoepselt_typst/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/tools/angestoepselt_typst/.envrc @@ -0,0 +1 @@ +use flake diff --git a/tools/angestoepselt_typst/.python-version b/tools/angestoepselt_typst/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/tools/angestoepselt_typst/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/tools/angestoepselt_typst/Dockerfile b/tools/angestoepselt_typst/Dockerfile new file mode 100644 index 0000000..d05eef2 --- /dev/null +++ b/tools/angestoepselt_typst/Dockerfile @@ -0,0 +1,44 @@ +# An example of using standalone Python builds with multistage images. + +# First, build the application in the `/app` directory +FROM ghcr.io/astral-sh/uv:bookworm-slim AS builder +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy + +# Configure the Python directory so it is consistent +ENV UV_PYTHON_INSTALL_DIR=/python + +# Only use the managed Python version +ENV UV_PYTHON_PREFERENCE=only-managed + +# Install Python before the project for caching +RUN uv python install 3.12 + +WORKDIR /app +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-dev +ADD . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +# Then, use a final image without uv +# FROM debian:bookworm-slim +FROM --platform=$BUILDPLATFORM ubuntu AS build +RUN apt update +RUN apt install -y ca-certificates +# Copy the Python version +COPY --from=builder --chown=python:python /python /python + +# Copy the application from the builder +COPY --from=builder --chown=app:app /app/src /app/src +COPY --from=builder --chown=app:app /app/.venv /app/.venv + +# Place executables in the environment at the front of the path +ENV PATH="/app/.venv/bin:$PATH" +WORKDIR /app +# Run the FastAPI application by default +CMD ["uvicorn", "--host", "0.0.0.0", "src:app"] + +# LABEL ogr.opencontainer.image.url="https://github.com/slashformotion/typst-http-api/pkgs/container/typst-http-api" +# LABEL org.opencontainers.image.licenses="MIT" diff --git a/tools/angestoepselt_typst/Makefile b/tools/angestoepselt_typst/Makefile new file mode 100644 index 0000000..e96e264 --- /dev/null +++ b/tools/angestoepselt_typst/Makefile @@ -0,0 +1,2 @@ +run: + uv run fastapi dev typst-http-api/app.py diff --git a/tools/angestoepselt_typst/flake.lock b/tools/angestoepselt_typst/flake.lock new file mode 100644 index 0000000..af4a614 --- /dev/null +++ b/tools/angestoepselt_typst/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1743797514, + "narHash": "sha256-i10Tcclcpqtd7sT+CXp9S8mBGsunR5fsRwu/37DluoA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e39c2791369b362637649eb62dfd7fcdd253fbfb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/tools/angestoepselt_typst/flake.nix b/tools/angestoepselt_typst/flake.nix new file mode 100644 index 0000000..5bd5de8 --- /dev/null +++ b/tools/angestoepselt_typst/flake.nix @@ -0,0 +1,34 @@ +{ + description = ""; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/master"; + }; + + outputs = { + self, + nixpkgs, + }: let + forAllSystems = function: + nixpkgs.lib.genAttrs [ + "x86_64-darwin" + "x86_64-linux" + "aarch64-darwin" + "aarch64-linux" + ] (system: function nixpkgs.legacyPackages.${system}); + in { + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + # nativeBuildInputs is usually what you want -- tools you need to run + nativeBuildInputs = with pkgs; [ + gnumake + + uv + python3 + ruff + pre-commit + ]; + }; + }); + }; +} diff --git a/tools/angestoepselt_typst/pyproject.toml b/tools/angestoepselt_typst/pyproject.toml new file mode 100644 index 0000000..b76170e --- /dev/null +++ b/tools/angestoepselt_typst/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "typst-as-a-service" +version = "0.1.0" +description = "Typst as a service." +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi[standard]>=0.115.12", + "prometheus-fastapi-instrumentator>=7.1.0", + "pydantic>=2.11.2", + "slowapi>=0.1.9", + "typst>=0.13.2", + "uvicorn>=0.34.0", +] + +[dependency-groups] +dev = [ + "ruff>=0.11.4", +] + + + diff --git a/tools/angestoepselt_typst/src/__init__.py b/tools/angestoepselt_typst/src/__init__.py new file mode 100644 index 0000000..0cb921a --- /dev/null +++ b/tools/angestoepselt_typst/src/__init__.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import logging +import typst +from fastapi import FastAPI, Request, Response, status +from fastapi.responses import StreamingResponse +from prometheus_fastapi_instrumentator import Instrumentator +from pydantic import BaseModel +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.util import get_remote_address +import os +import json + +DEFAULT_CHUNK_SIZE = 1024 +REQUEST_PER_MINUTES = os.getenv("TYPST_HTTP_API_REQUESTS_PER_MINUTES") + + +logger = logging.getLogger(__name__) + +app = FastAPI(docs_url=None, redoc_url=None) + +limiter = Limiter(key_func=get_remote_address) +if REQUEST_PER_MINUTES is not None: + app.state.limiter = limiter + +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + + +Instrumentator().instrument(app).expose(app) + + +class CompilationError(BaseModel): + reason: str + content: str + +@app.post("/label") +@limiter.limit(f"{REQUEST_PER_MINUTES}/minute") +async def build(request: Request, response: Response): + typst_bytes = await request.body() + typst_string = typst_bytes.decode("utf-8") + logger.info(f"Data Body {typst_bytes}") + j = json.loads(typst_string) + data = {"computer": json.dumps(j)} + try: + res = typst.compile(input="/app/src/main.typ", sys_inputs=data) + logger.info(f"successfully build {len(typst_bytes)}") + except RuntimeError as e: + response.status_code = status.HTTP_422_UNPROCESSABLE_ENTITY + logger.error(f"failed to build {len(typst_bytes)}") + return CompilationError(reason="Compilation error", content=str(e)) + + def iterfile( + input_bytes: bytes, + chunk_size: int = DEFAULT_CHUNK_SIZE, + ): + for i in range(0, len(input_bytes), chunk_size): + yield input_bytes[i : i + chunk_size] + + return StreamingResponse(iterfile(res), media_type="application/pdf") + +@app.post("/l") +@limiter.limit(f"{REQUEST_PER_MINUTES}/minute") +async def build(request: Request, response: Response): + typst_bytes = await request.body() + logger.info(f"tpyst_bytes {len(typst_bytes)}") + logger.info(f"tpyst_bytes {type(typst_bytes)}") + try: + res = typst.compile(typst_bytes) + logger.info(f"successfully build {len(typst_bytes)}") + except RuntimeError as e: + response.status_code = status.HTTP_422_UNPROCESSABLE_ENTITY + logger.error(f"failed to build {len(typst_bytes)}") + return CompilationError(reason="Compilation error", content=str(e)) + + def iterfile( + input_bytes: bytes, + chunk_size: int = DEFAULT_CHUNK_SIZE, + ): + for i in range(0, len(input_bytes), chunk_size): + yield input_bytes[i : i + chunk_size] + + + +@app.post("/") +@limiter.limit(f"{REQUEST_PER_MINUTES}/minute") +async def build(request: Request, response: Response): + typst_bytes = await request.body() + + try: + res = typst.compile(typst_bytes) + logger.info(f"successfully build {len(typst_bytes)}") + except RuntimeError as e: + response.status_code = status.HTTP_422_UNPROCESSABLE_ENTITY + logger.error(f"failed to build {len(typst_bytes)}") + return CompilationError(reason="Compilation error", content=str(e)) + + def iterfile( + input_bytes: bytes, + chunk_size: int = DEFAULT_CHUNK_SIZE, + ): + for i in range(0, len(input_bytes), chunk_size): + yield input_bytes[i : i + chunk_size] + + return StreamingResponse(iterfile(res), media_type="application/pdf") diff --git a/tools/angestoepselt_typst/src/logo_stack.png b/tools/angestoepselt_typst/src/logo_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..4328b5bd4181fcd27cd3a3a2419f0938694a2483 GIT binary patch literal 5017 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 j_+&ecbBQp75JLPPQIHV)wE