diff --git a/.github/workflows/integration_tests.yaml b/.github/workflows/integration_tests.yaml index c5006171..dae95035 100644 --- a/.github/workflows/integration_tests.yaml +++ b/.github/workflows/integration_tests.yaml @@ -219,6 +219,8 @@ jobs: goarch: amd64 - goos: illumos goarch: amd64 + - goos: plan9 + goarch: amd64 runs-on: ubuntu-latest timeout-minutes: 20 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e607a8bb..02361f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Release artifacts and cross-compile CI now include `arm` and `arm64` for FreeBSD, OpenBSD, and NetBSD. +- Added native-gated Plan 9 (`plan9/amd64`) fact support for canonical + OS/kernel identity, hostname, architecture/hardware, memory total, processor + count/ISA/models, basic IPv4 networking, uptime, timezone, path, schema docs, + a native `rc` release gate, and CI cross-compile coverage. Plan 9 release + artifacts remain a separate promotion decision. ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3c0579d..1ddd3b34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,10 +56,13 @@ tools). ## Platform scope -Release targets: **Linux, macOS/Darwin, Windows, FreeBSD, OpenBSD, and -NetBSD**. Solaris, AIX, DragonFly, and generic BSD-family paths are out of -scope — do not start work for them until a repeatable validation target -exists, and do not treat their gaps as blockers. +Release targets: **Linux, macOS/Darwin, Windows, FreeBSD, OpenBSD, NetBSD, +DragonFly BSD, and illumos**. Plan 9 has lab-validated fact support for +`plan9/amd64`, but it is not a published release artifact target until the +release matrix explicitly promotes it. Solaris, AIX, +generic BSD-family paths, and unvalidated Plan 9 tuples are out of scope — do +not start work for them until a repeatable validation target exists, and do not +treat their gaps as blockers. ## Benchmark discipline @@ -98,6 +101,14 @@ the pipeline (`.github/workflows/unit_tests.yaml`): `tools/illumos-release-gate.sh`. Local, untracked SSH wrappers can run the same checks for `dragonfly/amd64` and `illumos/amd64`; Oracle Solaris is not covered by the illumos gate. +- **Plan 9**: `plan9/amd64` facts are validated through the facts-lab Plan 9 + guest and `tools/plan9-release-gate.rc`. The local flow is: + build `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go build ./cmd/facts`, copy the + binary and gate script through your configured facts-lab transport, then run + the gate with `facts-lab ssh plan9` (or a local untracked wrapper). Keep lab + hostnames, keys, guest addresses, and generated credentials out of tracked + files. Any Plan 9 release-gate fact must have a matching + `docs/schema/facts.yaml` entry and regenerated `docs/supported-facts/plan9.md`. Local equivalents: `make lima-freebsd-smoke`, `make lima-linux-flavor-smoke`, `make local-bsd-smoke`, `make local-amd64-bsd-smoke`, @@ -110,7 +121,9 @@ The amd64 lab smoke targets are configured only through wrapper variables: `LOCAL_NETBSD_AMD64_SSH`, `LOCAL_DRAGONFLY_AMD64_SSH`, and `LOCAL_ILLUMOS_AMD64_SSH`. OpenBSD, NetBSD, and DragonFly wrappers must allow `sudo -n` because their release gates read privileged disk labels. Real -`.local` wrapper scripts stay untracked. +`.local` wrapper scripts stay untracked. Plan 9 wrappers must invoke the +tracked `tools/plan9-release-gate.rc` and must not add private lab details to +the repository. ## The change workflow diff --git a/Makefile b/Makefile index 546bb70a..ab1e9a34 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ LIMA_FREEBSD_FLAGS ?= --mount-none --cpus 2 --memory 4 --disk 40 LIMA_GO_CONTAINER_IMAGES ?= golang:1.26-bookworm golang:1.26-alpine LIMA_DISTRO_IMAGES ?= debian:12-slim ubuntu:24.04 archlinux:latest oraclelinux:9 LIMA_LINUX_FLAVORS ?= ubuntu-lts debian fedora opensuse oraclelinux rocky almalinux alpine archlinux -LIMA_CROSS_TARGETS ?= linux/amd64 linux/arm64 windows/amd64 windows/arm64 darwin/amd64 darwin/arm64 freebsd/amd64 freebsd/arm freebsd/arm64 openbsd/amd64 openbsd/arm openbsd/arm64 netbsd/amd64 netbsd/arm netbsd/arm64 dragonfly/amd64 illumos/amd64 +CROSS_TARGETS ?= linux/amd64 linux/arm64 windows/amd64 windows/arm64 darwin/amd64 darwin/arm64 freebsd/amd64 freebsd/arm freebsd/arm64 openbsd/amd64 openbsd/arm openbsd/arm64 netbsd/amd64 netbsd/arm netbsd/arm64 dragonfly/amd64 illumos/amd64 plan9/amd64 LIMA_LINUX_BINARY ?= dist/facts-linux-$(LIMA_GOARCH) LIMA_FREEBSD_BINARY ?= dist/facts-freebsd-$(LIMA_GOARCH) @@ -211,7 +211,7 @@ lima-build-linux-binary: lima-dev-bootstrap CGO_ENABLED=0 GOOS=linux GOARCH=$(LIMA_GOARCH) go build -o "$(LIMA_LINUX_BINARY)" ./cmd/facts' lima-cross-compile: lima-dev-bootstrap - @for target in $(LIMA_CROSS_TARGETS); do \ + @for target in $(CROSS_TARGETS); do \ goos=$${target%/*}; \ goarch=$${target#*/}; \ echo "==> cross compile $$goos/$$goarch"; \ diff --git a/README.md b/README.md index 4a8a8a27..250e7b39 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ make build # builds ./facts from the project root ## Tested where it ships. Every release target is a blocking CI gate — unit tests, the race detector over the engine, a built-binary smoke, and per-platform release-gate fact checks. +Plan 9 has lab-validated fact support for `plan9/amd64`; it is not listed as a +published release artifact target until that tuple is promoted in the release +matrix. | Platform | Architectures | Gate | Supported facts | | --- | --- | --- | --- | @@ -131,6 +134,7 @@ Every release target is a blocking CI gate — unit tests, the race detector ove | NetBSD | amd64, arm, arm64 | VM job + release-gate fact set | [NetBSD facts](docs/supported-facts/netbsd.md) | | DragonFly BSD | amd64 | VM job + release-gate fact set | [DragonFly BSD facts](docs/supported-facts/dragonfly.md) | | illumos | amd64 | VM job + release-gate fact set | [illumos facts](docs/supported-facts/illumos.md) | +| Plan 9 | amd64 | facts-lab release gate | [Plan 9 facts](docs/supported-facts/plan9.md) | Requires Go 1.26+. diff --git a/docs/schema/facts.yaml b/docs/schema/facts.yaml index a5e9b89d..e56ed0e8 100644 --- a/docs/schema/facts.yaml +++ b/docs/schema/facts.yaml @@ -6,7 +6,7 @@ # type: string | integer | double | boolean | map | array # description: what the fact means, one line # platforms: the platforms that can emit it (linux, darwin, windows, -# freebsd, openbsd, netbsd, dragonfly, illumos) +# freebsd, openbsd, netbsd, dragonfly, illumos, plan9) # conditional: true when presence depends on host state (cloud instances, # swap, DMI visibility, installed tools); such entries may be # absent from a discovery without it being a bug @@ -168,7 +168,7 @@ ec2_userdata: facterversion: type: string description: The Facter compatibility version of the Facts engine. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] filesystems: type: array @@ -325,8 +325,8 @@ is_virtual: kernel.name: type: string - description: The kernel name, such as Linux, Darwin, windows, or FreeBSD. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + description: The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] kernel.release.full: type: string description: The full kernel release reported by the system. @@ -417,11 +417,13 @@ memory.system.capacity: memory.system.total: type: string description: The display amount of total physical memory, such as 16.00 GiB. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true memory.system.total_bytes: type: integer description: The total physical memory, in bytes. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true memory.system.used: type: string description: The display amount of physical memory in use, such as 1.00 GiB. @@ -493,16 +495,17 @@ networking.fqdn: networking.hostname: type: string description: The short host name of the machine. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.interfaces.*: type: map description: A network interface, keyed by interface name. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.bindings: type: array description: The IPv4 bindings of the interface (address, netmask, network). - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.bindings6: type: array @@ -527,7 +530,7 @@ networking.interfaces.*.duplex: networking.interfaces.*.ip: type: string description: The first IPv4 address bound to the interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.ip6: type: string @@ -537,7 +540,7 @@ networking.interfaces.*.ip6: networking.interfaces.*.mac: type: string description: The MAC address of the interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.mtu: type: integer @@ -547,7 +550,7 @@ networking.interfaces.*.mtu: networking.interfaces.*.netmask: type: string description: The IPv4 netmask of the interface's first binding. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.netmask6: type: string @@ -557,7 +560,7 @@ networking.interfaces.*.netmask6: networking.interfaces.*.network: type: string description: The IPv4 network of the interface's first binding. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] conditional: true networking.interfaces.*.network6: type: string @@ -586,7 +589,8 @@ networking.interfaces.*.speed: networking.ip: type: string description: The IPv4 address of the primary interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.ip6: type: string description: The IPv6 address of the primary interface. @@ -595,7 +599,8 @@ networking.ip6: networking.mac: type: string description: The MAC address of the primary interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.mtu: type: integer description: The maximum transmission unit of the primary interface. @@ -604,7 +609,8 @@ networking.mtu: networking.netmask: type: string description: The IPv4 netmask of the primary interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.netmask6: type: string description: The IPv6 netmask of the primary interface. @@ -613,7 +619,8 @@ networking.netmask6: networking.network: type: string description: The IPv4 network of the primary interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.network6: type: string description: The IPv6 network of the primary interface. @@ -622,7 +629,8 @@ networking.network6: networking.primary: type: string description: The name of the primary interface. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true networking.scope6: type: string description: The IPv6 scope of the primary interface, such as global or link. @@ -632,7 +640,7 @@ networking.scope6: os.architecture: type: string description: The operating system's hardware architecture, such as x86_64 or arm64. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] os.distro.codename: type: string description: The codename of the distribution release, such as noble. @@ -666,12 +674,12 @@ os.distro.specification: conditional: true os.family: type: string - description: The operating system family, such as Debian, RedHat, Darwin, or windows. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + description: The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] os.hardware: type: string description: The hardware model of the machine, such as x86_64. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] os.macosx.build: type: string description: The macOS build number, such as 24F74. @@ -704,8 +712,8 @@ os.macosx.version.patch: conditional: true os.name: type: string - description: The operating system name, such as Ubuntu, Darwin, or windows. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + description: The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] os.release.branch: type: string description: The FreeBSD release branch, such as RELEASE-p5. @@ -842,7 +850,7 @@ partitions.*.uuid: path: type: array description: The PATH environment entries of the Facts process, in lookup order. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] processors.cores: type: integer @@ -851,19 +859,22 @@ processors.cores: processors.count: type: integer description: The number of logical processors. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true processors.extensions: type: array description: The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] processors.isa: type: string - description: The processor instruction set architecture, as reported by uname -p. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + description: The processor instruction set architecture, as reported by the platform. + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true processors.models: type: array description: The processor model strings, one entry per logical processor. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true processors.physicalcount: type: integer description: The number of physical processor sockets. @@ -912,24 +923,28 @@ system_profiler: system_uptime.days: type: integer description: The whole days the system has been up. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true system_uptime.hours: type: integer description: The whole hours the system has been up. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true system_uptime.seconds: type: integer description: The seconds the system has been up. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true system_uptime.uptime: type: string description: The display form of the system uptime, such as 3 days. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] + conditional: true timezone: type: string description: The abbreviated time zone name of the system, such as UTC. - platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos] + platforms: [linux, darwin, windows, freebsd, openbsd, netbsd, dragonfly, illumos, plan9] virtual: type: string diff --git a/docs/supported-facts/README.md b/docs/supported-facts/README.md index 60a4109f..150439ba 100644 --- a/docs/supported-facts/README.md +++ b/docs/supported-facts/README.md @@ -14,3 +14,4 @@ These pages are generated from [`docs/schema/facts.yaml`](../schema/facts.yaml). | [NetBSD](netbsd.md) | 117 | | [DragonFly BSD](dragonfly.md) | 115 | | [illumos](illumos.md) | 114 | +| [Plan 9](plan9.md) | 29 | diff --git a/docs/supported-facts/darwin.md b/docs/supported-facts/darwin.md index db841e31..5fb4cd1c 100644 --- a/docs/supported-facts/darwin.md +++ b/docs/supported-facts/darwin.md @@ -77,7 +77,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -97,8 +97,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -115,7 +115,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -129,18 +129,18 @@ $ facts --json | `networking.interfaces.*.network` | `string` | yes | The IPv4 network of the interface's first binding. | | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | | `os.macosx.build` | `string` | no | The macOS build number, such as 24F74. | | `os.macosx.product` | `string` | no | The macOS product name, such as macOS. | @@ -149,16 +149,16 @@ $ facts --json | `os.macosx.version.major` | `string` | no | The major macOS version, such as 15. | | `os.macosx.version.minor` | `string` | no | The minor macOS version. | | `os.macosx.version.patch` | `string` | yes | The patch macOS version (absent on macOS 10.x). | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.release.minor` | `string` | yes | The minor release number of the operating system, when it has one. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -168,9 +168,9 @@ $ facts --json | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | | `system_profiler` | `map` | no | macOS system_profiler hardware, software, and Ethernet details (provider-shaped keys, such as model_name and serial_number). | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | diff --git a/docs/supported-facts/dragonfly.md b/docs/supported-facts/dragonfly.md index ba75593c..372bed6b 100644 --- a/docs/supported-facts/dragonfly.md +++ b/docs/supported-facts/dragonfly.md @@ -91,7 +91,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -111,8 +111,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -129,7 +129,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -144,20 +144,20 @@ $ facts --json | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.operational_state` | `string` | yes | The operational state of the interface, such as up or down. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.release.minor` | `string` | yes | The minor release number of the operating system, when it has one. | @@ -168,10 +168,10 @@ $ facts --json | `partitions.*.size_bytes` | `integer` | yes | The size of the partition, in bytes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -180,9 +180,9 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | diff --git a/docs/supported-facts/freebsd.md b/docs/supported-facts/freebsd.md index 06c305ef..f051ad6e 100644 --- a/docs/supported-facts/freebsd.md +++ b/docs/supported-facts/freebsd.md @@ -92,7 +92,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -112,8 +112,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -130,7 +130,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -146,20 +146,20 @@ $ facts --json | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.operational_state` | `string` | yes | The operational state of the interface, such as up or down. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.branch` | `string` | no | The FreeBSD release branch, such as RELEASE-p5. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | @@ -175,10 +175,10 @@ $ facts --json | `partitions.*.size_bytes` | `integer` | yes | The size of the partition, in bytes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -187,10 +187,10 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | | `zfs.feature_numbers` | `array` | yes | The supported ZFS filesystem version numbers. | diff --git a/docs/supported-facts/illumos.md b/docs/supported-facts/illumos.md index 1ac0caf5..31879af5 100644 --- a/docs/supported-facts/illumos.md +++ b/docs/supported-facts/illumos.md @@ -110,7 +110,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -130,8 +130,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -148,7 +148,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -162,20 +162,20 @@ $ facts --json | `networking.interfaces.*.network` | `string` | yes | The IPv4 network of the interface's first binding. | | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `partitions.*` | `map` | yes | A disk partition (or device-mapper/loop device), keyed by device path. | @@ -184,10 +184,10 @@ $ facts --json | `partitions.*.size_bytes` | `integer` | yes | The size of the partition, in bytes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -196,10 +196,10 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | | `zfs.feature_numbers` | `array` | yes | The supported ZFS filesystem version numbers. | diff --git a/docs/supported-facts/linux.md b/docs/supported-facts/linux.md index 77088ec2..0f9c4039 100644 --- a/docs/supported-facts/linux.md +++ b/docs/supported-facts/linux.md @@ -131,7 +131,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -151,8 +151,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -169,7 +169,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -187,15 +187,15 @@ $ facts --json | `networking.interfaces.*.physical` | `boolean` | no | Whether the interface is a physical device. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | | `networking.interfaces.*.speed` | `integer` | yes | The negotiated speed of the interface, in Mbit/s. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | | `os.distro.codename` | `string` | yes | The codename of the distribution release, such as noble. | @@ -205,9 +205,9 @@ $ facts --json | `os.distro.release.major` | `string` | no | The major release number of the distribution. | | `os.distro.release.minor` | `string` | yes | The minor release number of the distribution, when it has one. | | `os.distro.specification` | `string` | yes | The LSB specification version, when lsb_release is installed. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.release.minor` | `string` | yes | The minor release number of the operating system, when it has one. | @@ -230,10 +230,10 @@ $ facts --json | `partitions.*.uuid` | `string` | yes | The filesystem UUID of the partition. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -242,10 +242,10 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | | `xen.domains` | `array` | yes | The names of the running Xen guest domains, on a Xen dom0 host. | diff --git a/docs/supported-facts/netbsd.md b/docs/supported-facts/netbsd.md index d71f9029..6467a773 100644 --- a/docs/supported-facts/netbsd.md +++ b/docs/supported-facts/netbsd.md @@ -107,7 +107,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -127,8 +127,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -144,7 +144,7 @@ $ facts --json | `mountpoints.*.used_bytes` | `integer` | no | The space in use on the mount, in bytes. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -158,20 +158,20 @@ $ facts --json | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.operational_state` | `string` | yes | The operational state of the interface, such as up or down. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.release.minor` | `string` | yes | The minor release number of the operating system, when it has one. | @@ -183,10 +183,10 @@ $ facts --json | `partitions.*.size_bytes` | `integer` | yes | The size of the partition, in bytes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -195,10 +195,10 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | | `zfs.feature_numbers` | `array` | yes | The supported ZFS filesystem version numbers. | diff --git a/docs/supported-facts/openbsd.md b/docs/supported-facts/openbsd.md index 1033d64b..6c288dcb 100644 --- a/docs/supported-facts/openbsd.md +++ b/docs/supported-facts/openbsd.md @@ -86,7 +86,7 @@ $ facts --json | `identity.uid` | `integer` | no | The user identifier of the user running Facts. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -106,8 +106,8 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | @@ -124,7 +124,7 @@ $ facts --json | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -139,20 +139,20 @@ $ facts --json | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.operational_state` | `string` | yes | The operational state of the interface, such as up or down. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.release.minor` | `string` | yes | The minor release number of the operating system, when it has one. | @@ -163,10 +163,10 @@ $ facts --json | `partitions.*.size_bytes` | `integer` | yes | The size of the partition, in bytes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.speed` | `string` | yes | The display speed of the processors, such as 2.40 GHz, where the platform reports one (absent on Apple Silicon). | | `processors.threads` | `integer` | no | The number of hardware threads per core. | @@ -175,9 +175,9 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | diff --git a/docs/supported-facts/plan9.md b/docs/supported-facts/plan9.md new file mode 100644 index 00000000..c76f6eae --- /dev/null +++ b/docs/supported-facts/plan9.md @@ -0,0 +1,106 @@ + + +# Plan 9 Supported Facts + +Generated from [`docs/schema/facts.yaml`](../schema/facts.yaml). `conditional` entries may be absent on a host when their preconditions do not hold. + +## Example Output + +```console +$ facts --json +{ + "facterversion": "dev", + "kernel": { + "name": "Plan 9" + }, + "memory": { + "system": { + "total": "1018.38 MiB", + "total_bytes": 1067843584 + } + }, + "networking": { + "hostname": "plan9host", + "interfaces": { + "ether0": { + "bindings": [ + { + "address": "192.0.2.90", + "netmask": "255.255.255.0", + "network": "192.0.2.0" + } + ], + "ip": "192.0.2.90", + "netmask": "255.255.255.0", + "network": "192.0.2.0", + "mac": "52:54:00:12:34:90" + } + }, + "primary": "ether0", + "ip": "192.0.2.90", + "netmask": "255.255.255.0", + "network": "192.0.2.0", + "mac": "52:54:00:12:34:90" + }, + "os": { + "architecture": "amd64", + "family": "Plan 9", + "hardware": "amd64", + "name": "Plan 9" + }, + "path": [ + "/bin", + "/usr/glenda/bin" + ], + "processors": { + "count": 1, + "isa": "amd64", + "models": [ + "Core 2/Xeon 3600" + ] + }, + "system_uptime": { + "days": 0, + "hours": 23, + "seconds": 83717, + "uptime": "23:15 hours" + }, + "timezone": "CET" +} +``` + +## Fact Contract + +29 schema entries include `plan9`. + +| Fact | Type | Conditional | Description | +| --- | --- | --- | --- | +| `facterversion` | `string` | no | The Facter compatibility version of the Facts engine. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | +| `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | +| `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | +| `networking.interfaces.*.ip` | `string` | yes | The first IPv4 address bound to the interface. | +| `networking.interfaces.*.mac` | `string` | yes | The MAC address of the interface. | +| `networking.interfaces.*.netmask` | `string` | yes | The IPv4 netmask of the interface's first binding. | +| `networking.interfaces.*.network` | `string` | yes | The IPv4 network of the interface's first binding. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | +| `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | +| `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | +| `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | +| `processors.count` | `integer` | yes | The number of logical processors. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | +| `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | diff --git a/docs/supported-facts/windows.md b/docs/supported-facts/windows.md index e0b6a6fc..d7740ef1 100644 --- a/docs/supported-facts/windows.md +++ b/docs/supported-facts/windows.md @@ -77,7 +77,7 @@ $ facts --json | `identity.privileged` | `boolean` | no | Whether Facts is running with root (or Administrator) privileges. | | `identity.user` | `string` | no | The name of the user running Facts. | | `is_virtual` | `boolean` | no | Whether the machine is a virtual machine or container. | -| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, or FreeBSD. | +| `kernel.name` | `string` | no | The kernel name, such as Linux, Darwin, windows, FreeBSD, or Plan 9. | | `kernel.release.full` | `string` | no | The full kernel release reported by the system. | | `kernel.release.major` | `string` | no | The major component of the kernel release. | | `kernel.release.minor` | `string` | no | The minor component of the kernel release. | @@ -89,15 +89,15 @@ $ facts --json | `memory.system.available` | `string` | no | The display amount of free physical memory, such as 1.00 GiB. | | `memory.system.available_bytes` | `integer` | no | The free physical memory, in bytes. | | `memory.system.capacity` | `string` | no | The percentage of physical memory in use. | -| `memory.system.total` | `string` | no | The display amount of total physical memory, such as 16.00 GiB. | -| `memory.system.total_bytes` | `integer` | no | The total physical memory, in bytes. | +| `memory.system.total` | `string` | yes | The display amount of total physical memory, such as 16.00 GiB. | +| `memory.system.total_bytes` | `integer` | yes | The total physical memory, in bytes. | | `memory.system.used` | `string` | no | The display amount of physical memory in use, such as 1.00 GiB. | | `memory.system.used_bytes` | `integer` | no | The physical memory in use, in bytes. | | `mountpoints.*` | `map` | yes | A mounted filesystem, keyed by mount path. | | `networking.dhcp` | `string` | no | The DHCP server of the primary interface, when known. | | `networking.domain` | `string` | yes | The DNS domain of the host, when one is configured. | | `networking.fqdn` | `string` | no | The fully qualified domain name of the host. | -| `networking.hostname` | `string` | no | The short host name of the machine. | +| `networking.hostname` | `string` | yes | The short host name of the machine. | | `networking.interfaces.*` | `map` | yes | A network interface, keyed by interface name. | | `networking.interfaces.*.bindings` | `array` | yes | The IPv4 bindings of the interface (address, netmask, network). | | `networking.interfaces.*.bindings6` | `array` | yes | The IPv6 bindings of the interface (address, netmask, network, scope6, flags). | @@ -112,20 +112,20 @@ $ facts --json | `networking.interfaces.*.network` | `string` | yes | The IPv4 network of the interface's first binding. | | `networking.interfaces.*.network6` | `string` | yes | The IPv6 network of the interface's first binding. | | `networking.interfaces.*.scope6` | `string` | yes | The IPv6 scope of the interface's first binding, such as global or link. | -| `networking.ip` | `string` | no | The IPv4 address of the primary interface. | +| `networking.ip` | `string` | yes | The IPv4 address of the primary interface. | | `networking.ip6` | `string` | yes | The IPv6 address of the primary interface. | -| `networking.mac` | `string` | no | The MAC address of the primary interface. | +| `networking.mac` | `string` | yes | The MAC address of the primary interface. | | `networking.mtu` | `integer` | yes | The maximum transmission unit of the primary interface. | -| `networking.netmask` | `string` | no | The IPv4 netmask of the primary interface. | +| `networking.netmask` | `string` | yes | The IPv4 netmask of the primary interface. | | `networking.netmask6` | `string` | yes | The IPv6 netmask of the primary interface. | -| `networking.network` | `string` | no | The IPv4 network of the primary interface. | +| `networking.network` | `string` | yes | The IPv4 network of the primary interface. | | `networking.network6` | `string` | yes | The IPv6 network of the primary interface. | -| `networking.primary` | `string` | no | The name of the primary interface. | +| `networking.primary` | `string` | yes | The name of the primary interface. | | `networking.scope6` | `string` | yes | The IPv6 scope of the primary interface, such as global or link. | | `os.architecture` | `string` | no | The operating system's hardware architecture, such as x86_64 or arm64. | -| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, or windows. | +| `os.family` | `string` | no | The operating system family, such as Debian, RedHat, Darwin, windows, or Plan 9. | | `os.hardware` | `string` | no | The hardware model of the machine, such as x86_64. | -| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, or windows. | +| `os.name` | `string` | no | The operating system name, such as Ubuntu, Darwin, windows, or Plan 9. | | `os.release.full` | `string` | no | The full release number of the operating system. | | `os.release.major` | `string` | no | The major release number of the operating system. | | `os.windows.display_version` | `string` | yes | The Windows display version, such as 24H2 (Windows 10 20H2 and later). | @@ -136,10 +136,10 @@ $ facts --json | `os.windows.system32` | `string` | no | The native system32 directory, sysnative-aware for 32-bit processes. | | `path` | `array` | no | The PATH environment entries of the Facts process, in lookup order. | | `processors.cores` | `integer` | no | The number of cores per processor socket. | -| `processors.count` | `integer` | no | The number of logical processors. | +| `processors.count` | `integer` | yes | The number of logical processors. | | `processors.extensions` | `array` | no | The instruction set architectures the processors support, including the base architecture and x86_64 microarchitecture levels. | -| `processors.isa` | `string` | no | The processor instruction set architecture, as reported by uname -p. | -| `processors.models` | `array` | no | The processor model strings, one entry per logical processor. | +| `processors.isa` | `string` | yes | The processor instruction set architecture, as reported by the platform. | +| `processors.models` | `array` | yes | The processor model strings, one entry per logical processor. | | `processors.physicalcount` | `integer` | no | The number of physical processor sockets. | | `processors.threads` | `integer` | no | The number of hardware threads per core. | | `ssh` | `map` | yes | The structured SSH host key tree, when host keys are visible. | @@ -147,9 +147,9 @@ $ facts --json | `ssh.*.fingerprints.sha256` | `string` | yes | The SSHFP SHA-256 fingerprint of the SSH host key, keyed by key algorithm. | | `ssh.*.key` | `string` | yes | The public SSH host key, keyed by key algorithm. | | `ssh.*.type` | `string` | yes | The SSH host key type, such as ssh-ed25519, keyed by key algorithm. | -| `system_uptime.days` | `integer` | no | The whole days the system has been up. | -| `system_uptime.hours` | `integer` | no | The whole hours the system has been up. | -| `system_uptime.seconds` | `integer` | no | The seconds the system has been up. | -| `system_uptime.uptime` | `string` | no | The display form of the system uptime, such as 3 days. | +| `system_uptime.days` | `integer` | yes | The whole days the system has been up. | +| `system_uptime.hours` | `integer` | yes | The whole hours the system has been up. | +| `system_uptime.seconds` | `integer` | yes | The seconds the system has been up. | +| `system_uptime.uptime` | `string` | yes | The display form of the system uptime, such as 3 days. | | `timezone` | `string` | no | The abbreviated time zone name of the system, such as UTC. | | `virtual` | `string` | no | The hypervisor or container technology the machine runs under, or physical. | diff --git a/internal/engine/core.go b/internal/engine/core.go index b201ec34..aaa16abf 100644 --- a/internal/engine/core.go +++ b/internal/engine/core.go @@ -37,7 +37,7 @@ func buildCoreFacts(s *Session) []ResolvedFact { facts := []ResolvedFact{ {Name: "facterversion", Value: Version}, {Name: "is_virtual", Value: isVirtualFact}, - {Name: "path", Value: pathEntries(os.Getenv("PATH"))}, + {Name: "path", Value: currentPathEntries(runtime.GOOS, os.Getenv)}, {Name: "virtual", Value: virtualFact}, } facts = append(facts, networkingCoreFacts(s)...) @@ -62,12 +62,26 @@ func buildCoreFacts(s *Session) []ResolvedFact { return facts } +func currentPathEntries(goos string, getenv func(string) string) []string { + key := "PATH" + separator := string(os.PathListSeparator) + if goos == "plan9" { + key = "path" + separator = "\x00" + } + return pathEntriesWithSeparator(getenv(key), separator) +} + // pathEntries splits a raw PATH value into an ordered array of entries on the // host platform's path-list separator, preserving entry order and dropping // empty entries. func pathEntries(raw string) []string { + return pathEntriesWithSeparator(raw, string(os.PathListSeparator)) +} + +func pathEntriesWithSeparator(raw, separator string) []string { entries := make([]string, 0) - for _, entry := range strings.Split(raw, string(os.PathListSeparator)) { + for _, entry := range strings.Split(raw, separator) { if entry == "" { continue } diff --git a/internal/engine/core_test.go b/internal/engine/core_test.go index 24c704a5..2061a544 100644 --- a/internal/engine/core_test.go +++ b/internal/engine/core_test.go @@ -34,6 +34,21 @@ func TestCoreFacts_includePathFromEnvironment(t *testing.T) { } } +func TestCurrentPathEntriesUsesPlan9PathEnvironment(t *testing.T) { + t.Parallel() + + got := currentPathEntries("plan9", func(key string) string { + if key != "path" { + t.Fatalf("getenv key = %q, want path", key) + } + return "/bin\x00." + }) + want := []string{"/bin", "."} + if !reflect.DeepEqual(got, want) { + t.Fatalf("currentPathEntries(plan9) = %#v, want %#v", got, want) + } +} + func TestPathEntries_splitsAndDropsEmpty(t *testing.T) { t.Parallel() diff --git a/internal/engine/identity.go b/internal/engine/identity.go index 232760ed..09cc273a 100644 --- a/internal/engine/identity.go +++ b/internal/engine/identity.go @@ -101,6 +101,9 @@ func numericIdentityValue(value string) any { // identityCoreFacts assembles the identity category fact (the current user, // group, and privilege state) for the current host. func identityCoreFacts(s *Session) []ResolvedFact { + if runtime.GOOS == "plan9" { + return nil + } return []ResolvedFact{ {Name: "identity", Value: s.cachedIdentity()}, } diff --git a/internal/engine/memory.go b/internal/engine/memory.go index 9b4fa9c8..9f628cca 100644 --- a/internal/engine/memory.go +++ b/internal/engine/memory.go @@ -78,6 +78,8 @@ func probeTotalPhysicalMemoryBytes(s *Session) int { return parseLinuxMeminfoBytes(s.cachedLinuxMeminfo(), "MemTotal") case "windows": return s.cachedWindowsMemory().TotalBytes + case "plan9": + return parsePlan9SwapMemoryTotal(readText("/dev/swap", s.readFile)) } return 0 } @@ -586,6 +588,9 @@ func parseDarwinMemoryAmountBytes(input string) int { // current host. func memoryCoreFacts(s *Session) []ResolvedFact { memoryTotalBytes := s.cachedTotalPhysicalMemoryBytes() + if runtime.GOOS == "plan9" { + return plan9MemoryCoreFacts(memoryTotalBytes) + } memoryAvailableBytes := s.cachedAvailablePhysicalMemoryBytes() memoryUsedBytes := max(0, memoryTotalBytes-memoryAvailableBytes) swapTotalBytes := s.cachedTotalSwapMemoryBytes() diff --git a/internal/engine/networking.go b/internal/engine/networking.go index ba4e1008..cb7f4768 100644 --- a/internal/engine/networking.go +++ b/internal/engine/networking.go @@ -128,7 +128,7 @@ func networkingDHCPFact(interfaces map[string]any, primaryIP string) string { func networkingDHCPValue(goos string, interfaces map[string]any, primaryIP string) any { dhcp := networkingDHCPFact(interfaces, primaryIP) - if goos == "netbsd" && dhcp == "" { + if (goos == "netbsd" || goos == "plan9") && dhcp == "" { return nil } return dhcp @@ -168,6 +168,9 @@ func networkingInterfaces(s *Session) map[string]any { } func networkingInterfacesForPlatform(s *Session, goos string, snapshotProvider func() ([]networkInterfaceSnapshot, error)) map[string]any { + if goos == "plan9" { + return currentPlan9Interfaces(s.readFile, filepath.Glob) + } snapshots, err := snapshotProvider() if err != nil { if goos == "windows" { @@ -391,6 +394,9 @@ func currentNetworkingData(goos string, interfaces map[string]any, run commandRu case "linux": expandInterfaceBindings(interfaces) return linuxPrimaryInterface(readText("/proc/net/route", readFile), interfaces, run), interfaces + case "plan9": + expandInterfaceBindings(interfaces) + return plan9PrimaryInterface(readText("/net/iproute", readFile), interfaces), interfaces default: return "", interfaces } @@ -1634,6 +1640,9 @@ func ipFromAddr(addr net.Addr) (net.IP, bool) { // domain, interfaces, primary interface and address selection, DHCP, and the // IPv4/IPv6 binding facts) for the current host. func networkingCoreFacts(s *Session) []ResolvedFact { + if runtime.GOOS == "plan9" { + return plan9NetworkingCoreFacts(s) + } nodeName, nodeNameValue := hostName(s) resolvedFQDN := fqdn(nodeName) hostname, fqdn, domain := currentHostnameFacts(runtime.GOOS, nodeName, resolvedFQDN, "/etc/resolv.conf", s.readFile) diff --git a/internal/engine/os.go b/internal/engine/os.go index 5354b9f7..b26945ba 100644 --- a/internal/engine/os.go +++ b/internal/engine/os.go @@ -163,6 +163,9 @@ func currentArchitectureName(goos, machine string) string { } func probeKernelRelease(s *Session) string { + if runtime.GOOS == "plan9" { + return "" + } return strings.TrimSpace(s.commandOutput("uname", "-r")) } @@ -170,6 +173,9 @@ func probeHardwareModel(s *Session) string { if runtime.GOOS == "windows" { return windowsHardwareFromGoArch(runtime.GOARCH) } + if runtime.GOOS == "plan9" { + return plan9Architecture(s.readFile, runtime.GOARCH) + } out := s.commandOutput("uname", "-m") if out == "" { return runtime.GOARCH @@ -229,6 +235,8 @@ func currentOSRelease(s *Session, goos string, readFile fileReader, run commandR if release := parseIllumosRelease(readFileString("/etc/release", readFile)).Release; len(release) > 0 { return release } + case "plan9": + return nil case "darwin": return parseDarwinOSRelease(run("uname", "-r")) case "windows": @@ -1738,6 +1746,8 @@ func osFamily(goos string, distro linuxDistro) string { return "DragonFly" case "illumos": return "illumos" + case "plan9": + return "Plan 9" default: return goos } @@ -1773,6 +1783,8 @@ func osName(goos string, distro linuxDistro) string { return distro.Name } return "illumos" + case "plan9": + return "Plan 9" default: return goos } @@ -1796,6 +1808,8 @@ func kernelName(goos string) string { return "DragonFly" case "illumos": return "SunOS" + case "plan9": + return "Plan 9" default: return goos } @@ -1838,6 +1852,15 @@ func osCoreFacts(s *Session) []ResolvedFact { osFamily := osFamily(runtime.GOOS, linuxDistro) osName := osName(runtime.GOOS, linuxDistro) kernelName := kernelName(runtime.GOOS) + if runtime.GOOS == "plan9" { + return []ResolvedFact{ + {Name: "os.architecture", Value: architecture}, + {Name: "os.family", Value: osFamily}, + {Name: "os.hardware", Value: hardwareModel}, + {Name: "os.name", Value: osName}, + {Name: "kernel.name", Value: kernelName}, + } + } kernelRelease := s.cachedKernelRelease() osRelease := s.cachedOSRelease() kernelVersion := kernelVersionFact(runtime.GOOS, kernelRelease, "") diff --git a/internal/engine/plan9.go b/internal/engine/plan9.go new file mode 100644 index 00000000..64e31936 --- /dev/null +++ b/internal/engine/plan9.go @@ -0,0 +1,316 @@ +package engine + +import ( + "net" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +func parsePlan9Sysname(input string) string { + return plan9CleanString(input) +} + +func plan9Architecture(readFile fileReader, fallback string) string { + if architecture := plan9EnvValue("/env/objtype", readFile); architecture != "" { + return architecture + } + return fallback +} + +func plan9ProcessorISA(readFile fileReader, fallback string) string { + if isa := plan9EnvValue("/env/cputype", readFile); isa != "" { + return isa + } + if isa := plan9EnvValue("/env/objtype", readFile); isa != "" { + return isa + } + return fallback +} + +func plan9EnvValue(path string, readFile fileReader) string { + data, err := readFile(path) + if err != nil { + return "" + } + return plan9CleanString(string(data)) +} + +func plan9CleanString(input string) string { + input = strings.ReplaceAll(input, "\x00", "") + return strings.TrimSpace(input) +} + +func parsePlan9SwapMemoryTotal(input string) int { + for line := range strings.Lines(input) { + fields := strings.Fields(line) + if len(fields) != 2 || fields[1] != "memory" { + continue + } + total, err := strconv.Atoi(fields[0]) + if err == nil && total > 0 { + return total + } + } + return 0 +} + +func parsePlan9SysstatProcessorCount(input string) int { + count := 0 + for line := range strings.Lines(input) { + if strings.TrimSpace(line) != "" { + count++ + } + } + return count +} + +func parsePlan9ProcessorModels(cputype, archctl string, count int) []string { + model := plan9CleanString(cputype) + if model == "" { + model = parsePlan9ArchctlCPUModel(archctl) + } + if model == "" { + return nil + } + if count <= 0 { + count = 1 + } + models := make([]string, count) + for i := range models { + models[i] = model + } + return models +} + +func parsePlan9ArchctlCPUModel(input string) string { + for line := range strings.Lines(input) { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "cpu ") { + continue + } + fields := strings.Fields(strings.TrimPrefix(line, "cpu ")) + if len(fields) == 0 { + return "" + } + lastModelField := len(fields) - 1 + for i := len(fields) - 1; i >= 0; i-- { + if _, err := strconv.Atoi(fields[i]); err == nil { + lastModelField = i + break + } + } + return strings.Join(fields[:lastModelField+1], " ") + } + return "" +} + +func currentPlan9ProcessorInfo(readFile fileReader) processorInfo { + count := parsePlan9SysstatProcessorCount(readText("/dev/sysstat", readFile)) + return processorInfo{ + LogicalCount: count, + Models: parsePlan9ProcessorModels( + readText("/dev/cputype", readFile), + readText("/dev/archctl", readFile), + count, + ), + } +} + +func parsePlan9IPIFCStatus(input string) map[string]any { + interfaces := map[string]any{} + name := "" + for line := range strings.Lines(input) { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "device" { + deviceName := path.Base(fields[1]) + if deviceName == "." || deviceName == "/" { + name = "" + continue + } + name = deviceName + if _, ok := interfaces[name]; !ok { + interfaces[name] = map[string]any{} + } + continue + } + if name == "" || len(fields) < 2 { + continue + } + ip := net.ParseIP(fields[0]).To4() + if ip == nil { + continue + } + prefix, err := strconv.Atoi(strings.TrimPrefix(fields[1], "/")) + if err != nil { + continue + } + prefix = plan9IPv4Prefix(prefix) + if prefix < 0 || prefix > 32 { + continue + } + iface, _ := interfaces[name].(map[string]any) + bindings, _ := iface["bindings"].([]any) + bindings = append(bindings, interfaceBinding(ip, &net.IPNet{IP: ip, Mask: net.CIDRMask(prefix, 32)})) + iface["bindings"] = bindings + } + if len(interfaces) == 0 { + return nil + } + return interfaces +} + +func plan9IPv4Prefix(prefix int) int { + if prefix >= 96 { + return prefix - 96 + } + return prefix +} + +func parsePlan9MACAddress(input string) string { + value := strings.ToLower(plan9CleanString(input)) + value = strings.ReplaceAll(value, "-", "") + value = strings.ReplaceAll(value, ":", "") + if len(value) != 12 { + return "" + } + parts := make([]string, 0, 6) + for i := 0; i < len(value); i += 2 { + part := value[i : i+2] + if _, err := strconv.ParseUint(part, 16, 8); err != nil { + return "" + } + parts = append(parts, part) + } + return strings.Join(parts, ":") +} + +func parsePlan9PrimaryRouteIP(input string) string { + candidate := "" + for line := range strings.Lines(input) { + fields := strings.Fields(line) + if len(fields) < 8 || fields[0] != "0.0.0.0" { + continue + } + source := fields[6] + ip := net.ParseIP(source).To4() + if ip == nil || ip.Equal(net.IPv4zero) { + continue + } + if fields[7] == "/128" { + return source + } + if candidate == "" { + candidate = source + } + } + return candidate +} + +func currentPlan9Interfaces(readFile fileReader, glob pathGlobber) map[string]any { + paths, err := glob("/net/ipifc/*/status") + if err != nil { + return nil + } + interfaces := map[string]any{} + for _, statusPath := range paths { + mergeMissingInterfaceFacts(interfaces, parsePlan9IPIFCStatus(readText(statusPath, readFile))) + } + for name, value := range interfaces { + iface, ok := value.(map[string]any) + if !ok { + continue + } + if mac := parsePlan9MACAddress(readText("/net/"+name+"/addr", readFile)); mac != "" { + iface["mac"] = mac + } + } + if len(interfaces) == 0 { + return nil + } + return interfaces +} + +func plan9PrimaryInterface(iproute string, interfaces map[string]any) string { + if primaryIP := parsePlan9PrimaryRouteIP(iproute); primaryIP != "" { + if primary := primaryInterface(interfaces, primaryIP); primary != "" { + return primary + } + } + return firstNonIgnoredInterface(interfaces) +} + +func plan9MemoryCoreFacts(totalBytes int) []ResolvedFact { + if totalBytes <= 0 { + return nil + } + return []ResolvedFact{ + {Name: "memory.system.total", Value: bytesToHumanReadable(totalBytes)}, + {Name: "memory.system.total_bytes", Value: totalBytes}, + } +} + +func plan9ProcessorsCoreFacts(info processorInfo, isa string) []ResolvedFact { + facts := make([]ResolvedFact, 0, 3) + if info.LogicalCount > 0 { + facts = append(facts, ResolvedFact{Name: "processors.count", Value: info.LogicalCount}) + } + if isa != "" { + facts = append(facts, ResolvedFact{Name: "processors.isa", Value: isa}) + } + if len(info.Models) > 0 { + facts = append(facts, ResolvedFact{Name: "processors.models", Value: info.Models}) + } + return facts +} + +func plan9NetworkingCoreFacts(s *Session) []ResolvedFact { + return plan9NetworkingCoreFactsWithGlob(s, filepath.Glob) +} + +func plan9NetworkingCoreFactsWithGlob(s *Session, glob pathGlobber) []ResolvedFact { + hostname := parsePlan9Sysname(readFileString("/dev/sysname", s.readFile)) + interfaces := currentPlan9Interfaces(s.readFile, glob) + primary, interfaces := currentNetworkingData("plan9", interfaces, s.commandOutput, s.readFile) + ipv4, _ := primaryInterfaceFact(interfaces, primary, "ip").(string) + primaryBinding := primaryIPv4Binding(interfaces, ipv4) + netmask, _ := primaryBinding["netmask"].(string) + network, _ := primaryBinding["network"].(string) + mac, _ := primaryInterfaceFact(interfaces, primary, "mac").(string) + + var hostnameValue any + if hostname != "" { + hostnameValue = hostname + } + return []ResolvedFact{ + {Name: "networking.hostname", Value: hostnameValue}, + {Name: "networking.interfaces", Value: interfaces}, + {Name: "networking.ip", Value: optionalNetworkingString(ipv4)}, + {Name: "networking.mac", Value: optionalNetworkingString(mac)}, + {Name: "networking.netmask", Value: optionalNetworkingString(netmask)}, + {Name: "networking.network", Value: optionalNetworkingString(network)}, + {Name: "networking.primary", Value: optionalNetworkingString(primary)}, + } +} + +func plan9UptimeCoreFacts(uptime uptimeInfo) []ResolvedFact { + if !uptime.Known { + return nil + } + return []ResolvedFact{ + {Name: "system_uptime.days", Value: int(uptime.Duration.Hours()) / 24}, + {Name: "system_uptime.hours", Value: int(uptime.Duration.Hours())}, + {Name: "system_uptime.seconds", Value: int(uptime.Duration.Seconds())}, + {Name: "system_uptime.uptime", Value: uptimeString(uptime)}, + } +} + +func currentPlan9Uptime(run commandRunner) uptimeInfo { + seconds := parseUptimeCommandSeconds(run("uptime")) + if seconds <= 0 { + return uptimeInfo{} + } + return uptimeInfo{Duration: time.Duration(seconds) * time.Second, Known: true} +} diff --git a/internal/engine/plan9_existing_test.go b/internal/engine/plan9_existing_test.go new file mode 100644 index 00000000..7da88f4e --- /dev/null +++ b/internal/engine/plan9_existing_test.go @@ -0,0 +1,91 @@ +package engine + +import ( + "os" + "reflect" + "testing" +) + +func TestPlan9IdentityNamesUseCanonicalSpelling(t *testing.T) { + t.Parallel() + + if got := osFamily("plan9", linuxDistro{}); got != "Plan 9" { + t.Fatalf("osFamily(plan9) = %q, want Plan 9", got) + } + if got := osName("plan9", linuxDistro{}); got != "Plan 9" { + t.Fatalf("osName(plan9) = %q, want Plan 9", got) + } + if got := kernelName("plan9"); got != "Plan 9" { + t.Fatalf("kernelName(plan9) = %q, want Plan 9", got) + } +} + +func TestCurrentOSReleasePlan9OmitsOSVersionProtocol(t *testing.T) { + t.Parallel() + + s := NewSession() + s.host = &fakeHostOS{runOutput: "2000\n"} + readFile := func(path string) ([]byte, error) { + if path != "/dev/osversion" { + return nil, os.ErrNotExist + } + return []byte("2000\n"), nil + } + + if got := currentOSRelease(s, "plan9", readFile, func(string, ...string) string { return "" }); got != nil { + t.Fatalf("currentOSRelease(plan9) = %#v, want nil", got) + } +} + +func TestCurrentLoadAveragesPlan9OmitsLoadAverages(t *testing.T) { + t.Parallel() + + got := currentLoadAverages("plan9", func(string) ([]byte, error) { + return []byte("0.00 0.01 0.02\n"), nil + }, func(string, ...string) string { + return "cirno up 0 days, 01:35:26" + }) + if got != nil { + t.Fatalf("currentLoadAverages(plan9) = %#v, want nil", got) + } +} + +func TestParseUptimeCommandSecondsPlan9Format(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want int + }{ + {name: "days and clock", input: "cirno up 0 days, 01:35:26", want: 5726}, + {name: "clock only", input: "cirno up 23:15:17", want: 83717}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := parseUptimeCommandSeconds(tt.input); got != tt.want { + t.Fatalf("parseUptimeCommandSeconds(%q) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + +func TestCurrentUptimeInfoPlan9UsesNativeUptimeFormat(t *testing.T) { + t.Parallel() + + got := currentUptimeInfo(testSession, "plan9", func(path string) ([]byte, error) { + return nil, os.ErrNotExist + }, func(name string, args ...string) string { + if name != "uptime" { + return "" + } + return "cirno up 0 days, 01:35:26" + }, nil) + + want := uptimeInfo{Duration: 5726 * 1_000_000_000, Known: true} + if !reflect.DeepEqual(got, want) { + t.Fatalf("currentUptimeInfo(plan9) = %#v, want %#v", got, want) + } +} diff --git a/internal/engine/plan9_parser_test.go b/internal/engine/plan9_parser_test.go new file mode 100644 index 00000000..d0e46851 --- /dev/null +++ b/internal/engine/plan9_parser_test.go @@ -0,0 +1,275 @@ +package engine + +import ( + "os" + "path/filepath" + "reflect" + "testing" +) + +func plan9Fixture(t *testing.T, name string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join("testdata", "plan9", name)) + if err != nil { + t.Fatal(err) + } + return string(data) +} + +func TestPlan9HostnameTrimsSysname(t *testing.T) { + t.Parallel() + + if got := parsePlan9Sysname(plan9Fixture(t, "sysname")); got != "cirno" { + t.Fatalf("parsePlan9Sysname() = %q, want cirno", got) + } + if got := parsePlan9Sysname("\n"); got != "" { + t.Fatalf("parsePlan9Sysname(empty) = %q, want empty", got) + } +} + +func TestPlan9ArchitecturePrefersObjtypeEnvFile(t *testing.T) { + t.Parallel() + + readFile := func(path string) ([]byte, error) { + if path != "/env/objtype" { + return nil, os.ErrNotExist + } + return []byte("amd64\x00"), nil + } + if got := plan9Architecture(readFile, "arm64"); got != "amd64" { + t.Fatalf("plan9Architecture() = %q, want amd64", got) + } + if got := plan9Architecture(func(string) ([]byte, error) { return nil, os.ErrNotExist }, "arm64"); got != "arm64" { + t.Fatalf("plan9Architecture(missing) = %q, want runtime fallback", got) + } +} + +func TestPlan9ProcessorISAPrefersCputypeThenObjtype(t *testing.T) { + t.Parallel() + + readFile := func(path string) ([]byte, error) { + switch path { + case "/env/cputype": + return []byte("386\x00"), nil + case "/env/objtype": + return []byte("amd64\x00"), nil + default: + return nil, os.ErrNotExist + } + } + if got := plan9ProcessorISA(readFile, "arm64"); got != "386" { + t.Fatalf("plan9ProcessorISA() = %q, want cputype 386", got) + } + + readFile = func(path string) ([]byte, error) { + switch path { + case "/env/cputype": + return nil, os.ErrNotExist + case "/env/objtype": + return []byte("amd64\x00"), nil + default: + return nil, os.ErrNotExist + } + } + if got := plan9ProcessorISA(readFile, "arm64"); got != "amd64" { + t.Fatalf("plan9ProcessorISA(cputype missing) = %q, want objtype amd64", got) + } +} + +func TestParsePlan9SwapMemoryTotal(t *testing.T) { + t.Parallel() + + if got := parsePlan9SwapMemoryTotal(plan9Fixture(t, "swap")); got != 1_067_843_584 { + t.Fatalf("parsePlan9SwapMemoryTotal() = %d, want 1067843584", got) + } + if got := parsePlan9SwapMemoryTotal("4096 pagesize\n"); got != 0 { + t.Fatalf("parsePlan9SwapMemoryTotal(no memory) = %d, want 0", got) + } +} + +func TestParsePlan9SysstatProcessorCount(t *testing.T) { + t.Parallel() + + if got := parsePlan9SysstatProcessorCount(plan9Fixture(t, "sysstat")); got != 1 { + t.Fatalf("parsePlan9SysstatProcessorCount() = %d, want 1", got) + } + if got := parsePlan9SysstatProcessorCount("0 1 2\n\n1 2 3\n"); got != 2 { + t.Fatalf("parsePlan9SysstatProcessorCount(two lines) = %d, want 2", got) + } +} + +func TestParsePlan9ProcessorModels(t *testing.T) { + t.Parallel() + + got := parsePlan9ProcessorModels(plan9Fixture(t, "cputype"), plan9Fixture(t, "archctl"), 1) + want := []string{"Core 2/Xeon 3600"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("parsePlan9ProcessorModels() = %#v, want %#v", got, want) + } + + got = parsePlan9ProcessorModels("", plan9Fixture(t, "archctl"), 2) + want = []string{"Core 2/Xeon 3600", "Core 2/Xeon 3600"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("parsePlan9ProcessorModels(archctl) = %#v, want %#v", got, want) + } +} + +func TestParsePlan9IPIFCStatus(t *testing.T) { + t.Parallel() + + got := parsePlan9IPIFCStatus(plan9Fixture(t, "ipifc_status")) + want := map[string]any{ + "ether0": map[string]any{ + "bindings": []any{map[string]any{ + "address": "192.168.122.163", + "netmask": "255.255.255.0", + "network": "192.168.122.0", + }}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("parsePlan9IPIFCStatus() = %#v, want %#v", got, want) + } +} + +func TestParsePlan9IPIFCStatusSkipsInvalidDevicePath(t *testing.T) { + t.Parallel() + + got := parsePlan9IPIFCStatus("device .\n\t192.0.2.10 /120 192.0.2.0 0 0\n") + if got != nil { + t.Fatalf("parsePlan9IPIFCStatus(invalid device) = %#v, want nil", got) + } +} + +func TestPlan9IPv4MappedPrefixConversion(t *testing.T) { + t.Parallel() + + if got := plan9IPv4Prefix(120); got != 24 { + t.Fatalf("plan9IPv4Prefix(120) = %d, want 24", got) + } + if got := plan9IPv4Prefix(24); got != 24 { + t.Fatalf("plan9IPv4Prefix(24) = %d, want 24", got) + } +} + +func TestParsePlan9MACAddress(t *testing.T) { + t.Parallel() + + if got := parsePlan9MACAddress(plan9Fixture(t, "ether0_addr")); got != "52:54:00:76:cc:6d" { + t.Fatalf("parsePlan9MACAddress() = %q, want colon-separated MAC", got) + } +} + +func TestParsePlan9PrimaryRouteIP(t *testing.T) { + t.Parallel() + + if got := parsePlan9PrimaryRouteIP(plan9Fixture(t, "iproute")); got != "192.168.122.163" { + t.Fatalf("parsePlan9PrimaryRouteIP() = %q, want 192.168.122.163", got) + } +} + +func TestCurrentPlan9InterfacesMergesStatusAndMAC(t *testing.T) { + t.Parallel() + + files := map[string][]byte{ + "/net/ipifc/0/status": []byte(plan9Fixture(t, "ipifc_status")), + "/net/ether0/addr": []byte(plan9Fixture(t, "ether0_addr")), + } + got := currentPlan9Interfaces(func(path string) ([]byte, error) { + data, ok := files[path] + if !ok { + return nil, os.ErrNotExist + } + return data, nil + }, func(pattern string) ([]string, error) { + if pattern != "/net/ipifc/*/status" { + t.Fatalf("glob pattern = %q, want /net/ipifc/*/status", pattern) + } + return []string{"/net/ipifc/0/status"}, nil + }) + + want := map[string]any{ + "ether0": map[string]any{ + "mac": "52:54:00:76:cc:6d", + "bindings": []any{map[string]any{ + "address": "192.168.122.163", + "netmask": "255.255.255.0", + "network": "192.168.122.0", + }}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("currentPlan9Interfaces() = %#v, want %#v", got, want) + } +} + +func TestPlan9MemoryCoreFactsEmitOnlyTotal(t *testing.T) { + t.Parallel() + + got := plan9MemoryCoreFacts(1_067_843_584) + want := []ResolvedFact{ + {Name: "memory.system.total", Value: "1018.38 MiB"}, + {Name: "memory.system.total_bytes", Value: 1_067_843_584}, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("plan9MemoryCoreFacts() = %#v, want %#v", got, want) + } +} + +func TestPlan9ProcessorsCoreFactsEmitOnlyFirstSlice(t *testing.T) { + t.Parallel() + + info := processorInfo{LogicalCount: 1, Models: []string{"Core 2/Xeon 3600"}} + got := plan9ProcessorsCoreFacts(info, "amd64") + want := []ResolvedFact{ + {Name: "processors.count", Value: 1}, + {Name: "processors.isa", Value: "amd64"}, + {Name: "processors.models", Value: []string{"Core 2/Xeon 3600"}}, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("plan9ProcessorsCoreFacts() = %#v, want %#v", got, want) + } +} + +func TestPlan9NetworkingCoreFactsEmitFirstSliceOnly(t *testing.T) { + t.Parallel() + + files := map[string][]byte{ + "/dev/sysname": []byte(plan9Fixture(t, "sysname")), + "/net/ipifc/0/status": []byte(plan9Fixture(t, "ipifc_status")), + "/net/ether0/addr": []byte(plan9Fixture(t, "ether0_addr")), + "/net/iproute": []byte(plan9Fixture(t, "iproute")), + } + s := NewSession() + s.host = &fakeHostOS{files: files} + + facts := plan9NetworkingCoreFactsWithGlob(s, func(pattern string) ([]string, error) { + if pattern != "/net/ipifc/*/status" { + t.Fatalf("glob pattern = %q, want /net/ipifc/*/status", pattern) + } + return []string{"/net/ipifc/0/status"}, nil + }) + got := Collection(facts) + networking, ok := got["networking"].(map[string]any) + if !ok { + t.Fatalf("networking facts = %#v, want map", got["networking"]) + } + + for key, want := range map[string]any{ + "hostname": "cirno", + "primary": "ether0", + "ip": "192.168.122.163", + "netmask": "255.255.255.0", + "network": "192.168.122.0", + "mac": "52:54:00:76:cc:6d", + } { + if networking[key] != want { + t.Fatalf("networking.%s = %#v, want %#v in %#v", key, networking[key], want, networking) + } + } + for _, key := range []string{"dhcp", "mtu", "fqdn", "domain", "ip6"} { + if value, ok := networking[key]; ok { + t.Fatalf("networking.%s = %#v, want omitted", key, value) + } + } +} diff --git a/internal/engine/processors.go b/internal/engine/processors.go index f206f1cf..fcc68c4f 100644 --- a/internal/engine/processors.go +++ b/internal/engine/processors.go @@ -27,6 +27,9 @@ func currentProcessorISA(s *Session, goos, fallback string, run commandRunner) s } return "" } + if goos == "plan9" { + return plan9ProcessorISA(s.readFile, fallback) + } processor := strings.TrimSpace(run("uname", "-p")) if processor == "" || processor == "unknown" { return fallback @@ -150,7 +153,7 @@ func probeProcessorModels(s *Session) []string { return models } } - case "freebsd", "netbsd", "openbsd", "dragonfly", "illumos", "windows": + case "freebsd", "netbsd", "openbsd", "dragonfly", "illumos", "windows", "plan9": models := s.cachedPlatformProcessorInfo().Models if len(models) > 0 { return append([]string(nil), models...) @@ -188,6 +191,9 @@ func probeProcessorTopology(s *Session) (int, int) { } func probePlatformProcessorInfo(s *Session) processorInfo { + if runtime.GOOS == "plan9" { + return currentPlan9ProcessorInfo(s.readFile) + } return currentProcessorInfo(runtime.GOOS, s.commandOutput, s.logr()) } @@ -600,6 +606,9 @@ func hertzToHumanReadable(hz any) string { // current host. func processorsCoreFacts(s *Session) []ResolvedFact { architecture := s.cachedArchitectureName() + if runtime.GOOS == "plan9" { + return plan9ProcessorsCoreFacts(s.cachedPlatformProcessorInfo(), currentProcessorISA(s, runtime.GOOS, architecture, s.commandOutput)) + } platformProcessors := processorInfo{} if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" || runtime.GOOS == "dragonfly" || runtime.GOOS == "illumos" || runtime.GOOS == "windows" { platformProcessors = s.cachedPlatformProcessorInfo() diff --git a/internal/engine/testdata/plan9/archctl b/internal/engine/testdata/plan9/archctl new file mode 100644 index 00000000..0ee2c1ea --- /dev/null +++ b/internal/engine/testdata/plan9/archctl @@ -0,0 +1,11 @@ +cpu Core 2/Xeon 3600 pge +pge on +coherence mfence +cmpswap cmpswap486 +arch ACPI +cache 0x0000000000000000 655360 wb +cache 0x00000000000a0000 131072 uc +cache 0x00000000000c0000 262144 wp +cache 0x0000000000100000 2146435072 wb +cache 0x0000000080000000 2147483648 uc +cache 0x0000000100000000 545460846592 wb diff --git a/internal/engine/testdata/plan9/cputype b/internal/engine/testdata/plan9/cputype new file mode 100644 index 00000000..0c3de231 --- /dev/null +++ b/internal/engine/testdata/plan9/cputype @@ -0,0 +1 @@ +Core 2/Xeon 3600 diff --git a/internal/engine/testdata/plan9/date b/internal/engine/testdata/plan9/date new file mode 100644 index 00000000..a91b94fa --- /dev/null +++ b/internal/engine/testdata/plan9/date @@ -0,0 +1 @@ +Sun Jun 21 10:08:19 CET 2026 diff --git a/internal/engine/testdata/plan9/ether0_addr b/internal/engine/testdata/plan9/ether0_addr new file mode 100644 index 00000000..8b0409f9 --- /dev/null +++ b/internal/engine/testdata/plan9/ether0_addr @@ -0,0 +1 @@ +52540076cc6d diff --git a/internal/engine/testdata/plan9/ipifc_status b/internal/engine/testdata/plan9/ipifc_status new file mode 100644 index 00000000..9c0c4abc --- /dev/null +++ b/internal/engine/testdata/plan9/ipifc_status @@ -0,0 +1,2 @@ +device /net/ether0 maxtu 1514 sendra 0 recvra 0 mflag 0 oflag 0 maxraint 600000 minraint 200000 linkmtu 0 reachtime 0... + 192.168.122.163 /120 192.168.122.0 4294967295 4294967295 diff --git a/internal/engine/testdata/plan9/iproute b/internal/engine/testdata/plan9/iproute new file mode 100644 index 00000000..0ead4510 --- /dev/null +++ b/internal/engine/testdata/plan9/iproute @@ -0,0 +1,9 @@ +0.0.0.0 /96 192.168.122.1 4 dhcp 0 192.168.122.0 /120 +0.0.0.0 /96 192.168.122.1 4 dhcp 0 192.168.122.163 /128 +224.0.0.1 /128 224.0.0.1 4m ifc 0 192.168.122.163 /128 +192.168.122.0 /120 192.168.122.0 4i ifc 0 0.0.0.0 /96 +192.168.122.0 /120 192.168.122.0 4i ifc 0 192.168.122.163 /128 +192.168.122.0 /128 192.168.122.0 4b ifc 0 192.168.122.163 /128 +192.168.122.163 /128 192.168.122.163 4u ifc 0 0.0.0.0 /96 +192.168.122.255 /128 192.168.122.255 4b ifc 0 192.168.122.163 /128 +255.255.255.255 /128 255.255.255.255 4b ifc 0 192.168.122.163 /128 diff --git a/internal/engine/testdata/plan9/ndb b/internal/engine/testdata/plan9/ndb new file mode 100644 index 00000000..6246c7e2 --- /dev/null +++ b/internal/engine/testdata/plan9/ndb @@ -0,0 +1,3 @@ +ip=192.168.122.163 ipmask=255.255.255.0 ipgw=192.168.122.1 + sys=cirno + dns=192.168.122.1 diff --git a/internal/engine/testdata/plan9/swap b/internal/engine/testdata/plan9/swap new file mode 100644 index 00000000..4db85374 --- /dev/null +++ b/internal/engine/testdata/plan9/swap @@ -0,0 +1,9 @@ +1067843584 memory +4096 pagesize +104282 kernel +4320/150312 user +0/320000 swap +13154/150312 reclaim +5821064/6112288/426728704 kernel malloc +0/0/384055834 kernel draw +608/65568/16777216 kernel secret diff --git a/internal/engine/testdata/plan9/sysname b/internal/engine/testdata/plan9/sysname new file mode 100644 index 00000000..58ef9552 --- /dev/null +++ b/internal/engine/testdata/plan9/sysname @@ -0,0 +1 @@ +cirno diff --git a/internal/engine/testdata/plan9/sysstat b/internal/engine/testdata/plan9/sysstat new file mode 100644 index 00000000..eefbf045 --- /dev/null +++ b/internal/engine/testdata/plan9/sysstat @@ -0,0 +1 @@ + 0 4163419 14305145 10777146 864106 0 0 847 71 1 diff --git a/internal/engine/testdata/plan9/timezone b/internal/engine/testdata/plan9/timezone new file mode 100644 index 00000000..b04de5af --- /dev/null +++ b/internal/engine/testdata/plan9/timezone @@ -0,0 +1,2 @@ +CET 3600 CET 7200 + 512532000 528256800 543981600 559706400 575431200 591156000 diff --git a/internal/engine/testdata/plan9/uptime b/internal/engine/testdata/plan9/uptime new file mode 100644 index 00000000..091b0602 --- /dev/null +++ b/internal/engine/testdata/plan9/uptime @@ -0,0 +1 @@ +cirno up 0 days, 23:15:17 diff --git a/internal/engine/uptime.go b/internal/engine/uptime.go index 6cae7db0..7d04149e 100644 --- a/internal/engine/uptime.go +++ b/internal/engine/uptime.go @@ -42,6 +42,9 @@ func currentUptimeInfo(s *Session, goos string, readFile fileReader, run command if goos == "windows" { return currentWindowsUptime(goos, run, s.logr()) } + if goos == "plan9" { + return currentPlan9Uptime(run) + } if goos == "linux" { virtual := detectLinuxVirtualization(currentLinuxVirtualizationInputWithCommands(s, run)) return currentLinuxUptimeInfo(readFile, run, now, virtual.Name == "docker") @@ -188,6 +191,12 @@ func parseUptimeCommandSeconds(input string) int { case "user", "users", "load", "loadavg": return seconds } + if strings.Count(field, ":") == 2 { + if clockSeconds := parseDockerElapsedTimeSeconds(field); clockSeconds > 0 { + seconds += clockSeconds + continue + } + } if hours, minutes, ok := parseUptimeHoursMinutes(field); ok { seconds += hours*3600 + minutes*60 continue @@ -310,6 +319,8 @@ func currentLoadAverages(goos string, readFile fileReader, run commandRunner) ma return emptyLoadAverages() } return parseLoadAverages(string(data)) + case "plan9": + return nil default: return emptyLoadAverages() } @@ -347,6 +358,9 @@ func emptyLoadAverages() map[string]any { // uptimeCoreFacts assembles the uptime category facts (the system_uptime fields // and the load_averages fact) for the current host. func uptimeCoreFacts(s *Session) []ResolvedFact { + if runtime.GOOS == "plan9" { + return plan9UptimeCoreFacts(s.cachedUptime()) + } uptime := s.cachedUptime() loadAverages := s.cachedLoadAverages() return []ResolvedFact{ diff --git a/internal/engine/virtual.go b/internal/engine/virtual.go index 002733d4..fa6eaddf 100644 --- a/internal/engine/virtual.go +++ b/internal/engine/virtual.go @@ -134,6 +134,8 @@ func detectVirtualization(s *Session) virtualization { return detectDMIHostVirtualization(currentIllumosVirtualizationInput(s.commandOutput)) case "windows": return detectWindowsVirtualization(currentWindowsVirtualizationInput(runtime.GOOS, s.commandOutput)) + case "plan9": + return virtualization{Unknown: true} default: return virtualization{Name: "physical"} } diff --git a/openspec/changes/add-bsd-arm-release-artifacts/proposal.md b/openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/proposal.md similarity index 100% rename from openspec/changes/add-bsd-arm-release-artifacts/proposal.md rename to openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/proposal.md diff --git a/openspec/changes/add-bsd-arm-release-artifacts/specs/go-port-ci-platform-gates/spec.md b/openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/specs/go-port-ci-platform-gates/spec.md similarity index 100% rename from openspec/changes/add-bsd-arm-release-artifacts/specs/go-port-ci-platform-gates/spec.md rename to openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/specs/go-port-ci-platform-gates/spec.md diff --git a/openspec/changes/add-bsd-arm-release-artifacts/specs/go-port-distribution-and-cutover/spec.md b/openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/specs/go-port-distribution-and-cutover/spec.md similarity index 100% rename from openspec/changes/add-bsd-arm-release-artifacts/specs/go-port-distribution-and-cutover/spec.md rename to openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/specs/go-port-distribution-and-cutover/spec.md diff --git a/openspec/changes/add-bsd-arm-release-artifacts/tasks.md b/openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/tasks.md similarity index 100% rename from openspec/changes/add-bsd-arm-release-artifacts/tasks.md rename to openspec/changes/archive/2026-06-21-add-bsd-arm-release-artifacts/tasks.md diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/.openspec.yaml b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/.openspec.yaml new file mode 100644 index 00000000..18edba1f --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-20 diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/design.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/design.md new file mode 100644 index 00000000..8c53b0cd --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/design.md @@ -0,0 +1,166 @@ +## Context + +Facts currently supports Linux, Darwin/macOS, Windows, FreeBSD, OpenBSD, NetBSD, DragonFly BSD, and illumos through a mix of cross-platform Go runtime data, OS-specific native probes, schema metadata, generated supported-facts pages, and release-gate scripts. Plan 9 is not in the schema platform vocabulary, README platform table, release artifact matrix, or platform gate set. + +The facts-lab Plan 9 guest is now reachable through the lab SSH path. Local compile-only checks have already shown that the current codebase can build for `GOOS=plan9 GOARCH=amd64`: + +- `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go build ./cmd/facts` +- `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go test -exec true ./...` + +Plan 9 should not be treated as a Unix variant. Lab probes and Plan 9 manpages show these constraints: + +- `uname` is absent. +- `sysctl` is absent. +- `/dev/sysname` provides the host name. +- `$cputype`, `$objtype`, `/dev/cputype`, and `/dev/archctl` provide CPU/architecture data. +- `/dev/swap` exposes total memory, page size, user pages, and swap pages. +- `/dev/sysstat` has one line per processor. +- `/net/ipifc/*/status`, `/net/*/addr`, `/net/iproute`, and `/net/ndb` expose network state. +- Plan 9 namespaces are per-process and do not map cleanly to Unix mountpoint/filesystem facts. +- `/dev/osversion` is the 9P protocol version, not an operating system release. + +The implementation should follow the existing Facts pattern: parse native output through small deterministic functions, test those parsers with fixtures, wire them into discovery, then validate the real binary on the native lab guest. + +## Goals / Non-Goals + +**Goals:** + +- Promote Plan 9 from "compiles" to an explicitly supported, lab-validated platform for a narrow initial fact set. +- Keep the Plan 9 fact contract honest: schema entries list `plan9` only when the fact is backed by deterministic tests and native lab validation. +- Add a tracked Plan 9 release gate using `rc`, because the guest is Plan 9 and should not depend on a POSIX shell. +- Validate the real `cmd/facts` binary on the facts-lab Plan 9 guest. +- Add `plan9/amd64` compile/build coverage once the native release gate proves the first supported fact set. +- Document unsupported or intentionally omitted Plan 9 facts so future work does not infer Unix behavior. + +**Non-Goals:** + +- Do not claim Plan 9 support for `os.release.*`, `kernel.release.*`, or `kernel.version.full` from `/dev/osversion`; that file is not an OS/kernel release. +- Do not implement mountpoint capacity, filesystem inventory, disk inventory, or partition facts in the first slice. +- Do not map Plan 9 `/dev/sysstat` load data onto the existing `load_averages` fact without a separate spec. +- Do not classify virtualization beyond safe, tested signals. +- Do not add Plan 9 cloud, DMI, FIPS, package manager, or container facts. +- Do not add unvalidated Plan 9 architectures to release artifacts just because the Go toolchain lists them. + +## Decisions + +### 1. Treat Plan 9 as an explicit platform, not a fallback `runtime.GOOS` string + +Facts should return canonical names for Plan 9 identity facts instead of letting the default branches emit raw `plan9` values inconsistently. + +Initial identity mapping: + +| Fact | Value | +| --- | --- | +| `os.name` | `Plan 9` | +| `os.family` | `Plan 9` | +| `kernel.name` | `Plan 9` | +| `networking.hostname` | trimmed contents of `/dev/sysname` | + +Alternative considered: leave the default `goos` fallback for `os.family`, `os.name`, and `kernel.name`. That is less code, but it produces lower-quality public output and creates schema/docs churn later if spelling changes. + +### 2. Do not invent release/version facts + +Plan 9 has `/dev/osversion`, but the researched source says it is the 9P protocol version. The lab value `2000` is not suitable for `os.release` or `kernel.release`. + +The first implementation must omit these Plan 9 facts: + +- `os.release` +- `os.release.full` +- `os.release.major` +- `kernel.release.full` +- `kernel.release.major` +- `kernel.version.full` + +Alternative considered: map `/dev/osversion` to `os.release.full`. That would be easy but misleading. + +### 3. Keep Plan 9 probe parsers small and fixture-backed + +Each native Plan 9 text format should have a focused parser with unit tests: + +| Surface | Native source | Parser responsibility | +| --- | --- | --- | +| Hostname | `/dev/sysname` | trim trailing newline | +| Memory | `/dev/swap` | parse total memory bytes and page size; first gate asserts total only | +| Processors | `/dev/sysstat`, `/dev/cputype`, `/dev/archctl` | count processor lines; extract model/ISA when present | +| Networking | `/net/ipifc/*/status`, `/net/*/addr`, `/net/iproute`, `/net/ndb` | parse IPv4 address, prefix/netmask/network, MAC, primary route | +| Uptime | `uptime` | parse `host up D days, HH:MM:SS` | +| Timezone | Go local time or `date`/`/env/timezone` | return the same `timezone` shape used elsewhere | + +Alternative considered: call commands inline and parse directly in fact-building functions. That is shorter initially but harder to test and more brittle under Plan 9's different command surface. + +### 4. Use Plan 9 `rc` for the release gate + +The release gate should be `tools/plan9-release-gate.rc`. It should run the real `facts` binary through structured fact queries and assert only the first supported fact set. + +The gate should avoid POSIX shell syntax and utilities that are not guaranteed on Plan 9. The lab invocation can copy the binary and gate through `facts-lab ssh plan9`, but tracked repository files should not include private lab addresses, SSH keys, or service internals. + +Alternative considered: write a POSIX `sh` gate for consistency with BSD gates. That fails the portability goal because Plan 9 is not expected to provide `sh`. + +### 5. Gate mandatory facts conservatively + +The first Plan 9 gate should require facts that are stable across normal Plan 9 installations and the lab guest: + +- `os.name` +- `os.family` +- `os.architecture` +- `os.hardware` +- `kernel.name` +- `networking.hostname` +- `networking.primary` +- `networking.ip` +- `networking.netmask` +- `networking.network` +- `networking.mac` +- `memory.system.total` +- `memory.system.total_bytes` +- `processors.count` +- `processors.isa` +- `processors.models` +- `system_uptime` +- `system_uptime.seconds` +- `timezone` +- `path` + +Facts with unresolved semantics, such as network MTU normalization, processor speed, swap usage, virtualization, and load averages, can be emitted later only after the spec and tests define their meaning. + +Alternative considered: mirror the BSD release-gate fact set. That overclaims Plan 9 support for Unix-specific surfaces and would create false confidence. + +### 6. Add release artifacts only after native validation is repeatable + +Plan 9 can be compiled by the Go toolchain for `386`, `amd64`, and `arm`, but the lab validation target is currently `plan9/amd64`. This change should allow `plan9/amd64` promotion after the release gate passes. Other Plan 9 tuples remain out of scope until there is native validation for them. + +Alternative considered: add every `go tool dist list` Plan 9 tuple to `DIST_TARGETS`. That is easy but repeats the mistake this project already avoids for unsupported Solaris/AIX-style targets. + +### 7. Use nlab as the trusted native validation surface + +Human decision on 2026-06-21: nlab is the intended Facts validation host for this work. Copying the built Plan 9 `facts` binary and the tracked Plan 9 gate script to nlab for native validation is allowed. Tracked repository files should still avoid private nlab hostnames, keys, guest addresses, generated credentials, and service internals. + +## Risks / Trade-offs + +- [Plan 9 output formats vary by installation] -> Keep parser tests based on documented formats plus lab samples, and mark host-state-dependent facts conditional in the schema. +- [Networking MTU semantics are ambiguous] -> Do not require `networking.mtu` in the first gate unless the implementation documents and tests a Plan 9 conversion rule. +- [Plan 9 namespaces do not match Unix mountpoint facts] -> Exclude mountpoints/filesystems/disks from this change. +- [Release gate may depend on lab transport] -> Track only the `rc` gate and configurable/local invocation points; keep nlab details outside git. +- [Schema can overclaim Plan 9 facts] -> Run schema conformance against the native Plan 9 discovery and fail missing non-conditional Plan 9 entries. +- [Plan 9 support increases CI time or flakiness] -> Start with compile coverage plus lab validation; require a passing native gate before release/dist promotion. +- [Future contributors may try to map `/dev/osversion` to OS release] -> Document this as explicitly unsupported in the spec and supported-facts page. + +## Migration Plan + +1. Add parser tests and Plan 9-specific parser helpers for the first stable fact set. +2. Wire Plan 9 branches into existing engine discovery without changing behavior on other platforms. +3. Add schema platform vocabulary and Plan 9 entries only for emitted, tested facts. +4. Generate `docs/supported-facts/plan9.md`. +5. Add `tools/plan9-release-gate.rc`. +6. Cross-build `cmd/facts` for `plan9/amd64`. +7. Copy the built binary and gate to the facts-lab Plan 9 guest and run the gate through `facts-lab ssh plan9`. +8. Add `plan9/amd64` to compile/build verification and documentation only after the native gate passes. + +Rollback is simple: remove `plan9` from schema/docs/build matrices and disable the Plan 9 gate. No persisted user data or migration state is involved. + +## Open Questions + +- Should the public display spelling be `Plan 9` or `Plan9`? This design recommends `Plan 9`. +- Should Plan 9 `networking.mtu` report raw `maxtu` or normalized Ethernet payload MTU? This change should skip the mandatory gate until decided. +- Should `memory.system.available` and `memory.swap.*` be emitted from `/dev/swap`, or should the first slice expose only total memory? +- Should virtualization report a generic value when virtio PCI devices are present, or remain absent until classification is broader than the lab guest? diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/proposal.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/proposal.md new file mode 100644 index 00000000..26012c38 --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/proposal.md @@ -0,0 +1,71 @@ +## Why + +The Plan 9 lab guest is now reachable through normal SSH, and the Go toolchain already cross-builds Facts for `plan9/amd64`. This lets Facts start adding real Plan 9 support with native validation instead of leaving Plan 9 as an unreachable or purely aspirational target. + +Plan 9 is not Unix-shaped: it has no `uname`, no `sysctl`, no conventional mount table, and different networking and memory surfaces. The first support slice needs an explicit contract so Facts only claims facts that are backed by native Plan 9 probes and a repeatable lab gate. + +## What Changes + +- Add Plan 9 as a supported, lab-validated platform for a narrow initial fact set. +- Implement Plan 9 discovery for stable native surfaces: + - OS/kernel identity from explicit Plan 9 constants. + - Hostname from `/dev/sysname`. + - Architecture and hardware from Go runtime values, `$cputype`/`$objtype`, `/dev/cputype`, and `/dev/archctl`. + - Memory total from `/dev/swap`. + - Processor count/model/speed from `/dev/sysstat`, `/dev/cputype`, and `/dev/archctl`. + - Basic IPv4 networking from `/net/ipifc/*/status`, `/net/*/addr`, `/net/iproute`, and `/net/ndb`. + - Uptime from Plan 9 `uptime`. + - Timezone from Go's local time support or Plan 9 `date`/`/env/timezone`. + - Existing path/environment facts where they already work under Go on Plan 9. +- Add deterministic parser tests for each Plan 9-specific format before wiring live probes into discovery. +- Add a tracked Plan 9 release-gate script written for `rc`, not `sh`, and validate it on the facts-lab Plan 9 guest. +- Add `plan9/amd64` to compile/build verification only after the native gate can prove the supported fact set. +- Update `docs/schema/facts.yaml`, generated `docs/supported-facts/plan9.md`, README/CONTRIBUTING support tables, and schema platform validation for only the facts Plan 9 can emit. +- Explicitly avoid first-pass support for facts that do not have a stable Plan 9 contract: + - `os.release.*` + - `kernel.release.*` + - `kernel.version.full` + - filesystems and mountpoint capacity + - disk/partition inventory + - `load_averages` + - DMI/cloud facts + - exact virtualization classification + - DHCP server facts +- No breaking changes are intended. + +## Capabilities + +### New Capabilities +- `plan9-platform-facts`: Plan 9 fact discovery, supported fact contract, and native release-gate validation for the initial stable fact set. + +### Modified Capabilities +- `facts-schema`: Add Plan 9 to the supported platform vocabulary and list `plan9` only on schema entries proven by parser tests and native Plan 9 validation. +- `go-port-ci-platform-gates`: Add Plan 9 lab validation and `plan9/amd64` compile coverage as an in-scope candidate gate with explicit limits. +- `go-port-distribution-and-cutover`: Define when Plan 9 enters build/dist artifacts and prevent publishing unsupported or unvalidated Plan 9 tuples. + +## Impact + +- Code: + - `internal/engine/os.go` + - `internal/engine/networking.go` + - `internal/engine/memory.go` + - `internal/engine/processors.go` + - `internal/engine/uptime.go` + - `internal/engine/timezone.go` if Go's local timezone behavior is insufficient on Plan 9 + - Small Plan 9-only helpers may be added when existing cross-platform code cannot safely read native files. +- Tests: + - Parser/unit tests beside the touched engine files. + - Compile-only `GOOS=plan9 GOARCH=amd64` verification. + - Native facts-lab Plan 9 release gate. + - Schema conformance for the Plan 9 supported fact set. +- Docs/schema: + - `docs/schema/facts.yaml` + - `docs/supported-facts/plan9.md` + - README platform table + - CONTRIBUTING platform gate notes +- Tooling: + - New `tools/plan9-release-gate.rc`. + - Optional Makefile target or documented lab command that runs the same tracked Plan 9 gate. +- Lab: + - Uses `facts-lab ssh plan9` through the nlab host. + - Does not store lab hostnames, keys, guest addresses, or private helper internals in tracked repository files. diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/facts-schema/spec.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/facts-schema/spec.md new file mode 100644 index 00000000..78826c41 --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/facts-schema/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: Plan 9 schema coverage +Facts SHALL include Plan 9 in `docs/schema/facts.yaml` platform metadata only for facts that Plan 9 discovery can emit through deterministic tests and native lab validation. + +#### Scenario: Schema lists Plan 9-supported facts +- **WHEN** a fact can be emitted by Plan 9 discovery and is validated by the Plan 9 native gate +- **THEN** the schema entry for that dotted path MUST include `plan9` in `platforms` + +#### Scenario: Schema avoids aspirational Plan 9 facts +- **WHEN** a fact has not been proven on Plan 9 through deterministic tests and a native validation path +- **THEN** the schema entry for that dotted path MUST NOT include `plan9` + +#### Scenario: Conditional Plan 9 facts stay conditional +- **WHEN** host state, interface configuration, probe availability, or lab environment controls whether a Plan 9 fact appears +- **THEN** the schema entry MUST be marked `conditional: true` + +### Requirement: Plan 9 platform vocabulary +Facts SHALL treat `plan9` as a supported platform vocabulary value after the first Plan 9 fact set is implemented. + +#### Scenario: Schema validation accepts Plan 9 +- **WHEN** schema validation reads `docs/schema/facts.yaml` +- **THEN** `plan9` MUST be accepted as a valid platform name alongside the existing supported platform names + +#### Scenario: Unsupported platform names remain rejected +- **WHEN** schema validation reads an unknown platform name +- **THEN** the validation MUST fail rather than silently accepting the unknown name + +### Requirement: Plan 9 supported-facts documentation +Facts SHALL publish generated supported-facts documentation for Plan 9. + +#### Scenario: Generated Plan 9 supported-facts page +- **WHEN** `docs/schema/facts.yaml` lists any fact with `plan9` in `platforms` +- **THEN** the generated supported-facts documentation MUST include `docs/supported-facts/plan9.md` + +#### Scenario: Generated docs stay in sync +- **WHEN** Plan 9 schema support changes +- **THEN** `go test ./...` MUST fail if `docs/supported-facts/plan9.md` is missing or drifted from the schema + +### Requirement: Plan 9 schema conformance +Facts SHALL run schema conformance against native Plan 9 discovery in the Plan 9 release gate. + +#### Scenario: Plan 9 undocumented emitted paths fail +- **WHEN** the Plan 9 schema conformance check flattens live Plan 9 discovery +- **THEN** every emitted path MUST match a schema entry whose `platforms` includes `plan9` + +#### Scenario: Plan 9 overclaimed paths fail +- **WHEN** the Plan 9 schema conformance check runs +- **THEN** every non-conditional schema entry listing `plan9` MUST be present in live Plan 9 discovery diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-ci-platform-gates/spec.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-ci-platform-gates/spec.md new file mode 100644 index 00000000..e291d82c --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-ci-platform-gates/spec.md @@ -0,0 +1,52 @@ +## ADDED Requirements + +### Requirement: Plan 9 validation is lab-backed +The Go port SHALL validate Plan 9 support with a native facts-lab gate before treating Plan 9 facts as supported. + +#### Scenario: Plan 9 native release gate +- **WHEN** the Plan 9 release gate runs +- **THEN** it MUST execute the real `cmd/facts` binary on the Plan 9 guest +- **AND** it MUST verify the tracked Plan 9 release-gate fact set through structured fact names only + +#### Scenario: Plan 9 release gate uses rc +- **WHEN** the Plan 9 release-gate script is added to the repository +- **THEN** it MUST be written for Plan 9 `rc` +- **AND** it MUST NOT require POSIX `sh` + +#### Scenario: Plan 9 gate excludes unsupported facts +- **WHEN** the first Plan 9 release gate runs +- **THEN** it MUST NOT require OS release facts, kernel release facts, filesystems, mountpoint capacity, disk inventory, partitions, DMI, cloud facts, FIPS, exact virtualization classification, DHCP server facts, or load averages + +### Requirement: Plan 9 compile coverage +The Go port SHALL add Plan 9 compile coverage only for validated Plan 9 targets. + +#### Scenario: Plan 9 amd64 compile +- **WHEN** the Plan 9 first-slice native gate is passing +- **THEN** the compile/build verification MUST include `plan9/amd64` + +#### Scenario: Unsupported Plan 9 tuples are not added +- **WHEN** the Go toolchain lists additional Plan 9 tuples such as `plan9/386` or `plan9/arm` +- **THEN** the CI build matrix MUST NOT include those tuples until they have an equivalent native validation path + +### Requirement: Plan 9 lab details stay out of tracked files +Tracked Facts files SHALL describe configurable Plan 9 validation entry points without committing private lab details. + +#### Scenario: Plan 9 local gate configuration +- **WHEN** tracked files document or invoke Plan 9 validation +- **THEN** they MUST use configurable commands, variables, or generic facts-lab documentation +- **AND** they MUST NOT commit private host addresses, SSH keys, generated passwords, or host-specific helper internals + +#### Scenario: Plan 9 lab command is documented +- **WHEN** a contributor wants to run the Plan 9 gate locally +- **THEN** the repository documentation MUST explain the expected high-level command flow for copying the Plan 9 binary and running `tools/plan9-release-gate.rc` through the lab + +### Requirement: Plan 9 gate and schema stay aligned +The Plan 9 native gate SHALL validate the same fact set that the schema documents as non-conditional for Plan 9. + +#### Scenario: Plan 9 schema conformance in gate +- **WHEN** the Plan 9 release gate runs +- **THEN** it MUST include schema conformance or an equivalent check that fails on undocumented emitted paths and missing non-conditional Plan 9 schema entries + +#### Scenario: Plan 9 gate fact set changes +- **WHEN** the Plan 9 release-gate fact set changes +- **THEN** the schema and generated Plan 9 supported-facts documentation MUST be updated in the same change diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-distribution-and-cutover/spec.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-distribution-and-cutover/spec.md new file mode 100644 index 00000000..64c11fcd --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/go-port-distribution-and-cutover/spec.md @@ -0,0 +1,40 @@ +## ADDED Requirements + +### Requirement: Plan 9 release artifact promotion is validation-gated +Facts SHALL publish Plan 9 release artifacts only for Plan 9 tuples that have native validation. + +#### Scenario: Plan 9 amd64 artifact eligibility +- **WHEN** `plan9/amd64` compile coverage and the Plan 9 native release gate are both passing +- **THEN** `plan9/amd64` MAY be added to the release artifact matrix + +#### Scenario: Plan 9 artifact names +- **WHEN** Plan 9 release artifacts are produced +- **THEN** they MUST follow the existing artifact naming scheme `facts--plan9-` +- **AND** the embedded version MUST be reported by `facts --version` + +#### Scenario: Unvalidated Plan 9 artifacts are not published +- **WHEN** the Go toolchain supports a Plan 9 architecture that lacks native validation +- **THEN** Facts MUST NOT publish an artifact for that tuple + +### Requirement: Plan 9 acceptance verification +Facts SHALL run binary-level acceptance verification on Plan 9 before Plan 9 is documented as a release target. + +#### Scenario: Plan 9 binary acceptance +- **WHEN** Plan 9 is promoted to a release target +- **THEN** acceptance verification MUST execute the real Plan 9 binary with representative CLI modes supported on Plan 9 +- **AND** it MUST assert the Plan 9 release-gate fact set and exit codes against the live Plan 9 guest + +#### Scenario: Plan 9 unsupported CLI behavior +- **WHEN** a representative CLI mode depends on an OS feature unavailable on Plan 9 +- **THEN** the acceptance verification MUST document the omission and continue to validate the supported CLI modes + +### Requirement: Plan 9 documentation matches promotion state +Facts SHALL distinguish lab-validated Plan 9 fact support from published release-target support until Plan 9 artifacts are actually shipped. + +#### Scenario: Plan 9 supported facts before artifact promotion +- **WHEN** Plan 9 facts are implemented and native-gated but no Plan 9 artifact is published +- **THEN** documentation MUST describe Plan 9 as lab-validated fact support rather than a published release artifact target + +#### Scenario: Plan 9 release target after artifact promotion +- **WHEN** Plan 9 artifacts are added to the release matrix +- **THEN** README and release documentation MUST list Plan 9 with only the validated architectures diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/plan9-platform-facts/spec.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/plan9-platform-facts/spec.md new file mode 100644 index 00000000..850b5ab8 --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/specs/plan9-platform-facts/spec.md @@ -0,0 +1,133 @@ +## ADDED Requirements + +### Requirement: Plan 9 identity facts +Facts SHALL emit canonical Plan 9 identity facts when running on `GOOS=plan9`. + +#### Scenario: Plan 9 OS and kernel identity +- **WHEN** the `facts` CLI runs on Plan 9 +- **THEN** `os.name` MUST be `Plan 9` +- **AND** `os.family` MUST be `Plan 9` +- **AND** `kernel.name` MUST be `Plan 9` + +#### Scenario: Plan 9 hostname +- **WHEN** `/dev/sysname` contains a non-empty system name +- **THEN** Facts MUST use the trimmed `/dev/sysname` value for `networking.hostname` + +#### Scenario: Missing Plan 9 hostname source +- **WHEN** `/dev/sysname` is missing or empty +- **THEN** Facts MUST omit `networking.hostname` rather than inventing a hostname + +### Requirement: Plan 9 release and version facts are not invented +Facts SHALL NOT derive OS or kernel release facts from Plan 9 sources whose semantics do not match those facts. + +#### Scenario: Plan 9 protocol version is not OS release +- **WHEN** `/dev/osversion` is present on Plan 9 +- **THEN** Facts MUST NOT use it as `os.release`, `os.release.full`, `os.release.major`, `kernel.release.full`, `kernel.release.major`, or `kernel.version.full` + +#### Scenario: Unsupported Plan 9 release facts +- **WHEN** the first Plan 9 support slice runs +- **THEN** Facts MUST omit `os.release.*` and `kernel.release.*` facts unless a later spec identifies a correct Plan 9 release source + +### Requirement: Plan 9 architecture and hardware facts +Facts SHALL report Plan 9 architecture and hardware facts from Go runtime values and native Plan 9 CPU sources. + +#### Scenario: Plan 9 architecture +- **WHEN** Facts runs on Plan 9 +- **THEN** `os.architecture` MUST be derived from the Plan 9 architecture value, preferring `$objtype` when available and falling back to `runtime.GOARCH` +- **AND** `os.hardware` MUST be derived from the same architecture source + +#### Scenario: Plan 9 processor ISA +- **WHEN** Facts can read `$cputype`, `$objtype`, or `runtime.GOARCH` +- **THEN** `processors.isa` MUST contain the best available Plan 9 processor architecture value + +#### Scenario: Plan 9 processor model +- **WHEN** `/dev/cputype` or `/dev/archctl` contains a CPU model string +- **THEN** Facts MUST include that model in `processors.models` + +### Requirement: Plan 9 memory facts +Facts SHALL report Plan 9 memory totals from `/dev/swap`. + +#### Scenario: Plan 9 total memory +- **WHEN** `/dev/swap` contains a line matching ` memory` +- **THEN** `memory.system.total_bytes` MUST equal that byte count +- **AND** `memory.system.total` MUST be the human-readable value derived from that byte count using the existing Facts formatting rules + +#### Scenario: Plan 9 memory parser ignores unsupported lines +- **WHEN** `/dev/swap` includes page size, user page, or swap page lines +- **THEN** the first Plan 9 support slice MUST NOT require those lines to produce `memory.system.available`, `memory.system.used`, or `memory.swap.*` + +#### Scenario: Plan 9 memory source missing +- **WHEN** `/dev/swap` cannot be read or does not contain a memory total +- **THEN** Facts MUST omit Plan 9 memory facts rather than returning zero + +### Requirement: Plan 9 processor count facts +Facts SHALL report Plan 9 processor count from `/dev/sysstat`. + +#### Scenario: Plan 9 processor count +- **WHEN** `/dev/sysstat` contains one or more processor status lines +- **THEN** `processors.count` MUST equal the number of non-empty processor status lines + +#### Scenario: Plan 9 processor count source missing +- **WHEN** `/dev/sysstat` cannot be read or contains no processor status lines +- **THEN** Facts MUST omit `processors.count` rather than returning zero + +### Requirement: Plan 9 basic IPv4 networking facts +Facts SHALL report basic Plan 9 IPv4 networking facts from the native `/net` filesystem. + +#### Scenario: Plan 9 interface address +- **WHEN** `/net/ipifc/*/status` contains an IPv4 address row +- **THEN** Facts MUST parse the interface IP address, network prefix, netmask, and network address from that row + +#### Scenario: Plan 9 IPv4 mapped prefix +- **WHEN** a Plan 9 IPv4 address row uses an IPv6-mapped prefix such as `/120` +- **THEN** Facts MUST convert it to the equivalent IPv4 prefix length such as `/24` before deriving `networking.netmask` and `networking.network` + +#### Scenario: Plan 9 interface MAC address +- **WHEN** the Plan 9 interface device has an `addr` file such as `/net/ether0/addr` +- **THEN** Facts MUST parse that value as the interface MAC address + +#### Scenario: Plan 9 primary network +- **WHEN** `/net/iproute` contains a default IPv4 route +- **THEN** Facts MUST use that route to select `networking.primary` and the top-level `networking.ip`, `networking.netmask`, `networking.network`, and `networking.mac` values + +#### Scenario: Plan 9 DHCP metadata +- **WHEN** `/net/ndb` lacks DHCP server metadata +- **THEN** Facts MUST omit DHCP server facts rather than inventing a value + +### Requirement: Plan 9 uptime facts +Facts SHALL report Plan 9 uptime from the native Plan 9 `uptime` command. + +#### Scenario: Plan 9 uptime format +- **WHEN** `uptime` returns a value like `cirno up 0 days, 01:35:26` +- **THEN** Facts MUST parse the day, hour, minute, and second values into `system_uptime.seconds` +- **AND** Facts MUST produce the existing `system_uptime` structure from that duration + +#### Scenario: Plan 9 uptime source missing +- **WHEN** `uptime` cannot be executed or does not match the Plan 9 format +- **THEN** Facts MUST omit uptime facts rather than returning zero + +### Requirement: Plan 9 timezone facts +Facts SHALL report the Plan 9 timezone using Go local time behavior or native Plan 9 timezone sources. + +#### Scenario: Plan 9 timezone from Go runtime +- **WHEN** Go's local time support returns a timezone abbreviation on Plan 9 +- **THEN** Facts MUST use the existing timezone fact path and formatting + +#### Scenario: Plan 9 timezone fallback +- **WHEN** Go's local time support cannot provide the timezone abbreviation +- **THEN** Facts MAY parse `date`, `date -t`, `/env/timezone`, or `/adm/timezone/local` to produce the existing timezone fact + +### Requirement: Plan 9 unsupported first-slice facts +Facts SHALL omit Plan 9 facts whose native semantics are unresolved in the first support slice. + +#### Scenario: Plan 9 mount and filesystem facts +- **WHEN** Facts runs on Plan 9 in the first support slice +- **THEN** Facts MUST NOT claim support for mountpoint capacity, filesystem inventory, disk inventory, or partition inventory + +#### Scenario: Plan 9 load facts +- **WHEN** `/dev/sysstat` exposes Plan 9 load data +- **THEN** Facts MUST NOT map it to `load_averages` unless a later spec defines the conversion + +#### Scenario: Plan 9 virtualization facts +- **WHEN** virtio or VM-specific devices are visible on the lab guest +- **THEN** Facts MUST NOT require exact virtualization classification in the first Plan 9 release gate diff --git a/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/tasks.md b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/tasks.md new file mode 100644 index 00000000..5c2b175f --- /dev/null +++ b/openspec/changes/archive/2026-06-21-add-plan9-platform-facts/tasks.md @@ -0,0 +1,70 @@ +## 1. Baseline And Probe Fixtures + +- [x] 1.1 Confirm the repo still cross-compiles with `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go build ./cmd/facts` +- [x] 1.2 Confirm compile-only tests still pass with `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go test -exec true ./...` +- [x] 1.3 Capture minimal Plan 9 lab samples for `/dev/sysname`, `/dev/swap`, `/dev/sysstat`, `/dev/cputype`, `/dev/archctl`, `/net/ipifc/*/status`, `/net/*/addr`, `/net/iproute`, `/net/ndb`, `uptime`, and timezone output +- [x] 1.4 Add parser test fixtures using the captured Plan 9 samples without requiring the tests to run on Plan 9 + +## 2. OS, Kernel, Hostname, And Architecture + +- [x] 2.1 Add Plan 9 cases for canonical `os.name`, `os.family`, and `kernel.name` values +- [x] 2.2 Add Plan 9 hostname discovery from trimmed `/dev/sysname` +- [x] 2.3 Add Plan 9 architecture and hardware discovery using `$objtype` with `runtime.GOARCH` fallback +- [x] 2.4 Add Plan 9 processor ISA discovery using `$cputype`, `$objtype`, or `runtime.GOARCH` +- [x] 2.5 Add tests proving Plan 9 does not emit OS/kernel release facts from `/dev/osversion` + +## 3. Memory And Processors + +- [x] 3.1 Add a parser for Plan 9 `/dev/swap` memory total lines +- [x] 3.2 Wire `memory.system.total_bytes` and `memory.system.total` on Plan 9 +- [x] 3.3 Add a parser for Plan 9 `/dev/sysstat` processor line counts +- [x] 3.4 Wire `processors.count` on Plan 9 +- [x] 3.5 Add parsers for `/dev/cputype` and `/dev/archctl` processor model data +- [x] 3.6 Wire `processors.models` on Plan 9 when model data is present + +## 4. Networking + +- [x] 4.1 Add a parser for Plan 9 `/net/ipifc/*/status` device and IPv4 address rows +- [x] 4.2 Convert Plan 9 IPv4-mapped prefixes such as `/120` into IPv4 prefixes such as `/24` +- [x] 4.3 Derive Plan 9 IPv4 netmask and network values from parsed address rows +- [x] 4.4 Add a parser for Plan 9 interface MAC files such as `/net/ether0/addr` +- [x] 4.5 Add a parser for Plan 9 default routes from `/net/iproute` +- [x] 4.6 Wire Plan 9 `networking.primary` and top-level `networking.ip`, `networking.netmask`, `networking.network`, and `networking.mac` +- [x] 4.7 Keep DHCP server and MTU facts out of the required Plan 9 gate unless their semantics are separately tested + +## 5. Uptime, Timezone, And Existing Generic Facts + +- [x] 5.1 Add a parser for Plan 9 `uptime` output such as `cirno up 0 days, 01:35:26` +- [x] 5.2 Wire Plan 9 `system_uptime` and `system_uptime.seconds` +- [x] 5.3 Verify existing timezone discovery works on Plan 9; add a Plan 9 fallback from native timezone output only if needed +- [x] 5.4 Verify existing `path` fact behavior on Plan 9 and include it in the gate only if native output is stable + +## 6. Schema And Documentation + +- [x] 6.1 Add `plan9` to the schema platform vocabulary in schema validation tests +- [x] 6.2 Add `plan9` to `docs/schema/facts.yaml` only for facts emitted by the implemented Plan 9 discovery +- [x] 6.3 Mark Plan 9 schema entries conditional when host state or probe availability controls emission +- [x] 6.4 Generate `docs/supported-facts/plan9.md` from the schema +- [x] 6.5 Update README platform support text to describe Plan 9 as lab-validated fact support unless release artifacts are added +- [x] 6.6 Update CONTRIBUTING with the Plan 9 gate workflow and the rule that Plan 9 release-gate facts must stay schema-backed + +## 7. Plan 9 Release Gate And Build Matrix + +- [x] 7.1 Add `tools/plan9-release-gate.rc` using Plan 9 `rc` syntax +- [x] 7.2 Make the Plan 9 gate execute the real `facts` binary through structured fact queries +- [x] 7.3 Make the Plan 9 gate assert the first supported fact set and explicitly avoid unsupported release/filesystem/disk/load/DMI/cloud facts +- [x] 7.4 Add or document a local Plan 9 lab command path that copies the binary and gate to the guest without committing private lab details +- [x] 7.5 Add `plan9/amd64` compile/build verification after the native Plan 9 gate passes +- [x] 7.6 Leave `plan9/386` and `plan9/arm` out of build and release matrices until native validation exists + +## 8. Validation + +- [x] 8.1 Run `gofmt -w` on edited Go files +- [x] 8.2 Run `go test ./...` +- [x] 8.3 Run `go vet ./...` +- [x] 8.4 Run `make build` +- [x] 8.5 Run `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go build ./cmd/facts` +- [x] 8.6 Run `CGO_ENABLED=0 GOOS=plan9 GOARCH=amd64 go test -exec true ./...` +- [x] 8.7 Run the Plan 9 native gate through `facts-lab ssh plan9` +- [x] 8.8 Run schema supported-facts generation/checks and verify `docs/supported-facts/plan9.md` is current +- [x] 8.9 Run Open Code Review after implementation and address actionable findings diff --git a/openspec/changes/fix-vm-virtualization-detection/proposal.md b/openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/proposal.md similarity index 100% rename from openspec/changes/fix-vm-virtualization-detection/proposal.md rename to openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/proposal.md diff --git a/openspec/changes/fix-vm-virtualization-detection/specs/go-port-supported-platform-facts/spec.md b/openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/specs/go-port-supported-platform-facts/spec.md similarity index 100% rename from openspec/changes/fix-vm-virtualization-detection/specs/go-port-supported-platform-facts/spec.md rename to openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/specs/go-port-supported-platform-facts/spec.md diff --git a/openspec/changes/fix-vm-virtualization-detection/tasks.md b/openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/tasks.md similarity index 100% rename from openspec/changes/fix-vm-virtualization-detection/tasks.md rename to openspec/changes/archive/2026-06-21-fix-vm-virtualization-detection/tasks.md diff --git a/openspec/changes/harden-security-boundaries/proposal.md b/openspec/changes/archive/2026-06-21-harden-security-boundaries/proposal.md similarity index 100% rename from openspec/changes/harden-security-boundaries/proposal.md rename to openspec/changes/archive/2026-06-21-harden-security-boundaries/proposal.md diff --git a/openspec/changes/harden-security-boundaries/specs/go-port-ci-platform-gates/spec.md b/openspec/changes/archive/2026-06-21-harden-security-boundaries/specs/go-port-ci-platform-gates/spec.md similarity index 100% rename from openspec/changes/harden-security-boundaries/specs/go-port-ci-platform-gates/spec.md rename to openspec/changes/archive/2026-06-21-harden-security-boundaries/specs/go-port-ci-platform-gates/spec.md diff --git a/openspec/changes/harden-security-boundaries/specs/go-port-framework-parity/spec.md b/openspec/changes/archive/2026-06-21-harden-security-boundaries/specs/go-port-framework-parity/spec.md similarity index 100% rename from openspec/changes/harden-security-boundaries/specs/go-port-framework-parity/spec.md rename to openspec/changes/archive/2026-06-21-harden-security-boundaries/specs/go-port-framework-parity/spec.md diff --git a/openspec/changes/harden-security-boundaries/tasks.md b/openspec/changes/archive/2026-06-21-harden-security-boundaries/tasks.md similarity index 100% rename from openspec/changes/harden-security-boundaries/tasks.md rename to openspec/changes/archive/2026-06-21-harden-security-boundaries/tasks.md diff --git a/openspec/changes/structure-flat-string-facts/design.md b/openspec/changes/archive/2026-06-21-structure-flat-string-facts/design.md similarity index 100% rename from openspec/changes/structure-flat-string-facts/design.md rename to openspec/changes/archive/2026-06-21-structure-flat-string-facts/design.md diff --git a/openspec/changes/structure-flat-string-facts/proposal.md b/openspec/changes/archive/2026-06-21-structure-flat-string-facts/proposal.md similarity index 100% rename from openspec/changes/structure-flat-string-facts/proposal.md rename to openspec/changes/archive/2026-06-21-structure-flat-string-facts/proposal.md diff --git a/openspec/changes/structure-flat-string-facts/specs/facts-schema/spec.md b/openspec/changes/archive/2026-06-21-structure-flat-string-facts/specs/facts-schema/spec.md similarity index 100% rename from openspec/changes/structure-flat-string-facts/specs/facts-schema/spec.md rename to openspec/changes/archive/2026-06-21-structure-flat-string-facts/specs/facts-schema/spec.md diff --git a/openspec/changes/structure-flat-string-facts/specs/go-port-supported-platform-facts/spec.md b/openspec/changes/archive/2026-06-21-structure-flat-string-facts/specs/go-port-supported-platform-facts/spec.md similarity index 100% rename from openspec/changes/structure-flat-string-facts/specs/go-port-supported-platform-facts/spec.md rename to openspec/changes/archive/2026-06-21-structure-flat-string-facts/specs/go-port-supported-platform-facts/spec.md diff --git a/openspec/changes/structure-flat-string-facts/tasks.md b/openspec/changes/archive/2026-06-21-structure-flat-string-facts/tasks.md similarity index 100% rename from openspec/changes/structure-flat-string-facts/tasks.md rename to openspec/changes/archive/2026-06-21-structure-flat-string-facts/tasks.md diff --git a/openspec/specs/facts-schema/spec.md b/openspec/specs/facts-schema/spec.md index 4a009bec..bc7abc17 100644 --- a/openspec/specs/facts-schema/spec.md +++ b/openspec/specs/facts-schema/spec.md @@ -113,3 +113,96 @@ Facts SHALL include DragonFly and illumos in `docs/schema/facts.yaml` platform m - **WHEN** DragonFly or illumos schema conformance runs after promotion - **THEN** undocumented emitted paths MUST fail the gate - **AND** missing non-conditional schema entries MUST fail the gate + +### Requirement: Plan 9 schema coverage +Facts SHALL include Plan 9 in `docs/schema/facts.yaml` platform metadata only for facts that Plan 9 discovery can emit through deterministic tests and native lab validation. + +#### Scenario: Schema lists Plan 9-supported facts +- **WHEN** a fact can be emitted by Plan 9 discovery and is validated by the Plan 9 native gate +- **THEN** the schema entry for that dotted path MUST include `plan9` in `platforms` + +#### Scenario: Schema avoids aspirational Plan 9 facts +- **WHEN** a fact has not been proven on Plan 9 through deterministic tests and a native validation path +- **THEN** the schema entry for that dotted path MUST NOT include `plan9` + +#### Scenario: Conditional Plan 9 facts stay conditional +- **WHEN** host state, interface configuration, probe availability, or lab environment controls whether a Plan 9 fact appears +- **THEN** the schema entry MUST be marked `conditional: true` + +### Requirement: Plan 9 platform vocabulary +Facts SHALL treat `plan9` as a supported platform vocabulary value after the first Plan 9 fact set is implemented. + +#### Scenario: Schema validation accepts Plan 9 +- **WHEN** schema validation reads `docs/schema/facts.yaml` +- **THEN** `plan9` MUST be accepted as a valid platform name alongside the existing supported platform names + +#### Scenario: Unsupported platform names remain rejected +- **WHEN** schema validation reads an unknown platform name +- **THEN** the validation MUST fail rather than silently accepting the unknown name + +### Requirement: Plan 9 supported-facts documentation +Facts SHALL publish generated supported-facts documentation for Plan 9. + +#### Scenario: Generated Plan 9 supported-facts page +- **WHEN** `docs/schema/facts.yaml` lists any fact with `plan9` in `platforms` +- **THEN** the generated supported-facts documentation MUST include `docs/supported-facts/plan9.md` + +#### Scenario: Generated docs stay in sync +- **WHEN** Plan 9 schema support changes +- **THEN** `go test ./...` MUST fail if `docs/supported-facts/plan9.md` is missing or drifted from the schema + +### Requirement: Plan 9 schema conformance +Facts SHALL run schema conformance against native Plan 9 discovery in the Plan 9 release gate. + +#### Scenario: Plan 9 undocumented emitted paths fail +- **WHEN** the Plan 9 schema conformance check flattens live Plan 9 discovery +- **THEN** every emitted path MUST match a schema entry whose `platforms` includes `plan9` + +#### Scenario: Plan 9 overclaimed paths fail +- **WHEN** the Plan 9 schema conformance check runs +- **THEN** every non-conditional schema entry listing `plan9` MUST be present in live Plan 9 discovery + +### Requirement: Flat grouped facts are represented as structured schema paths + +Facts SHALL document grouped fact concepts as nested schema paths, not as multiple top-level flat names with embedded group meaning. + +#### Scenario: Kernel schema is structured + +- **WHEN** a contributor reads `docs/schema/facts.yaml` +- **THEN** kernel data MUST be documented under `kernel.*` +- **AND** the schema MUST include `kernel.name`, `kernel.release.full`, `kernel.release.major`, `kernel.release.minor`, and `kernel.version.full` +- **AND** `kernel.release.patch` MUST be documented as conditional +- **AND** `kernel`, `kernelmajversion`, `kernelrelease`, and `kernelversion` MUST NOT be documented as supported facts + +### Requirement: String collections are represented as arrays + +Facts SHALL document collection-valued facts as arrays, not as delimiter-separated strings. + +#### Scenario: Filesystems and PATH schema are arrays + +- **WHEN** a contributor reads `docs/schema/facts.yaml` +- **THEN** `filesystems` MUST have type `array` +- **AND** `path` MUST have type `array` + +#### Scenario: ZFS schema is structured + +- **WHEN** ZFS facts are documented +- **THEN** the schema MUST include `zfs.feature_numbers` as an array +- **AND** the schema MUST include `zfs.version` as a string +- **AND** `zfs_featurenumbers` and `zfs_version` MUST NOT be documented as supported facts + +#### Scenario: Zpool schema is structured + +- **WHEN** Zpool facts are documented +- **THEN** the schema MUST include `zpool.feature_numbers` as an array +- **AND** the schema MUST include `zpool.feature_flags` as an array +- **AND** the schema MUST include `zpool.version` as a string +- **AND** `zpool_featurenumbers`, `zpool_featureflags`, and `zpool_version` MUST NOT be documented as supported facts + +#### Scenario: Supported fact pages reflect structured schema + +- **WHEN** `docs/supported-facts/` pages are generated +- **THEN** the pages MUST show the structured kernel, ZFS, and Zpool paths +- **AND** the pages MUST show `filesystems` and `path` as arrays +- **AND** the pages MUST NOT list the removed flat facts + diff --git a/openspec/specs/go-port-ci-platform-gates/spec.md b/openspec/specs/go-port-ci-platform-gates/spec.md index 5854e3b8..8096ff2a 100644 --- a/openspec/specs/go-port-ci-platform-gates/spec.md +++ b/openspec/specs/go-port-ci-platform-gates/spec.md @@ -74,3 +74,62 @@ The Go port SHALL validate DragonFly and illumos release targets through automat - **WHEN** local smoke targets invoke DragonFly, illumos, or amd64 BSD lab guests - **THEN** tracked files MUST reference configurable wrapper variables only - **AND** lab hostnames, addresses, keys, and private helper commands MUST remain outside tracked files + +### Requirement: Plan 9 validation is lab-backed +The Go port SHALL validate Plan 9 support with a native facts-lab gate before treating Plan 9 facts as supported. + +#### Scenario: Plan 9 native release gate +- **WHEN** the Plan 9 release gate runs +- **THEN** it MUST execute the real `cmd/facts` binary on the Plan 9 guest +- **AND** it MUST verify the tracked Plan 9 release-gate fact set through structured fact names only + +#### Scenario: Plan 9 release gate uses rc +- **WHEN** the Plan 9 release-gate script is added to the repository +- **THEN** it MUST be written for Plan 9 `rc` +- **AND** it MUST NOT require POSIX `sh` + +#### Scenario: Plan 9 gate excludes unsupported facts +- **WHEN** the first Plan 9 release gate runs +- **THEN** it MUST NOT require OS release facts, kernel release facts, filesystems, mountpoint capacity, disk inventory, partitions, DMI, cloud facts, FIPS, exact virtualization classification, DHCP server facts, or load averages + +### Requirement: Plan 9 compile coverage +The Go port SHALL add Plan 9 compile coverage only for validated Plan 9 targets. + +#### Scenario: Plan 9 amd64 compile +- **WHEN** the Plan 9 first-slice native gate is passing +- **THEN** the compile/build verification MUST include `plan9/amd64` + +#### Scenario: Unsupported Plan 9 tuples are not added +- **WHEN** the Go toolchain lists additional Plan 9 tuples such as `plan9/386` or `plan9/arm` +- **THEN** the CI build matrix MUST NOT include those tuples until they have an equivalent native validation path + +### Requirement: Plan 9 lab details stay out of tracked files +Tracked Facts files SHALL describe configurable Plan 9 validation entry points without committing private lab details. + +#### Scenario: Plan 9 local gate configuration +- **WHEN** tracked files document or invoke Plan 9 validation +- **THEN** they MUST use configurable commands, variables, or generic facts-lab documentation +- **AND** they MUST NOT commit private host addresses, SSH keys, generated passwords, or host-specific helper internals + +#### Scenario: Plan 9 lab command is documented +- **WHEN** a contributor wants to run the Plan 9 gate locally +- **THEN** the repository documentation MUST explain the expected high-level command flow for copying the Plan 9 binary and running `tools/plan9-release-gate.rc` through the lab + +### Requirement: Plan 9 gate and schema stay aligned +The Plan 9 native gate SHALL validate the same fact set that the schema documents as non-conditional for Plan 9. + +#### Scenario: Plan 9 schema conformance in gate +- **WHEN** the Plan 9 release gate runs +- **THEN** it MUST include schema conformance or an equivalent check that fails on undocumented emitted paths and missing non-conditional Plan 9 schema entries + +#### Scenario: Plan 9 gate fact set changes +- **WHEN** the Plan 9 release-gate fact set changes +- **THEN** the schema and generated Plan 9 supported-facts documentation MUST be updated in the same change + +### Requirement: Vulnerability scanning is automated in CI +The Go port SHALL run Go vulnerability analysis as a blocking CI check. + +#### Scenario: Vulnerability scan failure fails the workflow +- **WHEN** the Go checks workflow runs +- **THEN** it MUST run the repository-pinned `govulncheck` tool against `./...`, and any reported vulnerability or scanner failure MUST fail the workflow + diff --git a/openspec/specs/go-port-distribution-and-cutover/spec.md b/openspec/specs/go-port-distribution-and-cutover/spec.md index d0711bae..582a0843 100644 --- a/openspec/specs/go-port-distribution-and-cutover/spec.md +++ b/openspec/specs/go-port-distribution-and-cutover/spec.md @@ -40,3 +40,43 @@ The Go port SHALL ship user documentation that reflects the Go binary's actual b #### Scenario: Man page parity - **WHEN** the man page is regenerated or audited against the Go CLI - **THEN** every documented flag, default, and exit code MUST match the Go implementation, and Go-port deviations (the no-Ruby-DSL input contract) MUST be noted + +### Requirement: Plan 9 release artifact promotion is validation-gated +Facts SHALL publish Plan 9 release artifacts only for Plan 9 tuples that have native validation. + +#### Scenario: Plan 9 amd64 artifact eligibility +- **WHEN** `plan9/amd64` compile coverage and the Plan 9 native release gate are both passing +- **THEN** `plan9/amd64` MAY be added to the release artifact matrix + +#### Scenario: Plan 9 artifact names +- **WHEN** Plan 9 release artifacts are produced +- **THEN** they MUST follow the existing artifact naming scheme `facts--plan9-` +- **AND** the embedded version MUST be reported by `facts --version` + +#### Scenario: Unvalidated Plan 9 artifacts are not published +- **WHEN** the Go toolchain supports a Plan 9 architecture that lacks native validation +- **THEN** Facts MUST NOT publish an artifact for that tuple + +### Requirement: Plan 9 acceptance verification +Facts SHALL run binary-level acceptance verification on Plan 9 before Plan 9 is documented as a release target. + +#### Scenario: Plan 9 binary acceptance +- **WHEN** Plan 9 is promoted to a release target +- **THEN** acceptance verification MUST execute the real Plan 9 binary with representative CLI modes supported on Plan 9 +- **AND** it MUST assert the Plan 9 release-gate fact set and exit codes against the live Plan 9 guest + +#### Scenario: Plan 9 unsupported CLI behavior +- **WHEN** a representative CLI mode depends on an OS feature unavailable on Plan 9 +- **THEN** the acceptance verification MUST document the omission and continue to validate the supported CLI modes + +### Requirement: Plan 9 documentation matches promotion state +Facts SHALL distinguish lab-validated Plan 9 fact support from published release-target support until Plan 9 artifacts are actually shipped. + +#### Scenario: Plan 9 supported facts before artifact promotion +- **WHEN** Plan 9 facts are implemented and native-gated but no Plan 9 artifact is published +- **THEN** documentation MUST describe Plan 9 as lab-validated fact support rather than a published release artifact target + +#### Scenario: Plan 9 release target after artifact promotion +- **WHEN** Plan 9 artifacts are added to the release matrix +- **THEN** README and release documentation MUST list Plan 9 with only the validated architectures + diff --git a/openspec/specs/go-port-framework-parity/spec.md b/openspec/specs/go-port-framework-parity/spec.md index c44cc4d3..51ae5a3d 100644 --- a/openspec/specs/go-port-framework-parity/spec.md +++ b/openspec/specs/go-port-framework-parity/spec.md @@ -92,3 +92,26 @@ The `facts` CLI SHALL colorize keys in the default text format according to thei - **WHEN** `facts` runs with `--json`, `--yaml`, or `--hocon`, with or without `--color` - **THEN** the formatted output MUST be byte-identical regardless of color settings +### Requirement: Framework security boundaries +The Go port SHALL bound untrusted framework inputs and SHALL render machine formats with syntactically safe keys. + +#### Scenario: Cache groups stay within the cache directory +- **WHEN** fact cache TTL configuration or custom fact groups contain absolute paths, path traversal, separators, drive prefixes, empty names, or dot-only names +- **THEN** cache reads, writes, freshness checks, and invalidation MUST ignore those groups and MUST NOT read, write, or delete files outside the configured cache directory + +#### Scenario: External fact sources are bounded +- **WHEN** static external fact files, executable external fact stdout, or executable external fact stderr exceed the external fact byte limit +- **THEN** discovery MUST stop reading that source and report an oversized external fact failure through the same CLI/library error policy used for that source kind + +#### Scenario: YAML and HOCON keys are escaped +- **WHEN** selected or unselected fact output contains a map key that is unsafe in YAML or HOCON plain-key syntax +- **THEN** the YAML and HOCON formatters MUST quote the key so the output remains parseable data rather than injected syntax + +#### Scenario: Built-in probes ignore untrusted PATH entries +- **WHEN** core fact resolution runs built-in host probe commands +- **THEN** those commands MUST be resolved from a fixed platform system search path and MUST receive a sanitized `PATH` that excludes caller-provided path entries while preserving unrelated environment variables + +#### Scenario: Filesystem byte math clamps overflow +- **WHEN** platform statfs data reports block counts or block sizes whose product exceeds the Go `int` range +- **THEN** mountpoint byte totals MUST clamp to the largest representable `int` before formatting facts + diff --git a/openspec/specs/go-port-supported-platform-facts/spec.md b/openspec/specs/go-port-supported-platform-facts/spec.md index 004c93f7..a26a19ae 100644 --- a/openspec/specs/go-port-supported-platform-facts/spec.md +++ b/openspec/specs/go-port-supported-platform-facts/spec.md @@ -77,11 +77,12 @@ The Go port SHALL preserve supported-platform Ruby resolver fallback order and d - **THEN** it MUST do so through the Session's host seam (e.g. `Session.commandOutput`/`readFile`), and a test MUST be able to substitute a fake host so the resolver runs to completion without touching the real operating system ### Requirement: Virtualization and cloud parity -The Go port SHALL match Ruby-compatible virtualization, hypervisor, and cloud metadata behavior on supported platforms. +The Go port SHALL match Ruby-compatible virtualization, hypervisor, and cloud metadata behavior on supported platforms, and MAY expose accurate Facts-native virtualization detection when a supported platform has stable native indicators not covered by Ruby Facter. #### Scenario: Virtualization and hypervisor facts -- **WHEN** supported-platform virtualization facts are resolved from Linux `virt-what`, cgroups, DMI, VMware, Xen, OpenVZ, Windows OEM/netkvm/WMI indicators, macOS indicators, FreeBSD virtualization indicators, OpenBSD DMI product indicators, or NetBSD indicators identified by the parity audit +- **WHEN** supported-platform virtualization facts are resolved from Linux `virt-what`, cgroups, DMI, VMware, Xen, OpenVZ, Windows OEM/netkvm/WMI indicators, macOS indicators, FreeBSD virtualization indicators, OpenBSD DMI product/vendor indicators, NetBSD DMI indicators, DragonFly DMI/PCI indicators, illumos SMBIOS/PCI indicators, or other indicators identified by the parity audit - **THEN** the Go port MUST match Ruby `virtual`, `is_virtual`, `hypervisors.*`, Xen, container, and nil/unknown behavior for supported detection paths +- **AND** it MUST report QEMU/KVM guests as virtual when those supported native indicators expose QEMU, SeaBIOS, or Virtio metadata #### Scenario: Cloud metadata facts - **WHEN** EC2, GCE, or Azure metadata is available, invalid, blocked by virtualization/provider checks, empty, or unavailable @@ -125,6 +126,16 @@ A fact that cannot resolve a value or does not apply to the host platform SHALL - **WHEN** a fact's source cannot produce a value (no augparse binary for `augeas.version`, no enumerable devices for `disks`/`partitions`, unknown `processors.speed`) - **THEN** the fact (or key) MUST be absent from every output mode, not rendered as an empty string or empty map +#### Scenario: Optional primary networking values are absent when unresolved +- **WHEN** supported-platform networking facts are rendered for full structured output +- **THEN** optional top-level primary fields such as `networking.ip`, `networking.ip6`, `networking.mac`, `networking.netmask`, `networking.netmask6`, `networking.network`, `networking.network6`, `networking.primary`, and `networking.scope6` MUST be absent when unresolved +- **AND** populated primary networking values MUST still be emitted + +#### Scenario: Empty mountpoint entries are absent +- **WHEN** supported-platform mountpoint facts are rendered for full structured output +- **THEN** entries with no mountpoint fields MUST be absent instead of rendering an empty map +- **AND** populated mountpoint entries MUST still be emitted + #### Scenario: Platform-inapplicable facts are absent - **WHEN** discovery runs on a platform where Ruby Facter does not resolve a fact (`fips_enabled` outside Linux and Windows, `os.selinux` outside Linux) - **THEN** that fact MUST be absent from the canonical tree on that platform @@ -277,3 +288,50 @@ Facts SHALL extend FreeBSD, OpenBSD, and NetBSD structured fact coverage only wh #### Scenario: Unstable NetBSD DHCP source remains absent - **WHEN** NetBSD DHCP data is available only from an unstable or binary lease source - **THEN** `networking.interfaces..dhcp` and `networking.dhcp` MUST remain absent unless a stable text source is selected and tested + +### Requirement: Kernel facts are structured + +Kernel facts SHALL be emitted as a structured `kernel` map rather than as flat top-level facts. + +#### Scenario: Kernel output shape + +- **WHEN** core facts are resolved on any supported release target +- **THEN** `kernel.name` MUST contain the kernel name +- **AND** `kernel.release.full` MUST contain the full kernel release +- **AND** `kernel.release.major` and `kernel.release.minor` MUST contain the parsed release components when available +- **AND** `kernel.release.patch` MUST be present only when a patch component is available +- **AND** `kernel.version.full` MUST contain the kernel version +- **AND** `kernelmajversion`, `kernelrelease`, and `kernelversion` MUST be absent + +### Requirement: Collection facts are arrays + +Collection facts SHALL be emitted as arrays rather than delimiter-separated strings. + +#### Scenario: Filesystems output shape + +- **WHEN** filesystem types are resolved on Linux or macOS/Darwin +- **THEN** `filesystems` MUST be an array of filesystem type strings +- **AND** it MUST NOT be a comma-separated string + +#### Scenario: PATH output shape + +- **WHEN** core facts are resolved on any supported release target +- **THEN** `path` MUST be an array of PATH entries in lookup order +- **AND** platform path-list separators MUST NOT appear inside entries unless they are part of the entry text itself +- **AND** empty path entries MUST be omitted + +#### Scenario: ZFS output shape + +- **WHEN** usable ZFS command output is available on a supported platform +- **THEN** `zfs.feature_numbers` MUST be an array of supported filesystem version strings +- **AND** `zfs.version` MUST be the latest supported filesystem version string +- **AND** `zfs_featurenumbers` and `zfs_version` MUST be absent + +#### Scenario: Zpool output shape + +- **WHEN** usable Zpool command output is available on a supported platform +- **THEN** `zpool.feature_numbers` MUST be an array of supported pool version strings +- **AND** `zpool.feature_flags` MUST be an array of supported pool feature flag strings when feature flags are available +- **AND** `zpool.version` MUST be the latest supported pool version string, or `5000` when feature flags are present +- **AND** `zpool_featurenumbers`, `zpool_featureflags`, and `zpool_version` MUST be absent + diff --git a/openspec/specs/plan9-platform-facts/spec.md b/openspec/specs/plan9-platform-facts/spec.md new file mode 100644 index 00000000..c6a29a3a --- /dev/null +++ b/openspec/specs/plan9-platform-facts/spec.md @@ -0,0 +1,137 @@ +# plan9-platform-facts Specification + +## Purpose +TBD - created by archiving change add-plan9-platform-facts. Update Purpose after archive. +## Requirements +### Requirement: Plan 9 identity facts +Facts SHALL emit canonical Plan 9 identity facts when running on `GOOS=plan9`. + +#### Scenario: Plan 9 OS and kernel identity +- **WHEN** the `facts` CLI runs on Plan 9 +- **THEN** `os.name` MUST be `Plan 9` +- **AND** `os.family` MUST be `Plan 9` +- **AND** `kernel.name` MUST be `Plan 9` + +#### Scenario: Plan 9 hostname +- **WHEN** `/dev/sysname` contains a non-empty system name +- **THEN** Facts MUST use the trimmed `/dev/sysname` value for `networking.hostname` + +#### Scenario: Missing Plan 9 hostname source +- **WHEN** `/dev/sysname` is missing or empty +- **THEN** Facts MUST omit `networking.hostname` rather than inventing a hostname + +### Requirement: Plan 9 release and version facts are not invented +Facts SHALL NOT derive OS or kernel release facts from Plan 9 sources whose semantics do not match those facts. + +#### Scenario: Plan 9 protocol version is not OS release +- **WHEN** `/dev/osversion` is present on Plan 9 +- **THEN** Facts MUST NOT use it as `os.release`, `os.release.full`, `os.release.major`, `kernel.release.full`, `kernel.release.major`, or `kernel.version.full` + +#### Scenario: Unsupported Plan 9 release facts +- **WHEN** the first Plan 9 support slice runs +- **THEN** Facts MUST omit `os.release.*` and `kernel.release.*` facts unless a later spec identifies a correct Plan 9 release source + +### Requirement: Plan 9 architecture and hardware facts +Facts SHALL report Plan 9 architecture and hardware facts from Go runtime values and native Plan 9 CPU sources. + +#### Scenario: Plan 9 architecture +- **WHEN** Facts runs on Plan 9 +- **THEN** `os.architecture` MUST be derived from the Plan 9 architecture value, preferring `$objtype` when available and falling back to `runtime.GOARCH` +- **AND** `os.hardware` MUST be derived from the same architecture source + +#### Scenario: Plan 9 processor ISA +- **WHEN** Facts can read `$cputype`, `$objtype`, or `runtime.GOARCH` +- **THEN** `processors.isa` MUST contain the best available Plan 9 processor architecture value + +#### Scenario: Plan 9 processor model +- **WHEN** `/dev/cputype` or `/dev/archctl` contains a CPU model string +- **THEN** Facts MUST include that model in `processors.models` + +### Requirement: Plan 9 memory facts +Facts SHALL report Plan 9 memory totals from `/dev/swap`. + +#### Scenario: Plan 9 total memory +- **WHEN** `/dev/swap` contains a line matching ` memory` +- **THEN** `memory.system.total_bytes` MUST equal that byte count +- **AND** `memory.system.total` MUST be the human-readable value derived from that byte count using the existing Facts formatting rules + +#### Scenario: Plan 9 memory parser ignores unsupported lines +- **WHEN** `/dev/swap` includes page size, user page, or swap page lines +- **THEN** the first Plan 9 support slice MUST NOT require those lines to produce `memory.system.available`, `memory.system.used`, or `memory.swap.*` + +#### Scenario: Plan 9 memory source missing +- **WHEN** `/dev/swap` cannot be read or does not contain a memory total +- **THEN** Facts MUST omit Plan 9 memory facts rather than returning zero + +### Requirement: Plan 9 processor count facts +Facts SHALL report Plan 9 processor count from `/dev/sysstat`. + +#### Scenario: Plan 9 processor count +- **WHEN** `/dev/sysstat` contains one or more processor status lines +- **THEN** `processors.count` MUST equal the number of non-empty processor status lines + +#### Scenario: Plan 9 processor count source missing +- **WHEN** `/dev/sysstat` cannot be read or contains no processor status lines +- **THEN** Facts MUST omit `processors.count` rather than returning zero + +### Requirement: Plan 9 basic IPv4 networking facts +Facts SHALL report basic Plan 9 IPv4 networking facts from the native `/net` filesystem. + +#### Scenario: Plan 9 interface address +- **WHEN** `/net/ipifc/*/status` contains an IPv4 address row +- **THEN** Facts MUST parse the interface IP address, network prefix, netmask, and network address from that row + +#### Scenario: Plan 9 IPv4 mapped prefix +- **WHEN** a Plan 9 IPv4 address row uses an IPv6-mapped prefix such as `/120` +- **THEN** Facts MUST convert it to the equivalent IPv4 prefix length such as `/24` before deriving `networking.netmask` and `networking.network` + +#### Scenario: Plan 9 interface MAC address +- **WHEN** the Plan 9 interface device has an `addr` file such as `/net/ether0/addr` +- **THEN** Facts MUST parse that value as the interface MAC address + +#### Scenario: Plan 9 primary network +- **WHEN** `/net/iproute` contains a default IPv4 route +- **THEN** Facts MUST use that route to select `networking.primary` and the top-level `networking.ip`, `networking.netmask`, `networking.network`, and `networking.mac` values + +#### Scenario: Plan 9 DHCP metadata +- **WHEN** `/net/ndb` lacks DHCP server metadata +- **THEN** Facts MUST omit DHCP server facts rather than inventing a value + +### Requirement: Plan 9 uptime facts +Facts SHALL report Plan 9 uptime from the native Plan 9 `uptime` command. + +#### Scenario: Plan 9 uptime format +- **WHEN** `uptime` returns a value like `cirno up 0 days, 01:35:26` +- **THEN** Facts MUST parse the day, hour, minute, and second values into `system_uptime.seconds` +- **AND** Facts MUST produce the existing `system_uptime` structure from that duration + +#### Scenario: Plan 9 uptime source missing +- **WHEN** `uptime` cannot be executed or does not match the Plan 9 format +- **THEN** Facts MUST omit uptime facts rather than returning zero + +### Requirement: Plan 9 timezone facts +Facts SHALL report the Plan 9 timezone using Go local time behavior or native Plan 9 timezone sources. + +#### Scenario: Plan 9 timezone from Go runtime +- **WHEN** Go's local time support returns a timezone abbreviation on Plan 9 +- **THEN** Facts MUST use the existing timezone fact path and formatting + +#### Scenario: Plan 9 timezone fallback +- **WHEN** Go's local time support cannot provide the timezone abbreviation +- **THEN** Facts MAY parse `date`, `date -t`, `/env/timezone`, or `/adm/timezone/local` to produce the existing timezone fact + +### Requirement: Plan 9 unsupported first-slice facts +Facts SHALL omit Plan 9 facts whose native semantics are unresolved in the first support slice. + +#### Scenario: Plan 9 mount and filesystem facts +- **WHEN** Facts runs on Plan 9 in the first support slice +- **THEN** Facts MUST NOT claim support for mountpoint capacity, filesystem inventory, disk inventory, or partition inventory + +#### Scenario: Plan 9 load facts +- **WHEN** `/dev/sysstat` exposes Plan 9 load data +- **THEN** Facts MUST NOT map it to `load_averages` unless a later spec defines the conversion + +#### Scenario: Plan 9 virtualization facts +- **WHEN** virtio or VM-specific devices are visible on the lab guest +- **THEN** Facts MUST NOT require exact virtualization classification in the first Plan 9 release gate + diff --git a/schema_test.go b/schema_test.go index 97c93072..c8e059fb 100644 --- a/schema_test.go +++ b/schema_test.go @@ -59,6 +59,7 @@ var schemaPlatforms = map[string]bool{ "netbsd": true, "dragonfly": true, "illumos": true, + "plan9": true, } func loadSchema(t *testing.T) map[string]schemaEntry { diff --git a/tools/plan9-release-gate.rc b/tools/plan9-release-gate.rc new file mode 100644 index 00000000..027a9fe1 --- /dev/null +++ b/tools/plan9-release-gate.rc @@ -0,0 +1,49 @@ +#!/bin/rc +# Plan 9 release gate: verifies the first Plan 9 fact set through a built +# facts binary using Plan 9 rc syntax. It intentionally avoids POSIX sh. +# +# Usage: plan9-release-gate.rc [path-to-facts-binary] + +rfork e + +facts_bin=./facts +if(! ~ $#* 0) + facts_bin=$1 + +fn fail { + echo plan9-release-gate: $* >[1=2] + exit fail +} + +fn assert_present { + fact=$1 + if($facts_bin --json $fact | grep null >/dev/null) + fail fact $fact is null or missing +} + +fn assert_null { + fact=$1 + if(! $facts_bin --json $fact | grep null >/dev/null) + fail unsupported fact $fact is present +} + +fn assert_match { + fact=$1 + pattern=$2 + if(! $facts_bin --json $fact | grep $pattern >/dev/null) + fail fact $fact did not match $pattern +} + +assert_match os.name '"os.name": "Plan 9"' +assert_match os.family '"os.family": "Plan 9"' +assert_match kernel.name '"kernel.name": "Plan 9"' + +required=(os.architecture os.hardware networking.hostname networking.primary networking.ip networking.netmask networking.network networking.mac memory.system.total memory.system.total_bytes processors.count processors.isa processors.models system_uptime system_uptime.seconds timezone path) +for(fact in $required) + assert_present $fact + +unsupported=(os.release os.release.full os.release.major kernel.release.full kernel.release.major kernel.version.full filesystems mountpoints disks partitions load_averages dmi cloud.provider ec2_metadata gce az_metadata fips_enabled virtual is_virtual networking.dhcp networking.mtu) +for(fact in $unsupported) + assert_null $fact + +echo plan9-release-gate: first Plan 9 fact set present and unsupported facts absent diff --git a/tools/supportedfacts/main.go b/tools/supportedfacts/main.go index dc19102a..65c61f77 100644 --- a/tools/supportedfacts/main.go +++ b/tools/supportedfacts/main.go @@ -35,6 +35,7 @@ var platforms = []platform{ {ID: "netbsd", Label: "NetBSD"}, {ID: "dragonfly", Label: "DragonFly BSD"}, {ID: "illumos", Label: "illumos"}, + {ID: "plan9", Label: "Plan 9"}, } func main() { @@ -229,4 +230,15 @@ var exampleJSON = map[string]string{ "zfs": {"feature_numbers": ["1", "2", "3", "4", "5"], "version": "5"}, "zpool": {"feature_numbers": ["1", "2", "3", "4", "5"], "version": "5000"} }`, + "plan9": `{ + "facterversion": "dev", + "kernel": {"name": "Plan 9"}, + "memory": {"system": {"total": "1018.38 MiB", "total_bytes": 1067843584}}, + "networking": {"hostname": "plan9host", "interfaces": {"ether0": {"bindings": [{"address": "192.0.2.90", "netmask": "255.255.255.0", "network": "192.0.2.0"}], "ip": "192.0.2.90", "netmask": "255.255.255.0", "network": "192.0.2.0", "mac": "52:54:00:12:34:90"}}, "primary": "ether0", "ip": "192.0.2.90", "netmask": "255.255.255.0", "network": "192.0.2.0", "mac": "52:54:00:12:34:90"}, + "os": {"architecture": "amd64", "family": "Plan 9", "hardware": "amd64", "name": "Plan 9"}, + "path": ["/bin", "/usr/glenda/bin"], + "processors": {"count": 1, "isa": "amd64", "models": ["Core 2/Xeon 3600"]}, + "system_uptime": {"days": 0, "hours": 23, "seconds": 83717, "uptime": "23:15 hours"}, + "timezone": "CET" + }`, }