diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..cc676f3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,125 @@ +name: Build & Test + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +jobs: + x86_64: + name: ExampleOS x86_64 + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Free Disk Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: false + + - name: Install Build Dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + bc binutils build-essential bzip2 cpio \ + diffutils file findutils git gzip \ + libncurses-dev libssl-dev perl patch \ + python3 rsync sed tar unzip wget \ + autopoint bison flex autoconf automake \ + mtools + + - name: Install asciidoctor-pdf + run: sudo gem install --no-document asciidoctor-pdf + + # Full history, the OS version comes from `git describe` here. + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + # Keeps codeload.github.com from rate limiting the build. + - name: Restore Downloads + uses: actions/cache/restore@v5 + with: + path: dl/ + key: dl-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: dl- + + - name: Restore ccache + uses: actions/cache/restore@v5 + with: + path: .ccache/ + key: ccache-${{ github.run_id }}-${{ github.run_attempt }} + restore-keys: ccache- + + - name: Configure x86_64 + run: make x86_64_defconfig + + - name: Build x86_64 + run: make + + - name: Test x86_64 + run: make test + + - name: Publish Test Result + if: always() + run: cat infix/test/.log/last/result-gh.md >> $GITHUB_STEP_SUMMARY + + - name: Test Report x86_64 + if: always() + run: make test-report + + - name: Upload Test Report + if: always() + uses: actions/upload-artifact@v7 + with: + name: test-report + path: output/images/test-report.pdf + retention-days: 7 + + # Also on failure, so a retry keeps what did get fetched. + - name: Save Downloads + if: always() + uses: actions/cache/save@v5 + with: + path: dl/ + key: dl-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Save ccache + if: always() + uses: actions/cache/save@v5 + with: + path: .ccache/ + key: ccache-${{ github.run_id }}-${{ github.run_attempt }} + + # Just what `make run` boots, on demand, to stay inside the free + # artifact quota. + - name: Upload Image + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v7 + with: + name: example-os-x86_64 + path: | + output/images/*.qcow2 + output/images/OVMF.fd + retention-days: 7 + + # Artifacts expire after 90 days at most, release assets never do. + - name: Release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" --generate-notes \ + output/images/*.qcow2 \ + output/images/OVMF.fd \ + output/images/test-report.pdf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90542ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dl/ +output/ +.ccache/ +__pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9a5bc82 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "infix"] + path = infix + url = https://github.com/kernelkit/infix.git diff --git a/Config.in b/Config.in new file mode 100644 index 0000000..a4d1924 --- /dev/null +++ b/Config.in @@ -0,0 +1 @@ +source "$BR2_EXTERNAL_EXAMPLE_PATH/package/Config.in" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..596e21c --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +# Build the spin by handing over to Buildroot with two br2-externals +# stacked: Infix first, then this repo (branding, overlay, packages). +export BR2_EXTERNAL := $(CURDIR)/infix:$(CURDIR) + +# Run our tests instead of Infix's, using Infix's test harness, see test/ +export TESTS := $(CURDIR)/test/case/all.yaml +export NINEPM_PROJ_CONFIG := $(CURDIR)/test/9pm-proj.yaml + +# Brand the generated test report with our logo instead of the Infix one. +# The bracket suffix is an Asciidoctor image spec, tweak to taste. +export LOGO := $(CURDIR)/doc/logo.png[top=40%, align=right, pdfwidth=10cm] + +O ?= $(CURDIR)/output +config := $(O)/.config +bmake = $(MAKE) -C infix/buildroot O=$(O) $1 +imake = $(MAKE) -C infix O=$(O) $1 + +all: $(config) infix/buildroot/Makefile + @+$(call bmake,$@) + +$(config): + @+$(call bmake,list-defconfigs) + @echo "ERROR: No configuration selected." + @echo "Please choose a configuration from the list above by running" + @echo "'make _defconfig' before building an image." + @exit 1 + +%_defconfig: configs/%_defconfig + @+$(call bmake,$@) + +# Boot the image in QEMU and run $(TESTS) against it. Needs Docker or +# Podman; the harness pulls a container with QEMU and the test tooling. +# Explicit rule, since the catch-all below would find the test/ directory +# and think there is nothing to do. +test: | infix/buildroot/Makefile + @+$(call imake,$@) + +# Render the result of the last `make test` as output/images/test-report.pdf. +# Needs asciidoctor-pdf: gem install asciidoctor-pdf +test-report: | infix/buildroot/Makefile + @+$(call imake,$@) + +# Any other target (menuconfig, clean, migrate-configs, ...) is passed to +# Infix, which handles its own targets and forwards the rest to Buildroot. +%: | infix/buildroot/Makefile + @+$(call imake,$@) + +infix/buildroot/Makefile: + @git submodule update --init --recursive + +# configs/*.base. YES=1 follows Infix on everything +sync = ./infix/utils/update-defconfig.sh $(if $(YES),-y) + +# Regenerate configs/ +defconfigs-update: + @$(sync) infix/configs/x86_64_defconfig configs/x86_64_defconfig + +.PHONY: all test test-report defconfigs-generate diff --git a/README.md b/README.md index ecf5ea6..9de97dd 100644 --- a/README.md +++ b/README.md @@ -1 +1,146 @@ -# infix-spin-example \ No newline at end of file +

+ + + ExampleOS -- an Infix spin + +

+ +This is a *spin* of [Infix OS](https://github.com/kernelkit/infix) a +tiny skeleton repo showing how to make Infix your own without forking +it. Infix is included as a submodule; this repo only adds a thin layer +on top: + + * **Branding** — OS name, vendor, hostname, and boot tagline, all in + `config-snippets/` + * **Packages** — an example `hello-world` daemon, built from source in + this repo and supervised by finit (`package/`, `src/`) + * **Models** — a YANG model for that daemon, which is all it takes to + get it into the CLI, NETCONF, and RESTCONF (`yang/`) + * **Files** — a rootfs overlay for service settings, a CLI snippet, and + a factory default (`board/common/rootfs/`) + * **Tests** — a regression test for the daemon, run in QEMU by Infix's + test harness (`test/`) + +When a new Infix release comes out, you bump the submodule and rebuild. +No fork, no drift, no merge conflicts. Clone it, rename a few things, +and you have your own OS. + +## Get Started + +Install the build dependencies listed in the [Infix Developer's +Guide][building], for the Infix release this spin is based on, then +clone the repository along with its submodules: + +```bash +git clone --recurse-submodules https://github.com/kernelkit/infix-spin-example.git +cd infix-spin-example +``` + +Configure and build. The first build takes a while, since it compiles +the cross toolchain and the Linux kernel too: + +```bash +make x86_64_defconfig +make +``` + +Then boot your new OS in QEMU: + +```bash +sudo apt install qemu-system-x86 +make run +``` + +Log in as `admin` with password `admin`, and have a look at the example +daemon, which greets you in the system log once a minute: + +``` +example-os:~$ initctl status hello-world +example-os:~$ sudo grep hello-world /var/log/syslog +``` + +That's it. Have a look around and make it your own. + +## A Model of Your Own + +The daemon is configured the way everything in Infix is: with YANG. The +model lives in `yang/example-hello@2026-08-26.yang` and is a container +with two leaves — `audience`, which you set, and `greetings`, which the +daemon reports. It is loaded into sysrepo when the image is built, see +`package/hello-world/hello-world.mk`. + +That is the entire integration. There is no CLI code for it, because +configure mode is generated from whatever models sysrepo has loaded: + +``` +example-os:~$ configure +example-os:/config/> set hello audience there +example-os:/config/> leave +example-os:~$ show hello +audience: there +greetings: 4 +``` + +Tab completion, help texts, validation, `show`, `diff`, saving to +startup-config — all of it follows from the model. The same data is +reachable over NETCONF and RESTCONF, again for free. + +Only the last command needed writing: `show` in operational mode is an +ordinary CLI command, so the spin drops a klish snippet into +`board/common/rootfs/etc/klish/hello.xml` to add it. + +A `default` in the model is not the same as a starting configuration. +The daemon reads `world` whether or not anything is set, but a leaf that +still holds its default is left out when the datastore is serialized, so +`show hello` prints nothing for it. To make it visible, ship it as real +configuration: Infix collates the factory config from +`/usr/share/confd/factory.d/`, so the overlay drops a `50-hello.json` +there, and symlinks it into `test.d/` beside it, which is where the +image boots from under the test harness. Anything a spin wants in a +factory fresh configuration goes the same way. + +On the daemon side, two sysrepo subscriptions are all that is needed — +one to hear about `audience` changing, one to answer for `greetings`. +See `src/hello-world/hello-world.c`, it is about thirty lines of it. + +## Test + +Infix comes with a test harness that boots the image in QEMU and pokes +at it over SSH and NETCONF. A spin can reuse it for its own tests; this +one has a single case that checks the `hello-world` daemon, see +`test/case/hello-world/`: + +```bash +make test +``` + +Docker or Podman is required, the harness runs inside a container that +brings its own QEMU and Python tooling. Tests describe the *logical* +topology they need (`topology.dot`) and the harness maps it onto the +virtual network it starts, so nothing here depends on real hardware. + +The result of the last run can be rendered as a PDF, branded with +`doc/logo.png` (swap it for your own, and see `LOGO` in the Makefile if +you want it placed differently): + +```bash +gem install asciidoctor-pdf +make test-report # -> output/images/test-report.pdf +``` + +The same commands run in CI, on a stock GitHub runner, see +`.github/workflows/build.yml`. Every run uploads the report; a bootable +image is a few hundred MB, more than a free account's artifact storage +will take on every push, so that one is built on demand with the "Run +workflow" button. Push a `v*` tag and both are attached to a GitHub +release instead, where they are kept indefinitely. + +## Learn More + + * [Infix documentation](https://kernelkit.org/) + * [RFC 7950](https://www.rfc-editor.org/rfc/rfc7950), the YANG 1.1 + language + * [Buildroot br2-external manual](https://buildroot.org/downloads/manual/manual.html#outside-br-custom) + * [finit](https://github.com/troglobit/finit) + +[building]: https://www.kernelkit.org/infix/26.06/developers-guide/#building diff --git a/board/common/rootfs/etc/finit.d/available/hello-world.conf b/board/common/rootfs/etc/finit.d/available/hello-world.conf new file mode 100644 index 0000000..2807478 --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/hello-world.conf @@ -0,0 +1,7 @@ +# Hello World example daemon. Runs in runlevels 2-5. The conditions +# delay start until syslogd is up, since the daemon logs there, and until +# confd has populated sysrepo, since that is where it reads its +# configuration from. Finit supervises it and restarts it if it crashes. +# It is enabled by the symlink in ../enabled/ -- on a running system you +# can also use `initctl enable hello-world`. +service [2345] /usr/sbin/hello-world -- Hello World example daemon diff --git a/board/common/rootfs/etc/finit.d/enabled/hello-world.conf b/board/common/rootfs/etc/finit.d/enabled/hello-world.conf new file mode 120000 index 0000000..2147c7b --- /dev/null +++ b/board/common/rootfs/etc/finit.d/enabled/hello-world.conf @@ -0,0 +1 @@ +../available/hello-world.conf \ No newline at end of file diff --git a/board/common/rootfs/etc/klish/hello.xml b/board/common/rootfs/etc/klish/hello.xml new file mode 100644 index 0000000..68a07f9 --- /dev/null +++ b/board/common/rootfs/etc/klish/hello.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + copy operational -x /example-hello:hello | jq -r '.["example-hello:hello"] + | "audience: \(.audience)", "greetings: \(.greetings)"' + + + + + + + diff --git a/board/common/rootfs/usr/share/confd/factory.d/50-hello.json b/board/common/rootfs/usr/share/confd/factory.d/50-hello.json new file mode 100644 index 0000000..e3b00c1 --- /dev/null +++ b/board/common/rootfs/usr/share/confd/factory.d/50-hello.json @@ -0,0 +1,5 @@ +{ + "example-hello:hello": { + "audience": "world" + } +} diff --git a/board/common/rootfs/usr/share/confd/test.d/50-hello.json b/board/common/rootfs/usr/share/confd/test.d/50-hello.json new file mode 120000 index 0000000..b9f2972 --- /dev/null +++ b/board/common/rootfs/usr/share/confd/test.d/50-hello.json @@ -0,0 +1 @@ +../factory.d/50-hello.json \ No newline at end of file diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig new file mode 100644 index 0000000..c33946a --- /dev/null +++ b/configs/x86_64_defconfig @@ -0,0 +1,202 @@ +BR2_x86_64=y +BR2_x86_corei7=y +BR2_TOOLCHAIN_EXTERNAL=y +BR2_TOOLCHAIN_EXTERNAL_GDB_SERVER_COPY=y +BR2_WGET="wget -nd -t 5 --waitretry=30 --retry-on-http-error=429,500,502,503,504 --connect-timeout=10" +BR2_DL_DIR="${BR2_EXTERNAL_EXAMPLE_PATH}/dl" +BR2_CCACHE=y +BR2_CCACHE_DIR="${BR2_EXTERNAL_EXAMPLE_PATH}/.ccache" +BR2_ENABLE_DEBUG=y +BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches" +BR2_DOWNLOAD_FORCE_CHECK_HASHES=y +BR2_TARGET_GENERIC_HOSTNAME="example-os" +BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit" +BR2_INIT_FINIT=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y +BR2_ROOTFS_DEVICE_TABLE="system/device_table.txt ${BR2_EXTERNAL_INFIX_PATH}/board/common/xattrs" +BR2_ROOTFS_MERGED_USR=y +# BR2_TARGET_ENABLE_ROOT_LOGIN is not set +BR2_TARGET_GENERIC_GETTY_PORT="@console" +BR2_TARGET_GENERIC_GETTY_TERM="xterm" +BR2_SYSTEM_DHCP="eth0" +BR2_ENABLE_LOCALE_WHITELIST="C en_US en_CA C.UTF-8" +BR2_GENERATE_LOCALE="en_US en_CA C.UTF-8" +BR2_TARGET_TZ_INFO=y +BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/rootfs ${BR2_EXTERNAL_EXAMPLE_PATH}/board/common/rootfs" +BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh" +BR2_LINUX_KERNEL=y +BR2_LINUX_KERNEL_CUSTOM_VERSION=y +BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.18.56" +BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y +BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig" +BR2_LINUX_KERNEL_INSTALL_TARGET=y +BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y +BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_STRACE=y +BR2_PACKAGE_STRESS_NG=y +BR2_PACKAGE_JQ=y +BR2_PACKAGE_E2FSPROGS=y +BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y +BR2_PACKAGE_DBUS_CXX=y +BR2_PACKAGE_DBUS_GLIB=y +BR2_PACKAGE_EUDEV_RULES_GEN=y +# BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPTFDISK=y +BR2_PACKAGE_GPTFDISK_SGDISK=y +BR2_PACKAGE_PCIUTILS=y +BR2_PACKAGE_RNG_TOOLS=y +BR2_PACKAGE_UBOOT_TOOLS=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y +BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y +BR2_PACKAGE_LIBSSH2=y +BR2_PACKAGE_LIBSSH2_OPENSSL=y +BR2_PACKAGE_LIBOPENSSL_BIN=y +BR2_PACKAGE_LIBINPUT=y +BR2_PACKAGE_LIBCURL_CURL=y +BR2_PACKAGE_LIBNETCONF2_SSH_SUBSYSTEM=y +BR2_PACKAGE_NSS_MDNS=y +BR2_PACKAGE_SYSREPO_GROUP="sysrepo" +BR2_PACKAGE_LINUX_PAM=y +BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y +BR2_PACKAGE_ONIGURUMA=y +BR2_PACKAGE_AVAHI_DAEMON=y +BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y +BR2_PACKAGE_CHRONY=y +BR2_PACKAGE_CONNTRACK_TOOLS=y +BR2_PACKAGE_DNSMASQ=y +BR2_PACKAGE_ETHTOOL=y +BR2_PACKAGE_FPING=y +BR2_PACKAGE_FRR=y +# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set +BR2_PACKAGE_IPERF3=y +BR2_PACKAGE_IPROUTE2=y +BR2_PACKAGE_IPTABLES_NFTABLES=y +BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y +BR2_PACKAGE_LLDPD=y +BR2_PACKAGE_MSTPD=y +BR2_PACKAGE_MTR=y +BR2_PACKAGE_NETCALC=y +BR2_PACKAGE_NETCAT_OPENBSD=y +BR2_PACKAGE_NETSNMP=y +# BR2_PACKAGE_NETSNMP_ENABLE_MIBS is not set +BR2_PACKAGE_NETSNMP_WITH_MIB_MODULES="host if-mib ucd-snmp/loadave ucd-snmp/memory ucd-snmp/disk_hw agentx" +BR2_PACKAGE_NGINX=y +BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y +BR2_PACKAGE_NGINX_HTTP_V2_MODULE=y +BR2_PACKAGE_NMAP=y +BR2_PACKAGE_NMAP_NCAT=y +BR2_PACKAGE_NMAP_NMAP=y +BR2_PACKAGE_NMAP_NPING=y +BR2_PACKAGE_ODHCP6C=y +BR2_PACKAGE_OPENRESOLV=y +BR2_PACKAGE_OPENSSH=y +BR2_PACKAGE_PPPD=y +BR2_PACKAGE_SOCAT=y +BR2_PACKAGE_TCPDUMP=y +BR2_PACKAGE_TRACEROUTE=y +BR2_PACKAGE_ULOGD=y +BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y +BR2_PACKAGE_BASH_COMPLETION=y +BR2_PACKAGE_NEOFETCH=y +BR2_PACKAGE_SUDO=y +BR2_PACKAGE_TTYD=y +BR2_PACKAGE_GETENT=y +BR2_PACKAGE_HTOP=y +BR2_PACKAGE_IRQBALANCE=y +BR2_PACKAGE_KMOD_TOOLS=y +BR2_PACKAGE_PWGEN=y +BR2_PACKAGE_RAUC=y +BR2_PACKAGE_RAUC_DBUS=y +BR2_PACKAGE_RAUC_GPT=y +BR2_PACKAGE_RAUC_NETWORK=y +BR2_PACKAGE_RAUC_JSON=y +BR2_PACKAGE_SYSKLOGD=y +BR2_PACKAGE_SYSKLOGD_LOGGER=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON=y +BR2_PACKAGE_LESS=y +BR2_PACKAGE_MG=y +BR2_PACKAGE_NANO=y +# BR2_TARGET_ROOTFS_TAR is not set +BR2_TARGET_EDK2=y +BR2_TARGET_GRUB2=y +BR2_TARGET_GRUB2_X86_64_EFI=y +BR2_TARGET_GRUB2_BUILTIN_MODULES_EFI="boot linux ext2 squash4 part_gpt normal efi_gop configfile loadenv test echo reboot net efinet tftp loopback cat search" +BR2_TARGET_GRUB2_BUILTIN_CONFIG_EFI="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/grub-embed.cfg" +BR2_TARGET_GRUB2_INSTALL_TOOLS=y +BR2_PACKAGE_HOST_DOSFSTOOLS=y +BR2_PACKAGE_HOST_E2FSPROGS=y +BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y +BR2_PACKAGE_HOST_GENEXT2FS=y +BR2_PACKAGE_HOST_GO_BIN=y +BR2_PACKAGE_HOST_MTOOLS=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y +IX_VENDOR="Example Inc." +IX_VENDOR_HOME="https://example.com" +IX_NAME="ExampleOS" +IX_ID="example-os" +IX_COMPATIBLE="infix-${BR2_ARCH}" +IX_TAGLINE="ExampleOS -- an Infix spin" +IX_DESC="Example spin of Infix OS, use as a starting point for your own." +IX_HOME="https://example.com" +IX_DOC="https://example.com/docs" +IX_SUPPORT="mailto:support@example.com" +IX_OEM_PATH="${BR2_EXTERNAL_EXAMPLE_PATH}" +BR2_PACKAGE_FEATURE_GPS=y +BR2_PACKAGE_FEATURE_WIFI=y +BR2_PACKAGE_FEATURE_WIFI_HWSIM=y +BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y +BR2_PACKAGE_FEATURE_WIFI_REALTEK=y +BR2_PACKAGE_FEATURE_WIFI_QUALCOMM=y +BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y +BR2_PACKAGE_CONFD_TEST_MODE=y +BR2_PACKAGE_CURIOS_HTTPD=y +BR2_PACKAGE_CURIOS_NFTABLES=y +BR2_PACKAGE_GENCERT=y +BR2_PACKAGE_STATD=y +BR2_PACKAGE_SUPPORT_ENCRYPT=y +BR2_PACKAGE_FACTORY=y +BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y +BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y +BR2_PACKAGE_FINIT_PLUGIN_MODULES_LOAD=y +BR2_PACKAGE_FINIT_PLUGIN_RTC=y +BR2_PACKAGE_FINIT_RTC_DATE="2024-11-04 10:54:00" +BR2_PACKAGE_FINIT_RTC_FILE="/var/lib/misc/rtc" +BR2_PACKAGE_FINIT_PLUGIN_TTY=y +BR2_PACKAGE_FINIT_PLUGIN_URANDOM=y +BR2_PACKAGE_FIREWALL=y +BR2_PACKAGE_IITO=y +BR2_PACKAGE_KEYACK=y +BR2_PACKAGE_KLISH_PLUGIN_INFIX=y +BR2_PACKAGE_LOWDOWN=y +BR2_PACKAGE_MCD=y +BR2_PACKAGE_MDNS_ALIAS=y +BR2_PACKAGE_NETBROWSE=y +BR2_PACKAGE_ONIEPROM=y +BR2_PACKAGE_PODMAN=y +BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y +BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y +BR2_PACKAGE_PODMAN_DRIVER_VFS=y +BR2_PACKAGE_TETRIS=y +BR2_PACKAGE_ROUSETTE=y +BR2_PACKAGE_WEBUI=y +BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y +BR2_PACKAGE_HOST_PYTHON_YANGDOC=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y +BR2_PACKAGE_HELLO_WORLD=y diff --git a/configs/x86_64_defconfig.base b/configs/x86_64_defconfig.base new file mode 100644 index 0000000..2d73704 --- /dev/null +++ b/configs/x86_64_defconfig.base @@ -0,0 +1,197 @@ +BR2_x86_64=y +BR2_x86_corei7=y +BR2_TOOLCHAIN_EXTERNAL=y +BR2_TOOLCHAIN_EXTERNAL_GDB_SERVER_COPY=y +BR2_DL_DIR="${BR2_EXTERNAL_INFIX_PATH}/dl" +BR2_CCACHE=y +BR2_CCACHE_DIR="${BR2_EXTERNAL_INFIX_PATH}/.ccache" +BR2_ENABLE_DEBUG=y +BR2_GLOBAL_PATCH_DIR="${BR2_EXTERNAL_INFIX_PATH}/patches" +BR2_DOWNLOAD_FORCE_CHECK_HASHES=y +BR2_TARGET_GENERIC_HOSTNAME="infix" +BR2_TARGET_GENERIC_ISSUE="Infix by KernelKit" +BR2_INIT_FINIT=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y +BR2_ROOTFS_DEVICE_TABLE="system/device_table.txt ${BR2_EXTERNAL_INFIX_PATH}/board/common/xattrs" +BR2_ROOTFS_MERGED_USR=y +# BR2_TARGET_ENABLE_ROOT_LOGIN is not set +BR2_TARGET_GENERIC_GETTY_PORT="@console" +BR2_TARGET_GENERIC_GETTY_TERM="xterm" +BR2_SYSTEM_DHCP="eth0" +BR2_ENABLE_LOCALE_WHITELIST="C en_US en_CA C.UTF-8" +BR2_GENERATE_LOCALE="en_US en_CA C.UTF-8" +BR2_TARGET_TZ_INFO=y +BR2_ROOTFS_OVERLAY="${BR2_EXTERNAL_INFIX_PATH}/board/common/rootfs ${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/rootfs" +BR2_ROOTFS_POST_BUILD_SCRIPT="board/qemu/x86_64/post-build.sh ${BR2_EXTERNAL_INFIX_PATH}/board/common/post-build.sh" +BR2_LINUX_KERNEL=y +BR2_LINUX_KERNEL_CUSTOM_VERSION=y +BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.18.56" +BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y +BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/linux_defconfig" +BR2_LINUX_KERNEL_INSTALL_TARGET=y +BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y +BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_STRACE=y +BR2_PACKAGE_STRESS_NG=y +BR2_PACKAGE_JQ=y +BR2_PACKAGE_E2FSPROGS=y +BR2_PACKAGE_E2FSPROGS_RESIZE2FS=y +BR2_PACKAGE_DBUS_CXX=y +BR2_PACKAGE_DBUS_GLIB=y +BR2_PACKAGE_EUDEV_RULES_GEN=y +# BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set +BR2_PACKAGE_GPTFDISK=y +BR2_PACKAGE_GPTFDISK_SGDISK=y +BR2_PACKAGE_RNG_TOOLS=y +BR2_PACKAGE_UBOOT_TOOLS=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y +BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y +BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y +BR2_PACKAGE_LIBNETCONF2_SSH_SUBSYSTEM=y +BR2_PACKAGE_LIBSSH2=y +BR2_PACKAGE_LIBSSH2_OPENSSL=y +BR2_PACKAGE_LIBOPENSSL_BIN=y +BR2_PACKAGE_LIBINPUT=y +BR2_PACKAGE_LIBCURL_CURL=y +BR2_PACKAGE_NSS_MDNS=y +BR2_PACKAGE_SYSREPO_GROUP="sysrepo" +BR2_PACKAGE_LINUX_PAM=y +BR2_PACKAGE_LIBPAM_RADIUS_AUTH=y +BR2_PACKAGE_ONIGURUMA=y +BR2_PACKAGE_AVAHI_DAEMON=y +BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y +BR2_PACKAGE_CHRONY=y +BR2_PACKAGE_CONNTRACK_TOOLS=y +BR2_PACKAGE_DNSMASQ=y +BR2_PACKAGE_ETHTOOL=y +BR2_PACKAGE_FPING=y +BR2_PACKAGE_FRR=y +# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set +BR2_PACKAGE_IPERF3=y +BR2_PACKAGE_IPROUTE2=y +BR2_PACKAGE_IPTABLES_NFTABLES=y +BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y +BR2_PACKAGE_LLDPD=y +BR2_PACKAGE_MSTPD=y +BR2_PACKAGE_MTR=y +BR2_PACKAGE_NETCALC=y +BR2_PACKAGE_NETCAT_OPENBSD=y +BR2_PACKAGE_NETSNMP=y +BR2_PACKAGE_NETSNMP_WITH_MIB_MODULES="host if-mib ucd-snmp/loadave ucd-snmp/memory ucd-snmp/disk_hw agentx" +# BR2_PACKAGE_NETSNMP_ENABLE_MIBS is not set +BR2_PACKAGE_NGINX=y +BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y +BR2_PACKAGE_NGINX_HTTP_V2_MODULE=y +BR2_PACKAGE_NMAP=y +BR2_PACKAGE_NMAP_NCAT=y +BR2_PACKAGE_NMAP_NMAP=y +BR2_PACKAGE_NMAP_NPING=y +BR2_PACKAGE_ODHCP6C=y +BR2_PACKAGE_OPENRESOLV=y +BR2_PACKAGE_OPENSSH=y +BR2_PACKAGE_PPPD=y +BR2_PACKAGE_SOCAT=y +BR2_PACKAGE_TCPDUMP=y +BR2_PACKAGE_TRACEROUTE=y +BR2_PACKAGE_ULOGD=y +BR2_PACKAGE_WHOIS=y +BR2_PACKAGE_WIREGUARD_TOOLS=y +BR2_PACKAGE_BASH=y +BR2_PACKAGE_BASH_COMPLETION=y +BR2_PACKAGE_NEOFETCH=y +BR2_PACKAGE_SUDO=y +BR2_PACKAGE_TTYD=y +BR2_PACKAGE_GETENT=y +BR2_PACKAGE_HTOP=y +BR2_PACKAGE_IRQBALANCE=y +BR2_PACKAGE_KMOD_TOOLS=y +BR2_PACKAGE_PWGEN=y +BR2_PACKAGE_RAUC=y +BR2_PACKAGE_RAUC_DBUS=y +BR2_PACKAGE_RAUC_GPT=y +BR2_PACKAGE_RAUC_NETWORK=y +BR2_PACKAGE_RAUC_JSON=y +BR2_PACKAGE_SYSKLOGD=y +BR2_PACKAGE_SYSKLOGD_LOGGER=y +BR2_PACKAGE_WATCHDOGD=y +BR2_PACKAGE_WATCHDOGD_GENERIC=y +BR2_PACKAGE_WATCHDOGD_LOADAVG=y +BR2_PACKAGE_WATCHDOGD_FILENR=y +BR2_PACKAGE_WATCHDOGD_MEMINFO=y +BR2_PACKAGE_WATCHDOGD_FSMON=y +BR2_PACKAGE_WATCHDOGD_TEMPMON=y +BR2_PACKAGE_LESS=y +BR2_PACKAGE_MG=y +BR2_PACKAGE_NANO=y +# BR2_TARGET_ROOTFS_TAR is not set +BR2_TARGET_EDK2=y +BR2_TARGET_GRUB2=y +BR2_TARGET_GRUB2_X86_64_EFI=y +BR2_TARGET_GRUB2_BUILTIN_MODULES_EFI="boot linux ext2 squash4 part_gpt normal efi_gop configfile loadenv test echo reboot net efinet tftp loopback cat search" +BR2_TARGET_GRUB2_BUILTIN_CONFIG_EFI="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/grub-embed.cfg" +BR2_TARGET_GRUB2_INSTALL_TOOLS=y +BR2_PACKAGE_HOST_DOSFSTOOLS=y +BR2_PACKAGE_HOST_E2FSPROGS=y +BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y +BR2_PACKAGE_HOST_GENEXT2FS=y +BR2_PACKAGE_HOST_GO_BIN=y +BR2_PACKAGE_HOST_MTOOLS=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y +BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y +IX_VENDOR_HOME="https://www.kernelkit.org" +IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" +BR2_PACKAGE_FEATURE_GPS=y +BR2_PACKAGE_FEATURE_WIFI=y +BR2_PACKAGE_FEATURE_WIFI_HWSIM=y +BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y +BR2_PACKAGE_FEATURE_WIFI_QUALCOMM=y +BR2_PACKAGE_FEATURE_WIFI_REALTEK=y +BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y +BR2_PACKAGE_CONFD_TEST_MODE=y +BR2_PACKAGE_CURIOS_HTTPD=y +BR2_PACKAGE_CURIOS_NFTABLES=y +BR2_PACKAGE_GENCERT=y +BR2_PACKAGE_STATD=y +BR2_PACKAGE_SUPPORT_ENCRYPT=y +BR2_PACKAGE_FACTORY=y +BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y +BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y +BR2_PACKAGE_FINIT_PLUGIN_MODULES_LOAD=y +BR2_PACKAGE_FINIT_PLUGIN_RTC=y +BR2_PACKAGE_FINIT_RTC_DATE="2024-11-04 10:54:00" +BR2_PACKAGE_FINIT_RTC_FILE="/var/lib/misc/rtc" +BR2_PACKAGE_FINIT_PLUGIN_TTY=y +BR2_PACKAGE_FINIT_PLUGIN_URANDOM=y +BR2_PACKAGE_FIREWALL=y +BR2_PACKAGE_IITO=y +BR2_PACKAGE_KEYACK=y +BR2_PACKAGE_KLISH_PLUGIN_INFIX=y +BR2_PACKAGE_LOWDOWN=y +BR2_PACKAGE_MCD=y +BR2_PACKAGE_MDNS_ALIAS=y +BR2_PACKAGE_NETBROWSE=y +BR2_PACKAGE_ONIEPROM=y +BR2_PACKAGE_PODMAN=y +BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y +BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y +BR2_PACKAGE_PODMAN_DRIVER_VFS=y +BR2_PACKAGE_TETRIS=y +BR2_PACKAGE_ROUSETTE=y +# BR2_PACKAGE_LANDING is not set +BR2_PACKAGE_WEBUI=y +BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y +BR2_PACKAGE_HOST_PYTHON_YANGDOC=y +BR2_PACKAGE_PCIUTILS=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/doc/logo-dark.png b/doc/logo-dark.png new file mode 100644 index 0000000..fde4099 Binary files /dev/null and b/doc/logo-dark.png differ diff --git a/doc/logo.png b/doc/logo.png new file mode 100644 index 0000000..bbdb65f Binary files /dev/null and b/doc/logo.png differ diff --git a/external.desc b/external.desc new file mode 100644 index 0000000..d2b7fcb --- /dev/null +++ b/external.desc @@ -0,0 +1,2 @@ +name: EXAMPLE +desc: Example Infix OS Spin diff --git a/external.mk b/external.mk new file mode 100644 index 0000000..401ab65 --- /dev/null +++ b/external.mk @@ -0,0 +1 @@ +include $(sort $(wildcard $(BR2_EXTERNAL_EXAMPLE_PATH)/package/*/*.mk)) diff --git a/infix b/infix new file mode 160000 index 0000000..6eb2e07 --- /dev/null +++ b/infix @@ -0,0 +1 @@ +Subproject commit 6eb2e079fdc292d102cfb88ccb3fd7e4c11df052 diff --git a/package/Config.in b/package/Config.in new file mode 100644 index 0000000..631bf53 --- /dev/null +++ b/package/Config.in @@ -0,0 +1,3 @@ +menu "ExampleOS packages" + source "$BR2_EXTERNAL_EXAMPLE_PATH/package/hello-world/Config.in" +endmenu diff --git a/package/hello-world/Config.in b/package/hello-world/Config.in new file mode 100644 index 0000000..9014753 --- /dev/null +++ b/package/hello-world/Config.in @@ -0,0 +1,15 @@ +config BR2_PACKAGE_HELLO_WORLD + bool "hello-world" + depends on BR2_PACKAGE_CONFD + help + Example daemon for the spin: logs a greeting to syslog + once a minute. Started by finit, see + board/common/rootfs/etc/finit.d/available/hello-world.conf + + Takes its configuration from the example-hello YANG model + in yang/, and publishes a greeting counter as operational + data, so it can be driven from the CLI, NETCONF, and + RESTCONF without any glue of its own. + +comment "hello-world needs confd, for sysrepo and the YANG datastore" + depends on !BR2_PACKAGE_CONFD diff --git a/package/hello-world/hello-world.mk b/package/hello-world/hello-world.mk new file mode 100644 index 0000000..d694e67 --- /dev/null +++ b/package/hello-world/hello-world.mk @@ -0,0 +1,58 @@ +################################################################################ +# +# hello-world +# +################################################################################ + +HELLO_WORLD_VERSION = 1.0 +# Build from source vendored in this repo instead of a download URL. +HELLO_WORLD_SITE_METHOD = local +HELLO_WORLD_SITE = $(BR2_EXTERNAL_EXAMPLE_PATH)/src/hello-world +HELLO_WORLD_LICENSE = MIT + +# sysrepo and libyang are what the daemon links against. confd is here +# for ordering: it wipes and re-populates the build's sysrepo datastore, +# so our model has to be loaded after it has had its turn. +HELLO_WORLD_DEPENDENCIES = sysrepo libyang confd + +HELLO_WORLD_YANG_REV = 2026-08-26 +HELLO_WORLD_YANG_SRC = $(BR2_EXTERNAL_EXAMPLE_PATH)/yang +HELLO_WORLD_YANG_DIR = $(TARGET_DIR)/usr/share/yang/modules/example + +define HELLO_WORLD_BUILD_CMDS + $(MAKE) -C $(@D) \ + CC="$(TARGET_CC)" \ + CFLAGS="$(TARGET_CFLAGS) -Wall" \ + LDFLAGS="$(TARGET_LDFLAGS)" +endef + +define HELLO_WORLD_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0755 $(@D)/hello-world \ + $(TARGET_DIR)/usr/sbin/hello-world + $(INSTALL) -D -m 0644 \ + $(HELLO_WORLD_YANG_SRC)/example-hello@$(HELLO_WORLD_YANG_REV).yang \ + $(HELLO_WORLD_YANG_DIR)/example-hello@$(HELLO_WORLD_YANG_REV).yang +endef + +# Load the model into sysrepo at build time, the way Infix loads its own. +# infix/package/confd-test-mode/ is the pattern this follows: a package +# outside confd, adding one model of its own. The SHM prefix has to be +# confd's, so srload attaches to the datastore confd just set up. +define HELLO_WORLD_INSTALL_YANG_MODULES + SYSREPO_SHM_PREFIX=$(CONFD_SYSREPO_SHM_PREFIX) \ + SYSREPOCTL_EXECUTABLE="$(HOST_DIR)/bin/sysrepoctl" \ + SYSREPOCFG_EXECUTABLE="$(HOST_DIR)/bin/sysrepocfg" \ + SEARCH_PATH="$(HELLO_WORLD_YANG_DIR)/" \ + $(BR2_EXTERNAL_INFIX_PATH)/utils/srload \ + $(HELLO_WORLD_YANG_SRC)/example-hello.inc +endef +HELLO_WORLD_POST_INSTALL_TARGET_HOOKS += HELLO_WORLD_INSTALL_YANG_MODULES + +# sysrepo keeps each model's datastore under here, group readable so the +# CLI can get at it as the logged in user. +define HELLO_WORLD_PERMISSIONS + /etc/sysrepo/data/ r 660 root sysrepo - - - - - + /etc/sysrepo/data d 770 root sysrepo - - - - - +endef + +$(eval $(generic-package)) diff --git a/src/hello-world/Makefile b/src/hello-world/Makefile new file mode 100644 index 0000000..d256985 --- /dev/null +++ b/src/hello-world/Makefile @@ -0,0 +1,13 @@ +CC ?= cc +CFLAGS ?= -O2 -Wall +LIBS = -lsysrepo -lyang + +all: hello-world + +hello-world: hello-world.c + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS) + +clean: + rm -f hello-world + +.PHONY: all clean diff --git a/src/hello-world/hello-world.c b/src/hello-world/hello-world.c new file mode 100644 index 0000000..52b4ebb --- /dev/null +++ b/src/hello-world/hello-world.c @@ -0,0 +1,157 @@ +/* hello-world - example daemon for the Infix spin + * + * Greets an audience in the system log once a minute. Who to greet is + * configuration, and how many times it has greeted them is operational + * state -- both described by the example-hello YANG model shipped by + * this spin, see yang/example-hello@2026-08-26.yang. + * + * The daemon talks to sysrepo, the datastore behind Infix's CLI, + * NETCONF, and RESTCONF interfaces. Two subscriptions is all it takes: + * one to be told when the configuration changes, one to answer when + * someone asks for the state. Nothing in the CLI knows about this + * daemon; it picks the model up from sysrepo on its own. + * + * Runs in the foreground, as finit expects, which handles supervision + * and restart. + */ +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MODULE "example-hello" +#define XPATH_HELLO "/example-hello:hello" +#define XPATH_AUDIENCE XPATH_HELLO "/audience" +#define XPATH_GREETINGS XPATH_HELLO "/greetings" +#define INTERVAL 60 + +static volatile sig_atomic_t running = 1; +static char audience[64] = "world"; +static uint64_t greetings; + +static void sig_exit(int signo) +{ + running = 0; +} + +/* + * Read the audience from the running datastore. The leaf has a default + * in the YANG model, so this returns "world" on a system that has never + * been configured -- no fallback needed here. + */ +static void read_audience(sr_session_ctx_t *sess) +{ + sr_val_t *val = NULL; + int rc; + + rc = sr_get_item(sess, XPATH_AUDIENCE, 0, &val); + if (rc || !val) { + syslog(LOG_WARNING, "cannot read %s (%s), still greeting '%s'", + XPATH_AUDIENCE, sr_strerror(rc), audience); + return; + } + + snprintf(audience, sizeof(audience), "%s", val->data.string_val); + sr_free_val(val); + + syslog(LOG_NOTICE, "greeting '%s' from now on", audience); +} + +/* + * Called once a 'set hello audience ...' has been committed, i.e., after + * 'leave' in the CLI or a NETCONF . + */ +static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *xpath, sr_event_t event, uint32_t request_id, void *priv) +{ + if (event == SR_EV_DONE) + read_audience(priv); + + return SR_ERR_OK; +} + +/* + * Called when someone reads operational data, e.g., 'show hello'. We + * hand back a data tree with the counter in it; sysrepo merges it into + * whatever the caller asked for. + */ +static int oper_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *path, const char *request_xpath, uint32_t request_id, + struct lyd_node **parent, void *priv) +{ + const struct ly_ctx *ctx; + sr_conn_ctx_t *conn; + char buf[32]; + + conn = sr_session_get_connection(session); + ctx = sr_acquire_context(conn); + + snprintf(buf, sizeof(buf), "%llu", (unsigned long long)greetings); + if (!*parent) + lyd_new_path(NULL, ctx, XPATH_HELLO, NULL, 0, parent); + lyd_new_path(*parent, NULL, XPATH_GREETINGS, buf, 0, NULL); + + sr_release_context(conn); + + return SR_ERR_OK; +} + +int main(void) +{ + sr_subscription_ctx_t *sub = NULL; + sr_session_ctx_t *sess; + sr_conn_ctx_t *conn; + int rc; + + signal(SIGTERM, sig_exit); + signal(SIGINT, sig_exit); + + openlog("hello-world", LOG_PID, LOG_DAEMON); + + rc = sr_connect(0, &conn); + if (rc) { + syslog(LOG_ERR, "cannot connect to sysrepo: %s", sr_strerror(rc)); + return 1; + } + + rc = sr_session_start(conn, SR_DS_RUNNING, &sess); + if (rc) { + syslog(LOG_ERR, "cannot start sysrepo session: %s", sr_strerror(rc)); + return 1; + } + + read_audience(sess); + + /* Tell me when the configuration changes ... */ + rc = sr_module_change_subscribe(sess, MODULE, XPATH_HELLO, change_cb, sess, + 0, SR_SUBSCR_DONE_ONLY, &sub); + if (rc) { + syslog(LOG_ERR, "cannot subscribe to %s: %s", XPATH_HELLO, sr_strerror(rc)); + return 1; + } + + /* ... and ask me when someone wants the state. */ + rc = sr_oper_get_subscribe(sess, MODULE, XPATH_GREETINGS, oper_cb, NULL, 0, &sub); + if (rc) { + syslog(LOG_ERR, "cannot subscribe to %s: %s", XPATH_GREETINGS, sr_strerror(rc)); + return 1; + } + + while (running) { + syslog(LOG_INFO, "Hello %s!", audience); + greetings++; + sleep(INTERVAL); + } + + syslog(LOG_INFO, "exiting"); + sr_unsubscribe(sub); + sr_disconnect(conn); + closelog(); + + return 0; +} diff --git a/test/9pm-proj.yaml b/test/9pm-proj.yaml new file mode 100644 index 0000000..6422b3a --- /dev/null +++ b/test/9pm-proj.yaml @@ -0,0 +1,6 @@ +# Tell 9pm which project it is testing. Paths are relative to the 9pm +# submodule, infix/test/9pm/, so "../../../" is the root of this spin. +9pm: + PROJECT-NAME: "ExampleOS" + PROJECT-ROOT: "../../../" + PROJECT-TOPDOC: "../case/" diff --git a/test/case/all.yaml b/test/case/all.yaml new file mode 100644 index 0000000..40b79a6 --- /dev/null +++ b/test/case/all.yaml @@ -0,0 +1,11 @@ +# Test suite for this spin. `make test` runs it instead of Infix's own +# suite, see TESTS in the top-level Makefile. Add a line per test case; +# to also run (parts of) the Infix suite, point at it with a relative +# path, e.g.: +# +# - name: "System" +# suite: ../../infix/test/case/system/all.yaml +# +--- +- name: "Hello World Daemon" + case: hello-world/test.py diff --git a/test/case/hello-world/test.py b/test/case/hello-world/test.py new file mode 100755 index 0000000..b8d9423 --- /dev/null +++ b/test/case/hello-world/test.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Hello World daemon + +Verify what this spin adds on top of Infix: the hello-world daemon runs +under finit, takes its configuration from the example-hello YANG model +this repo ships, and reports state back through the same model. + +Nothing here is specific to NETCONF -- the CLI and RESTCONF reach the +very same data, because they are all generated from the model. + +""" +import infamy + +NEW = "there" + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + + with test.step("Verify finit supervises the hello-world service"): + status = tgtssh.runsh("initctl status hello-world", check=True).stdout + print(status) + if "running" not in status: + test.fail() + + with test.step("Verify the daemon greets the default audience"): + # Nothing has been configured, so this is the factory default + # the overlay ships, see board/common/rootfs/usr/share/confd/ + running = target.get_config_dict("/example-hello:hello") + print(running) + if running["hello"]["audience"] != "world": + test.fail() + + infamy.until(lambda: tgtssh.runsh("sudo grep -q 'Hello world!' /var/log/syslog", + check=False).returncode == 0) + + with test.step("Verify the greeting counter is reported as operational data"): + oper = target.get_data("/example-hello:hello") + print(f"greetings: {oper['hello']['greetings']}") + if int(oper["hello"]["greetings"]) < 1: + test.fail() + + with test.step(f"Set audience to '{NEW}'"): + target.put_config_dicts({ + "example-hello": { + "hello": { + "audience": NEW, + } + } + }) + + with test.step(f"Verify '{NEW}' is in the running configuration"): + running = target.get_config_dict("/example-hello:hello") + if running["hello"]["audience"] != NEW: + test.fail() + + with test.step(f"Verify the daemon starts greeting '{NEW}'"): + # The daemon greets once a minute, so give it a full cycle to + # pick the new audience up. + infamy.until(lambda: tgtssh.runsh(f"sudo grep -q 'Hello {NEW}!' /var/log/syslog", + check=False).returncode == 0, + attempts=90, interval=1) + + test.succeed() diff --git a/test/case/hello-world/topology.dot b/test/case/hello-world/topology.dot new file mode 100644 index 0000000..9f8d895 --- /dev/null +++ b/test/case/hello-world/topology.dot @@ -0,0 +1,27 @@ +// Logical topology required by this test: one DUT reachable over a +// management link. Infamy maps it onto whatever physical topology the +// test environment provides, so the same test runs in QEMU or on real +// hardware without changes. +graph "1x1" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] +} diff --git a/yang/example-hello.inc b/yang/example-hello.inc new file mode 100644 index 0000000..80b60da --- /dev/null +++ b/yang/example-hello.inc @@ -0,0 +1,7 @@ +# -*- sh -*- +# Model list for utils/srload, same format Infix uses for its own, see +# infix/src/confd/yang/*.inc + +MODULES=( + "example-hello@2026-08-26.yang" +) diff --git a/yang/example-hello@2026-08-26.yang b/yang/example-hello@2026-08-26.yang new file mode 100644 index 0000000..8331969 --- /dev/null +++ b/yang/example-hello@2026-08-26.yang @@ -0,0 +1,57 @@ +module example-hello { + yang-version 1.1; + namespace "urn:example:params:xml:ns:yang:example-hello"; + prefix example-hello; + + organization "Example Inc."; + contact "mailto:support@example.com"; + + description + "Configuration and state for the hello-world example daemon. + + This model is the whole point of the example. It is defined here in + the spin, loaded into sysrepo at build time (see + package/hello-world/hello-world.mk), and served by the daemon itself + (see src/hello-world/hello-world.c). Nothing else needs to know + about it: the CLI, NETCONF, and RESTCONF are all generated from the + models sysrepo has loaded, so this file is what makes + + set hello audience there + + a valid command."; + + revision 2026-08-26 { + description "Initial revision."; + } + + container hello { + description "Who the hello-world daemon greets, and how often it has."; + + leaf audience { + type string { + length "1..64"; + } + default "world"; + description + "Who to greet. The daemon logs 'Hello !' to the system + log once a minute. + + A default is not the same as a starting configuration. The + daemon reads 'world' here whether or not anything is set, but a + leaf still holding its default is left out when the datastore is + serialized, so 'show hello' would print nothing for it. The + snippets in board/common/rootfs/usr/share/confd/ set it for + real, which is what makes it visible."; + } + + leaf greetings { + config false; + type uint64; + description + "Number of greetings logged since the daemon started. Marked + 'config false', so this is operational state: the daemon answers + for it live, it is never stored, and the CLI will not offer it + for editing in configure mode."; + } + } +}