Facts¶
Collect a host’s current state with from rmote.tools import facts and
await facts.fetch(remote, sections=["system", "network"]). Each collector is an
ordinary Tool, and fetch calls every selected one in its own RPC, so a branch
arrives as soon as the target produces it. rmote.cache.Cache lives on the
controller. Each collector owns a top-level key, so refreshing one branch
preserves the others.
The connection can use any of rmote’s transports; the collectors and their call interface stay the same.
facts.gather collects on the side that runs it. Pass it through RPC
(await remote(facts.gather)) to do the whole collection on the target in one
call and one answer, or call it directly to collect the local host.
What available means¶
Every branch carries available. It reports whether the branch read the kernel
interface it is built on, which today means Linux. It does not mean “this
branch has no data”: a branch that can answer through the standard library does
so whatever available says.
A target that is POSIX but not Linux therefore reports this much:
Branch |
Reported without the Linux interface |
Source |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
everything; neither needs a kernel interface |
standard library |
The other branches report available=False and nothing else. apt, pacman,
systemd* and networkd describe software the target does not have. network
reads Linux sysfs and ip, and no portable call replaces them. No collector
raises on any platform: an absent source gives None, not an error.
A collector does not run an external command to fill a gap. sysctl,
diskutil and system_profiler would report more on macOS, and the project
reads kernel files and the standard library only.
Print a snapshot over SSH¶
From a checkout, run the SSH facts example:
uv run examples/ssh-facts.py user@server another-server > facts.json
Hosts are collected concurrently. Standard output contains one JSON object
keyed by the supplied hostnames, even for a single host. Each host contains
system, cpu, memory, python, network, networkd, storage,
systemd, systemd_timesync, systemd_resolved, apt, and pacman
branches. The script saves them in
.cache/facts using JSONCacheDir and reuses branches younger than 60 seconds.
Set --max-age SECONDS to change this lifetime or --max-age 0 to refresh all
collectors. When every branch is fresh, the script returns the cached snapshot
without opening SSH. Missing or stale branches share one connection per host;
only those branches are collected again.
Errors go to standard error and produce a nonzero exit status.
System and Python facts use Python’s standard library. Network properties
come from Linux sysfs; addresses, routes and rules use ip. The common
network schema below describes what they expose.
The independent
networkd branch uses busctl to read systemd-networkd’s JSON description,
including configuration sources, link state and DNS/NTP settings. Both sources
are collected by default and cached separately. A stopped or missing networkd
leaves its branch unavailable without affecting collection through ip.
The networkd collector works on Ubuntu Noble (systemd 255); it does not depend
on networkctl JSON output. Systemd and
package collectors check for their required commands before running them.
Missing commands leave the corresponding branch unavailable; they do not
prevent other collectors from running. A command that is present but fails,
returns unsupported output or times out raises an error. Collection reads
state without changing services, packages or repository indexes.
See the operations and types below for each collector’s fields, availability flags and requirements.
Choose collectors¶
facts.DEFAULT_COLLECTORS is the tuple of built-in collector classes used by
fetch, gather and validate when collectors is omitted:
Branch key |
Collector |
Result model |
|---|---|---|
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
Use sections=["system", "python"] to select keys from that default set.
Pass collectors=(SystemFacts, PythonFacts) to define a different set instead;
it replaces the default tuple. sections then selects only from the supplied
collectors. Import collector classes from their defining modules, as below.
Refuse the source texts¶
A branch keeps the text of every source it read in raw, so a field the model
does not have can still be read from the snapshot. Those texts are most of a
snapshot: on a host with sixteen processors the cpu branch measures 45 385
bytes pickled, of which 41 980 are the text of /proc/cpuinfo, and the
memory branch measures 1 503 bytes, of which 1 190 are /proc/meminfo. The
two branches together shrink 12.6 times without them.
raw=False asks every collector that can to parse its sources and drop their
texts. The parsed fields are the same, and for fetch the texts never reach
the wire. A collector that does not accept the keyword is collected as it is,
with its texts; facts.drops_raw(Collector.collect) answers which is which.
import asyncio
from rmote.protocol import Protocol
from rmote.tools import facts
from rmote.tools.facts.collectors import MemoryFacts, PythonFacts
async def lean() -> None:
async with await Protocol.from_command() as remote:
branch = await facts.fetch(remote, collectors=(MemoryFacts, PythonFacts), raw=False)
assert branch["memory"].raw == {}
asyncio.run(lean())
This example collects two branches locally and configures a cache with the same expected schema versions:
import asyncio
from typing import Any
from rmote.cache import Cache
from rmote.tools import facts
from rmote.tools.facts.collectors.python import PythonFacts, PythonInfo
from rmote.tools.facts.collectors.system import SystemFacts, SystemInfo
collectors = (SystemFacts, PythonFacts)
snapshot = asyncio.run(facts.gather(collectors=collectors))
assert set(snapshot) == {"system", "python"}
assert isinstance(snapshot["system"], SystemInfo)
assert isinstance(snapshot["python"], PythonInfo)
cache = Cache[Any](versions={collector.key: collector.version for collector in collectors})
cache.update(snapshot)
assert facts.validate(cache.data, collectors=collectors) == snapshot
For remote collection, pass the same tuple to
await facts.fetch(remote, collectors=collectors).
CollectorRegistry validates collector
definitions and selects keys for these helpers. Normal callers use the helpers
directly; custom collection code can use the registry to validate its own
branches. It belongs to facts, while the general cache stores values and versions.
Common network schema¶
NetworkFacts uses a platform-neutral result model. The built-in collector
currently reads network state only on Linux, using sysfs, iproute2 and
/etc/resolv.conf. On other operating systems it returns available=False,
interfaces=None, dns=None and an empty raw. The ipv4 and ipv6 objects
still exist, with addresses=None and routes=None; both default exits are
None too.
On Linux, a returned result has available=True even if individual sources
are absent. Check each section for None to determine which data was observed.
Interface names remain native (eth0,
enp3s0, Unicode aliases); they are keys in
network.interfaces. Indices identify interfaces within the current host,
not across hosts or interface recreation.
Field |
Meaning |
|---|---|
|
Whether the collector supports this platform; currently true only on Linux |
|
Mapping from interface name to |
|
Flat lists of |
|
Lists of |
|
One primary exit: |
|
Resolver endpoints: |
|
|
|
Interface properties and statistics read from sysfs |
|
Parsed resolver endpoints from |
The raw keys identify data sources. On Linux, sysfs, iproute and
resolv_conf coexist. Without the ip command, raw.iproute.available is
false and its ipv4/ipv6 sections are null. Missing sysfs or resolver files
leave raw.sysfs.interfaces or raw.resolv_conf null; successful empty reads
produce an empty mapping or list. Unsupported platforms return an empty raw.
The primary-exit summary uses native routes and addresses from raw.iproute.
For example, an observed address has this JSON representation:
{
"interface": "eth0",
"ifindex": 7,
"address": "192.0.2.10",
"prefixlen": 24
}
MAC addresses use lowercase colon notation, MTU is in bytes, and transmit link
speed is in bits per second. State uses up, down, testing, unknown,
dormant, notpresent or lowerlayerdown; a missing value is null.
A per-family MTU difference without a known link MTU leaves common mtu
null.
Route destinations always use CIDR notation, including 0.0.0.0/0 and ::/0
for default routes and /32 or /128 for host routes. An on-link route has no
gateway. Linux multipath routes keep a nexthops list, each with interface,
ifindex, gateway, and weight; ordinary routes have an empty list. A route
without a single outgoing interface has interface and ifindex set to null.
metric is the route metric. These records describe observations, not a portable
routing-policy configuration. Linux route types, tables, rules, advanced
next-hop attributes and all original iproute2 fields remain in raw.iproute.
Routes from different tables may have identical common fields.
default.ipv4 and default.ipv6 always exist; each is null when no usable
default route is observed. Linux considers unicast defaults in the main table,
ordered by route metric. Down or missing
interfaces are excluded. Ties use interface index and gateway; multipath chooses
one representative next hop while retaining all hops in routes.
The selected interface’s address prefers Linux prefsrc, then a non-link-local
usable address, then numeric address order. Tentative, duplicate and deprecated
addresses are excluded. If no usable
address is observed, address and prefixlen are null while the route remains.
This is a summary of the main exit, not a destination-specific routing lookup:
policy rules, ECMP hashing and source-address policy can produce another result.
Linux DNS endpoints come from /etc/resolv.conf and have null interface
fields. They can be local stubs, such as 127.0.0.53; they are not assumed
to be upstream servers. The list does not describe split-DNS policy or DNS
selection order. For the Linux
service view, use the independent systemd_resolved and networkd branches.
Unavailable sections are null; a successful query with no entries gives
[] or {}. The ipv4 and ipv6 objects always exist, even when their
addresses and routes are null. Linux without ip still provides available
sysfs and resolver facts. Errors and timeouts from available commands
propagate rather than being cached as an empty network.
Linux queries observe the RPC process’s network namespace and mounted sysfs. Separate queries are not an atomic snapshot. Each command has a 30-second timeout. The remote needs only Python’s standard library and the OS commands.
Processors and memory¶
The cpu branch reports the model, the architecture, how many processors the
machine has and how many this process may use, the topology, the caches and
the state of every hardware mitigation. The memory branch reports what the
kernel manages, what is free, what is available and the swap.
Every field of both branches may be absent, because a target can hide its source or refuse a read. Name the gap instead of substituting a zero:
import asyncio
from rmote.tools import facts
from rmote.tools.facts.collectors.cpu import CpuInfo
from rmote.tools.facts.collectors.memory import MemoryInfo
def report_capacity(snapshot):
"""Describe the processor and the memory of one host, naming every gap."""
cpu, memory = snapshot["cpu"], snapshot["memory"]
threads = cpu.topology.logical if cpu.topology else None
parts = [f"{cpu.model or 'unknown processor'} ({cpu.architecture})"]
parts.append(f"{threads} threads" if threads else "thread count unknown")
if memory.total_bytes is None:
parts.append("memory unknown")
else:
parts.append(f"{memory.total_bytes / 2**30:.1f} GiB total")
if memory.available_bytes is None:
parts.append("available memory unknown")
else:
parts.append(f"{memory.available_bytes / 2**30:.1f} GiB available")
return ", ".join(parts)
# The machine running this example, whatever it exposes.
print(report_capacity(asyncio.run(facts.gather(sections=["cpu", "memory"]))))
# A target that exposes nothing still gives a readable report.
nothing = {"cpu": CpuInfo(available=False, architecture="aarch64"), "memory": MemoryInfo(available=False)}
assert report_capacity(nothing) == "unknown processor (aarch64), thread count unknown, memory unknown"
# A target that reports its memory but hides its topology.
partial = {
"cpu": CpuInfo(available=True, architecture="x86_64", model="Example CPU"),
"memory": MemoryInfo(available=True, total_bytes=8 * 2**30),
}
assert report_capacity(partial) == (
"Example CPU (x86_64), thread count unknown, 8.0 GiB total, available memory unknown"
)
The installed count and the usable count are separate. topology.logical
counts the online processors of the machine, and usable counts those this
process may run on, which a CPU affinity or a control group reduces. quota
expresses a control group CPU limit as a number of processors, so a container
limited to half a processor reports 0.5.
/proc/cpuinfo repeats almost every field for each processor, so only the
first block describes the model, and the counts come from
/sys/devices/system/cpu. Sockets and cores per socket come from the
identifiers of /proc/cpuinfo; an architecture that omits them leaves those
counts null instead of a guess. vulnerabilities maps each mitigation the
kernel knows to its current state, such as Not affected or
Mitigation: Enhanced IBRS. A target without the cpufreq driver, such as
most virtual machines, reports only the current frequency.
Memory values are bytes, converted from the kibibytes of /proc/meminfo. A
target without that file reports total_bytes from sysconf, which counts the
same pages, and free_bytes when the system names the free ones; macOS has no
such name and leaves it null.
total_bytes is what the kernel manages, which is slightly below the
installed hardware because firmware and the kernel reserve some of it.
available_bytes is the kernel’s own estimate of what a new workload can
take without swapping, and it exceeds free_bytes because reclaimable caches
count towards it. used_bytes is total minus available, not total minus free,
so a cache does not look like consumption. limit_bytes and usage_bytes
come from control-group files, alongside the totals of the machine. The
complete /proc/meminfo stays in raw.
Control-group limits¶
The internal rmote.tools.facts.collectors.cgroup helper reads cgroup v2
only. It finds the collecting process’s group from the 0::<path> entry
in /proc/self/cgroup, relative to /sys/fs/cgroup. On a cgroup v1-only
host, or when that entry cannot be read, cpu.quota, memory.limit_bytes
and memory.usage_bytes are None.
A limit reports the strictest group, because the kernel enforces the limit of
every ancestor. The helper starts at the process’s group, walks towards
/sys/fs/cgroup, including the root, and compares every readable file it
finds. A file that says max, is missing, is unreadable or holds malformed
text declares no limit and is skipped, so a child with memory.max=max still
reports the finite limit of its parent. None means that no group of the
chain limits the process.
memory.limit_bytes compares memory.max. cpu.quota compares cpu.max,
whose values are a quota and a period: each pair becomes a number of
processors before the comparison, because the periods of two groups can
differ. A child with 200000 100000 nested in a parent with 100000 1000000
reports 0.1, not 2.0, although the parent names the larger quota.
memory.usage_bytes reads memory.current in the process’s own group and
never in a parent, because a parent file counts the processes of its other
children too. It is None when that group has no readable usage file.
The walk ends at the visible mount root. Inside a cgroup namespace,
/proc/self/cgroup names the group relative to the namespace root, so a
limit set above that boundary is enforced but cannot be read. None
therefore means “no visible limit” and does not prove that the process is
unrestricted.
Storage, mounts and free space¶
The storage branch describes mounted filesystems with their space, block
devices and swap. Every source is a kernel file or a standard library call, so
no command runs and no privileges are needed.
def report_free_space(snapshot):
"""Print the space a caller can still use on every real filesystem."""
for mount in snapshot["storage"].mounts or ():
if mount.usage and not mount.pseudo:
free = mount.usage.available_bytes / 2**30
print(f"{mount.target:<20}{mount.fstype:<8}{free:.1f} GiB free")
Mounts come from /proc/self/mountinfo, which also gives the device number,
the mount root and the shadowing order. The older /proc/mounts is the
fallback and reports neither a device number nor a root. Octal escapes in a
path, such as \040 for a space, are decoded.
Mounts keep the order of the kernel, so a mount that shadows an earlier one at
the same target stays visible. Nothing is filtered: a kernel filesystem that
stores no data carries pseudo, and the caller selects what it needs.
Space comes from os.statvfs for each real filesystem, in bytes.
free_bytes is what the filesystem reports as unused, and available_bytes
is what an unprivileged process can still use; the two differ when the
filesystem reserves space for the superuser. usage is null for a pseudo
filesystem, which is not queried, or when os.statvfs raises OSError, such
as a permission error. A filesystem without inodes reports null inode counts.
Block devices come from /sys/block. Sizes are bytes, converted from the
512-byte sectors that sysfs always reports, whatever the hardware block size.
Each whole device lists the sizes of its partitions. Swap areas come from
/proc/swaps, converted from kibibytes to bytes. The configured table of
/etc/fstab is reported without interpretation.
available reports whether the mount table could be read. Each section is
null when its source is absent, which separates an unsupported target from an
empty but successful read. One unreadable source leaves its own section null
and keeps the others. raw keeps the text of every source that was read.
root is the space of the root filesystem and is filled whatever available
says, because os.statvfs answers on every POSIX target while the mount table
does not. On Linux the same numbers are the usage of the / mount. A target
without the mount table reports root alone: no portable call lists the
mounts, so mounts stays null there.
os.statvfs on an unresponsive mount can block indefinitely. The collector has
no per-mount timeout: a blocked call does not produce a result with usage=None.
It runs in a worker thread, so other collectors can progress, but fetch and
gather wait for all selected branches before returning a snapshot.
storage is included in DEFAULT_COLLECTORS. If a collection must avoid
waiting on mounts, select other branches explicitly, for example
sections=["system", "python"], or supply a collector set without
StorageFacts. Cancelling an RPC only stops local waiting; it does not
interrupt a remote statvfs call already in progress.
Choose a cache and reuse results¶
Collection, freshness and persistence are explicit operations. Load saved entries, check which sections need refreshing, then open a connection only if needed:
import asyncio
import time
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from rmote.cache import Cache, SQLiteCache
from rmote.protocol import Protocol
from rmote.tools import facts
async def main(directory):
backend = SQLiteCache(Path(directory) / "facts.sqlite")
cache = Cache[Any](versions={c.key: c.version for c in facts.DEFAULT_COLLECTORS})
host = "user@server"
keys = ["system", "python"]
await cache.load(backend, namespace=host, keys=keys)
missing = cache.stale(keys=keys, max_age=300)
if missing:
async with await Protocol.from_ssh(host) as remote:
started = time.time()
cache.update(
await facts.fetch(remote, sections=missing, concurrency=4),
collected_at=started,
)
await cache.save(backend, namespace=host, keys=missing)
snapshot = facts.validate(cache.data)
assert set(snapshot) == set(keys)
assert snapshot["system"].hostname
assert snapshot["python"].version
assert not cache.stale(keys=keys)
# Read persisted facts without opening a connection.
saved = Cache[Any]()
await saved.load(backend, namespace=host, keys=keys)
assert saved.data == snapshot
saved.invalidate(keys=["python"])
assert set(saved.data) == {"system"}
# Invalidation above affects memory only. Durable deletion is explicit.
await backend.delete(host, "python")
with TemporaryDirectory() as directory:
asyncio.run(main(directory))
For an in-memory cache alone, the entire operation is:
cache = Cache[Any]()
cache.update(await facts.fetch(remote))
update is synchronous and replaces supplied sections in memory, preserving
others. The cache retains values as supplied, without copying or freezing them.
cache.data is a read-only key mapping sharing those values. Their owner is
responsible for mutations. Fact model validation belongs to fetch, gather
and the explicit facts.validate(cache.data) helper. By default,
timestamps come from the controller at update time. Supply collected_at with
the collection start time to order overlapping observations. Older timestamps
cannot overwrite newer ones in memory or in the bundled backends.
stale(max_age=300) identifies sections older than five minutes. Zero requests
all selected sections; None accepts any age. Missing sections and schema-version
mismatches always require collection. load merges saved entries regardless of
age, omitting incompatible versions and retaining newer in-memory observations.
A failed load leaves the existing data unchanged.
Cache[Any]() performs no I/O and chooses no backend. Use JSONCacheDir(path)
or SQLiteCache(path) explicitly. SQLite’s parent directory must already exist.
JSON documents use percent-encoded host names, such as admin@server.json,
and one-space indentation. Both backends support multiple controller processes
and atomic replacement of the selected sections as a batch. JSON reads and
replaces the host document once per save; SQLite uses one transaction. A failed
batch does not leave earlier sections persisted. Newer timestamps are preserved
independently for each key, and omitted keys are not deleted. Loading selected
sections reads one consistent backend snapshot.
Custom backends implement CacheBackend.get_many(namespace, keys) and
set_many(namespace, entries) as well as single-key get, set, and delete.
get_many returns a dictionary of entries, omitting missing keys. set_many
validates and merges the entire batch atomically. Serialization and blocking
storage work belong in the backend’s worker thread. Cancelling an await does
not stop an already-running worker; its batch may still commit.
Use one Cache per host and execution context. Its first load/save binds its namespace; another namespace requires a separate instance. Cache freshness never opens a connection or initiates collection automatically.
Both helpers perform independent observations, not an atomic snapshot of the
host. If any collector fails, neither returns a result, so
cache.update(await facts.fetch(remote)) leaves the entire cache unchanged.
Unfinished siblings are cancelled and awaited; already running synchronous work
cannot be stopped. Cancelling fetch stops local waiting only, as described in
Concepts. Call a collector on its own when a failing branch must not
affect the others. Concurrent collections are independent, without implicit
coalescing.
Add a collector¶
Define a Tool class in a readable module, as described in
Writing Tools. Give it a unique key, a positive integer version and
a result_type dataclass and a static collect() method returning that model. Increment
version when the cached structure or meaning changes. Helper methods belong
inside the collector class; use async def and await async_process(...) for
command queries.
Pass collector classes in await facts.fetch(remote, collectors=[...]) and
configure the local Cache[Any](versions={c.key: c.version for c in [...]}) with the same definitions. This
replaces the default collector set. Use sections=[...] to choose registered
keys. Tool classes passed as arguments carry their source definitions using the
usual class Tool transfer rules; the cache stays on the controller.
Typed results and cache representations¶
Each collector declares its result dataclass in the same module as the Tool.
fetch(), gather() and facts.validate(cache.data) return dictionaries described by
FactsData(TypedDict, total=False). Literal string keys retain their model types:
snapshot["network"] is NetworkInfo, and snapshot["system"] is SystemInfo.
Any key may be absent after selecting sections or reading an incomplete cache;
check membership or use .get() when the key’s presence is uncertain.
Nested common records are dataclasses too, for example
snapshot["network"].default.ipv4. Check optional fields for None before
accessing their properties. Native raw, systemd properties and variable
vendor records remain dictionaries.
Direct remote calls preserve the same model type: await remote(NetworkFacts.collect)
with Protocol, or remote(NetworkFacts.collect) with Connection, returns
NetworkInfo. A helper accepting collector: Collector[T] keeps T through
either client. Cached and offline results
use the same dataclass types as fresh collections.
To export ordinary JSON or pass facts into a template context:
from dataclasses import asdict
from typing import cast
from rmote.serialization import Dataclass
plain = {key: asdict(cast(Dataclass, value)) for key, value in snapshot.items()}
A custom collector can declare its model alongside the Tool:
from dataclasses import dataclass
from rmote.protocol import Tool
@dataclass
class ExampleInfo:
enabled: bool
class ExampleFacts(Tool):
key = "example"
version = 1
result_type = ExampleInfo
@staticmethod
def collect() -> ExampleInfo:
return ExampleInfo(enabled=True)
Extend FactsData to describe custom collector keys. The collector registry
is dynamic, so a type checker cannot infer those keys from collectors=[...].
Declare the schema at the application boundary with cast; it does not convert
or copy the dictionary:
from typing import cast
from typing import Any
from rmote.cache import Cache
from rmote.tools.facts.schema import FactsData
from rmote.tools import facts
class ProjectFacts(FactsData, total=False):
example: ExampleInfo
async def collect_project(remote):
cache = Cache[Any](versions={c.key: c.version for c in [ExampleFacts]})
snapshot = cast(ProjectFacts, await facts.fetch(remote, collectors=[ExampleFacts]))
cache.update(snapshot)
assert snapshot["example"].enabled
return cast(ProjectFacts, facts.validate(cache.data, collectors=[ExampleFacts]))
total=False makes keys optional. Additional keys are declared by extending the
schema; unknown literal keys and incompatible model assignments remain type
errors. Built-in keys retain their declared model types even when supplied by
custom collectors. For dynamic iteration, TypedDict values are typed as
object, hence the cast in the JSON-export example above.
CacheEntry[T] carries the model, collection time and collector schema version.
A backend accepts and returns entries containing models. Cache never serializes
JSON: custom memory, pickle or database backends can use other representations.
The bundled backends use json_default() and json_object_hook() to preserve
class identities through JSON, recursively including nested models. The
dump_dataclass() and load_dataclass() helpers also expose a materialized
dictionary representation when needed:
from rmote.serialization import dump_dataclass, load_dataclass
from rmote.tools.facts.collectors.network import NetworkInfo
encoded = dump_dataclass(network)
restored = load_dataclass(encoded, NetworkInfo)
The record contains "_type": "dataclass" and an _value object with class
("module:qualname") and fields. Bytes use "_type": "bytes" and a base64
string in _value. The exact two-key shape { "_type": ..., "_value": ... }
is reserved and must not be used for ordinary data. Other values follow standard
JSON semantics, including conversion of tuples to lists. Unsupported objects,
cycles and function-local dataclasses are rejected.
init=False fields are recomputed by constructors. asdict() is intentionally
not used before cache encoding because it discards nested class identities.
Cache files are trusted local data: loading imports recorded modules and calls constructors. Keep model paths importable across runs. Constructors provide their normal validation; the helper does not validate every field against annotations. Increment the collector version when changing its model and migrate or clear entries if removing or renaming recorded classes. Malformed entries and I/O errors propagate; they do not silently become misses.
JSONCacheDir uses POSIX file locks. SQLiteCache does not import fcntl.
Operations and types¶
The rmote.tools.facts package exports fetch for remote collection, gather
for collection on the calling side, and validate for checking existing results.
fetch accepts an asynchronous callable, so custom transports can use the same
entry point. DEFAULT_COLLECTORS supplies the default set; Collector and
FactsData describe the collector contract and result types.
- async rmote.tools.facts.fetch(remote, *, sections=None, collectors=(<class 'rmote.tools.facts.collectors.apt.AptFacts'>, <class 'rmote.tools.facts.collectors.cpu.CpuFacts'>, <class 'rmote.tools.facts.collectors.memory.MemoryFacts'>, <class 'rmote.tools.facts.collectors.pacman.PacmanFacts'>, <class 'rmote.tools.facts.collectors.system.SystemFacts'>, <class 'rmote.tools.facts.collectors.python.PythonFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkdFacts'>, <class 'rmote.tools.facts.collectors.storage.StorageFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdResolvedFacts'>), concurrency=8, raw=True)[source]¶
Collect sections through remote, with one call for each collector.
Each collector travels as an ordinary Tool and runs in its own RPC, so a branch arrives as soon as the target produces it, and a long branch does not hold the others.
- Parameters:
remote (
Callable[...,Awaitable[Any]]) – Callable that performs one RPC, such as a Protocol instance.sections (
Iterable[Text] |None) – Keys to collect, or None for every registered collector.collectors (
Iterable[Collector[Any]]) – Collector Tool classes with key, version and result_type.concurrency (
getint) – Maximum simultaneous calls.raw (
bool) – False asks every collector that can to drop the source texts it read, so they never reach the wire.
- Return type:
Cancelling this call stops local waiting. It does not stop work already running on the target. Call the collectors separately when a failing branch must not affect the others.
- async rmote.tools.facts.gather(*, sections=None, collectors=(<class 'rmote.tools.facts.collectors.apt.AptFacts'>, <class 'rmote.tools.facts.collectors.cpu.CpuFacts'>, <class 'rmote.tools.facts.collectors.memory.MemoryFacts'>, <class 'rmote.tools.facts.collectors.pacman.PacmanFacts'>, <class 'rmote.tools.facts.collectors.system.SystemFacts'>, <class 'rmote.tools.facts.collectors.python.PythonFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkdFacts'>, <class 'rmote.tools.facts.collectors.storage.StorageFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdResolvedFacts'>), concurrency=8, raw=True)[source]¶
Collect sections on this side, returning only after all have succeeded.
- Parameters:
sections (
Iterable[Text] |None) – Keys to collect, or None for every registered collector.collectors (
Iterable[Collector[Any]]) – Collector Tool classes with key, version and result_type. Tool classes passed as RPC arguments carry their source definitions.concurrency (
getint) – Maximum simultaneous collectors in this call.raw (
bool) – False asks every collector that can to drop the source texts it read. The parsed fields stay; the texts are most of a snapshot.
- Return type:
Synchronous collectors run in worker threads.
- rmote.tools.facts.validate(values, *, collectors=(<class 'rmote.tools.facts.collectors.apt.AptFacts'>, <class 'rmote.tools.facts.collectors.cpu.CpuFacts'>, <class 'rmote.tools.facts.collectors.memory.MemoryFacts'>, <class 'rmote.tools.facts.collectors.pacman.PacmanFacts'>, <class 'rmote.tools.facts.collectors.system.SystemFacts'>, <class 'rmote.tools.facts.collectors.python.PythonFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkdFacts'>, <class 'rmote.tools.facts.collectors.storage.StorageFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdResolvedFacts'>))[source]¶
Validate cached or externally supplied branches and retain their models.
The returned dictionary shares the values. This optional facts-specific check is separate from the general-purpose cache.
- Return type:
Shared fact models and collector validation; no cache or transport I/O.
- class rmote.tools.facts.schema.Collector(*args, **kwargs)[source]¶
Bases:
Protocol[T_co]Structural interface implemented by a Tool class owning one facts key.
Increment
versionwhenever the cached shape or meaning changes.collectruns on the target and returns the complete branch, not a patch.
- class rmote.tools.facts.schema.CollectorRegistry(collectors=(<class 'rmote.tools.facts.collectors.apt.AptFacts'>, <class 'rmote.tools.facts.collectors.cpu.CpuFacts'>, <class 'rmote.tools.facts.collectors.memory.MemoryFacts'>, <class 'rmote.tools.facts.collectors.pacman.PacmanFacts'>, <class 'rmote.tools.facts.collectors.system.SystemFacts'>, <class 'rmote.tools.facts.collectors.python.PythonFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkdFacts'>, <class 'rmote.tools.facts.collectors.storage.StorageFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdResolvedFacts'>))[source]¶
Bases:
objectValidate collector definitions and select independent fact branches.
fetch,gatherandvalidateconstruct a registry for each call. Custom collection code can also use one directly. It performs no collection, transport or cache I/O. Thecollectorsattribute maps keys to the supplied collector classes in registration order.- Parameters:
collectors (
Iterable[Collector[Any]]) – Collector classes to register, replacing DEFAULT_COLLECTORS. Each needs a unique nonempty string key, a positive integer version, a callable collect method and a dataclass result_type. A built-in key must retain its FactsData model type or a subclass of that model.- Raises:
ValueError – A collector definition violates these requirements.
Example using a pre-existing result without collecting or opening a connection:
>>> registry = CollectorRegistry((PythonFacts,)) >>> registry.select() ('python',) >>> registry.select(['python', 'python']) ('python',) >>> registry.validate('python', PythonInfo(version='3.11', executable='/usr/bin/python3', implementation='CPython'))
- select(sections=None)[source]¶
Return unique selected keys in order, or all keys when sections is None.
An empty iterable selects nothing. A string is not a list of keys.
- Raises:
ValueError – sections is a string, or a selected key is unregistered.
- Return type:
- validate(key, data)[source]¶
Check a branch’s result_type without copying it or inspecting fields.
Subclasses of the declared dataclass are accepted. This does not check field annotations, nested values or freshness metadata.
- Raises:
ValueError – The key is unregistered or data has an incompatible type.
- Return type:
- rmote.tools.facts.schema.DEFAULT_COLLECTORS: tuple[Collector[Any], ...] = (<class 'rmote.tools.facts.collectors.apt.AptFacts'>, <class 'rmote.tools.facts.collectors.cpu.CpuFacts'>, <class 'rmote.tools.facts.collectors.memory.MemoryFacts'>, <class 'rmote.tools.facts.collectors.pacman.PacmanFacts'>, <class 'rmote.tools.facts.collectors.system.SystemFacts'>, <class 'rmote.tools.facts.collectors.python.PythonFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkFacts'>, <class 'rmote.tools.facts.collectors.network.NetworkdFacts'>, <class 'rmote.tools.facts.collectors.storage.StorageFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts'>, <class 'rmote.tools.facts.collectors.systemd.SystemdResolvedFacts'>)¶
Built-in collector classes used when gather, fetch or validate omits collectors. Pass a different iterable to those helpers to replace this set for that call.
- class rmote.tools.facts.schema.FactsData[source]¶
Bases:
TypedDictOptional built-in fact branches, stored in an ordinary dictionary.
Subsets and offline reads can omit any key. Extend this schema with the keys of custom collectors; their values remain dataclass instances.
- memory: MemoryInfo¶
- network: NetworkInfo¶
- networkd: NetworkdInfo¶
- pacman: PacmanInfo¶
- python: PythonInfo¶
- storage: StorageInfo¶
- system: SystemInfo¶
- systemd: SystemdInfo¶
- systemd_resolved: ResolvedInfo¶
- systemd_timesync: TimesyncInfo¶
Processor facts: model, architecture, counts, topology, caches and mitigations.
Every source is a kernel file or a standard library call. On a target without
the processor table of Linux the branch reports the counts that sysconf
gives. No command runs and no privileges are needed.
- class rmote.tools.facts.collectors.cpu.CpuCache(level, kind, size_bytes, shared_with)[source]¶
Bases:
objectOne cache of the first processor, with the processors that share it.
- class rmote.tools.facts.collectors.cpu.CpuFacts[source]¶
Bases:
ToolCollect the processor model, architecture, counts and mitigations.
Owns the
cpukey. Model, vendor and flags come from/proc/cpuinfo, counts and topology from/sys/devices/system/cpu, and the state of each hardware mitigation from itsvulnerabilitiesdirectory.The installed count and the usable count are reported separately. A container or a CPU affinity reduces what the process may use without changing what the machine has.
/proc/cpuinforepeats almost every field for each processor, so only the first block is kept; the per-processor values that do differ are the current frequency and the identifiers that topology already reports.- static collect(*, raw=True)[source]¶
Return the processor facts of the machine executing this method.
Without raw the branch keeps every parsed field and drops the text of
/proc/cpuinfo, which is about nine tenths of its size.- Return type:
- static count_range(value)[source]¶
Count the processors of a kernel range such as
0-3,8.- Return type:
getint|None
- key = 'cpu'¶
- static number_of(value)[source]¶
Convert a /proc/cpuinfo value to an integer, or None.
- Return type:
getint|None
- static parse_size(value)[source]¶
Convert a sysfs size such as
16384Kinto bytes.- Return type:
getint|None
- static portable_topology()[source]¶
Report the online processors of a target without the kernel table.
SC_NPROCESSORS_ONLNcounts them on every POSIX target. The fields that only the kernel table gives stay None: no portable call reports sockets, cores or the ranges.- Return type:
- static read_caches(root=PosixPath('/sys/devices/system/cpu'))[source]¶
List the caches of the first processor, ordered by level.
- static read_frequency(record, root=PosixPath('/sys/devices/system/cpu'))[source]¶
Read the frequency policy of the first processor.
A target without the cpufreq driver, such as most virtual machines, reports only the current frequency of
/proc/cpuinfo.- Return type:
- static read_quota()[source]¶
Express the strictest control group CPU limit as processors.
Every group of the chain enforces its own
cpu.max, and the periods can differ, so each pair becomes a number of processors before the comparison.max, absent and malformed files declare no limit and are skipped. None means that no visible group limits the process.- Return type:
getdouble|None
- static read_topology(records, root=PosixPath('/sys/devices/system/cpu'))[source]¶
Describe the arrangement of the online processors.
Sockets and cores come from the identifiers of
/proc/cpuinfowhen it reports them. An architecture that omits them leaves those counts None instead of a guess.- Return type:
- static read_vulnerabilities(root=PosixPath('/sys/devices/system/cpu'))[source]¶
Report the state of every hardware mitigation the kernel knows.
- static smt(root=PosixPath('/sys/devices/system/cpu'))[source]¶
Report whether simultaneous multithreading is active.
- static usable_count()[source]¶
Count the processors this process may run on.
CPU affinity exists on Linux only, so the installed count is the answer elsewhere.
- Return type:
getint|None
- version = 1¶
- class rmote.tools.facts.collectors.cpu.CpuFrequency(governor, driver, minimum_khz, maximum_khz, current_mhz)[source]¶
Bases:
objectFrequency policy of the first processor, in kilohertz.
- class rmote.tools.facts.collectors.cpu.CpuInfo(available, architecture, model=None, vendor=None, family=None, stepping=None, microcode=None, usable=None, quota=None, topology=None, frequency=None, caches=None, flags=None, bugs=None, vulnerabilities=None, raw=<factory>)[source]¶
Bases:
objectProcessor facts of the target.
availablereports whether the kernel exposes its processor table. A target without it still reports the architecture, the usable count and the online count intopology, so False means “no Linux processor table” and not “no data at all”.usableis how many processors this process may run on, which a CPU affinity or a control group can reduce below the installed count.quotais the control group limit expressed as a number of processors. It is the strictestcpu.maxof the groups the target shows, and None means that none of them limits the process.modelis absent on an architecture whose/proc/cpuinfogives no model name, for example arm64.vendorthen carries theCPU implementeridentifier.Each section is None when its source is absent, which separates an unsupported target from an empty but successful read.
rawkeeps the text of every source that was read.- frequency: CpuFrequency | None = None¶
- topology: CpuTopology | None = None¶
- class rmote.tools.facts.collectors.cpu.CpuTopology(logical, sockets, cores_per_socket, threads_per_core, numa_nodes, smt, online, present, offline, possible)[source]¶
Bases:
objectHow the online processors are arranged.
logicalcounts the online processors. The range strings keep the kernel’s own notation, so a sparse set stays readable.
Memory facts: what the kernel manages, what is free and what swap exists.
The source is /proc/meminfo and the control group limits of the running
process. On another POSIX target the branch reports the memory of the machine
through sysconf, which every such target answers. No command runs and no
privileges are needed.
- class rmote.tools.facts.collectors.memory.HugePages(size_bytes, total, free, reserved)[source]¶
Bases:
objectHuge page pool of the default size.
- class rmote.tools.facts.collectors.memory.MemoryFacts[source]¶
Bases:
ToolCollect installed, free and available memory, swap and huge pages.
Owns the
memorykey./proc/meminforeports its values in kibibytes, and every field here is bytes.used_bytesis derived as total minus available, which is what a workload actually finds occupied; it is not total minus free, because caches are reclaimable.The complete
/proc/meminfois kept inraw, so a field this model does not name is still available.A target without
/proc/meminforeports whatsysconfgives: the memory of the machine, and the free pages where the name exists. The other fields stay None, because no portable call reports them.- static collect(*, raw=True)[source]¶
Return the memory facts of the machine executing this method.
- Return type:
- key = 'memory'¶
- static parse(text)[source]¶
Parse /proc/meminfo into bytes, keeping its original field names.
A field in kibibytes is multiplied; a plain count stays as it is.
- Return type:
WSGIEnvironment[Text,getint]
- static portable()[source]¶
Report what every POSIX target gives, for a target without meminfo.
SC_PHYS_PAGEScounts the pages the kernel manages, which is whatMemTotalreports on Linux.SC_AVPHYS_PAGEScounts the pages that are free now; macOS has no such name and gives None.- Return type:
- static read_swap(values)[source]¶
Report swap totals, or None when the target has no swap table.
- Return type:
- static read_text(path=PosixPath('/proc/meminfo'))[source]¶
Read the memory table, treating an absent source as None.
- result_type¶
alias of
MemoryInfo
- version = 1¶
- class rmote.tools.facts.collectors.memory.MemoryInfo(available, total_bytes=None, free_bytes=None, available_bytes=None, used_bytes=None, buffers_bytes=None, cached_bytes=None, shared_bytes=None, swap=None, hugepages=None, commit_limit_bytes=None, committed_bytes=None, limit_bytes=None, usage_bytes=None, raw=<factory>)[source]¶
Bases:
objectMemory facts of the target, in bytes.
total_bytesis what the kernel manages, which is slightly below the installed hardware because firmware and the kernel reserve some of it.available_bytesis the kernel’s own estimate of what a new workload can take without swapping, and it is larger thanfree_bytesbecause reclaimable caches count towards it.limit_bytesandusage_bytescome from the control group, so a container reports what it may use next to what the machine has.limit_bytesis the strictestmemory.maxof the groups the target shows, and None means that none of them limits the process.usage_bytesis thememory.currentof the process’s own group, so it never counts the processes of a parent group.availablereports whether/proc/meminfowas read. A target without it still reportstotal_bytes, andfree_byteswhen itssysconfnames the free pages, so False means “no Linux memory table” and not “no data at all”.- swap: SwapMemory | None = None¶
- class rmote.tools.facts.collectors.memory.SwapMemory(total_bytes, free_bytes, used_bytes, cached_bytes)[source]¶
Bases:
objectSwap totals, in bytes.
Storage facts: mounted filesystems, their space, block devices and swap.
Every source is a kernel file or a standard library call. The space of the
root filesystem comes from os.statvfs, which every POSIX target answers,
so a target without the mount table of Linux still reports it. No external
command runs, and no privileges are needed.
- class rmote.tools.facts.collectors.storage.BlockDevice(name, size_bytes, logical_block_size, rotational, removable, model, partitions)[source]¶
Bases:
objectOne whole block device from sysfs, with the sizes of its partitions.
- class rmote.tools.facts.collectors.storage.FilesystemUsage(total_bytes, free_bytes, available_bytes, total_inodes, free_inodes, available_inodes)[source]¶
Bases:
objectSpace and inodes of one mounted filesystem, in bytes and counts.
free_bytesis what the filesystem reports as unused, andavailable_bytesis what an unprivileged process can still use. The two differ when the filesystem reserves space for the superuser.
- class rmote.tools.facts.collectors.storage.MountPoint(target, source, fstype, options, super_options, device, root, read_only, pseudo, usage)[source]¶
Bases:
objectOne mounted filesystem, as the kernel lists it.
usageis None for a pseudo filesystem, which is not queried, or whenos.statvfsraises OSError, such as a permission error. An unresponsive mount can instead block collection: there is no per-mount timeout that would turn a pending call into None.- usage: FilesystemUsage | None¶
- class rmote.tools.facts.collectors.storage.StorageFacts[source]¶
Bases:
ToolCollect mounted filesystems, their space, block devices and swap.
Owns the
storagekey.availablereports whether the mount table of the kernel could be read; the space of the root filesystem is reported on any POSIX target, becauseos.statvfsis portable and the mount table is not. Mounts come from/proc/self/mountinfo, which also gives the device number, the mount root and the shadowing order; the older/proc/mountsis the fallback. Space comes fromos.statvfsfor each mount, devices from/sys/blockand swap from/proc/swaps.Every mount stays in the result, including a pseudo filesystem, which carries
pseudoso the caller can select. Mounts keep the order of the kernel, so a mount that shadows an earlier one at the same target stays visible.A single unreadable source leaves its own section None and keeps the others. A mount whose
os.statvfsraises OSError retains its record withusage=None. An unresponsive mount can block indefinitely instead: there is no per-mount timeout, and the collector cannot interrupt the call.The collection helpers run this method in a worker thread. Other branches can progress, but fetch and gather return only after all selected branches succeed. This collector belongs to DEFAULT_COLLECTORS; select other keys or a collector set without StorageFacts to avoid querying mounts.
- static collect(*, raw=True)[source]¶
Return the storage facts of the machine executing this method.
Without raw the branch keeps every parsed field and drops the texts of the sources it read.
- Return type:
- key = 'storage'¶
- static parse_mountinfo(text)[source]¶
Parse /proc/self/mountinfo, keeping the order of the kernel.
Fields before the
-separator describe the mount, and the fields after it describe the filesystem. The optional fields between them vary by kernel, so the separator is found instead of counted.
- static parse_mounts(text)[source]¶
Parse the older /proc/mounts, which has no device number or root.
- static read_attribute(path)[source]¶
Read one sysfs attribute, or None when it is absent or refused.
- static read_devices(root=PosixPath('/sys/block'))[source]¶
Describe every whole block device and the sizes of its partitions.
- Return type:
WSGIEnvironment[Text,BlockDevice] |None
- static read_fstab(path=PosixPath('/etc/fstab'))[source]¶
Read the configured filesystem table, without interpreting it.
- static read_mounts(mountinfo=PosixPath('/proc/self/mountinfo'), mounts=PosixPath('/proc/mounts'))[source]¶
Build the mount list with its space, and return the source text too.
- static read_number(path, factor=1)[source]¶
Read one numeric sysfs attribute and scale it.
- Return type:
getint|None
- static read_swaps(path=PosixPath('/proc/swaps'))[source]¶
List active swap areas. The kernel reports their sizes in kibibytes.
- result_type¶
alias of
StorageInfo
- static unescape(value)[source]¶
Decode the octal escapes that the kernel writes in a path.
The kernel escapes space, tab, newline and backslash. Any other sequence stays as written.
- Return type:
- static usage(target)[source]¶
Report the space of one mount in bytes, or None on OSError.
The kernel reports space in fragments, so every number is multiplied by the fragment size. A filesystem without inodes reports None for them. This synchronous call has no timeout and may block on an unresponsive mount; a pending call does not return None.
- Return type:
- version = 1¶
- class rmote.tools.facts.collectors.storage.StorageInfo(available, root=None, mounts=None, devices=None, swaps=None, fstab=None, raw=<factory>)[source]¶
Bases:
objectStorage of the target: mounts with their space, devices and swap.
availablereports whether the kernel exposes its mount table. Each section is None when its source is absent, which separates an unsupported target from an empty but successful read.rawkeeps the text of every source that was read, by source name.rootis the space of the root filesystem, which every POSIX target reports. It is filled whateveravailablesays, so a target without the mount table of Linux still reports its space. On Linux the same numbers are the usage of the/mount.- devices: dict[str, BlockDevice] | None = None¶
- mounts: list[MountPoint] | None = None¶
- root: FilesystemUsage | None = None¶
- class rmote.tools.facts.collectors.storage.SwapArea(path, kind, size_bytes, used_bytes, priority)[source]¶
Bases:
objectOne active swap area. Sizes come from /proc/swaps, in bytes.
Platform-neutral network models collected from Linux sources, plus networkd.
- class rmote.tools.facts.collectors.network.DNSServer(address, interface, ifindex)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.network.DefaultRoute(interface, ifindex, address, prefixlen, gateway, metric)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.network.NetworkAddress(interface, ifindex, address, prefixlen)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.network.NetworkDefaults(ipv4=None, ipv6=None)[source]¶
Bases:
object- ipv4: DefaultRoute | None = None¶
- ipv6: DefaultRoute | None = None¶
- class rmote.tools.facts.collectors.network.NetworkFacts[source]¶
Bases:
ToolCollect Linux network state in a JSON-compatible schema.
Owns
network, schema version 1.interfacesmaps names toifindex,mac(lowercase colon-separated),state,mtu(bytes) andspeed_bps(transmit link speed). Missing properties are None.ipv4/ipv6contain flataddressesandroutesarrays:Address:
interface,ifindex,address,prefixlen.Route:
destination(CIDR),interface,ifindex,gateway(None for on-link),metricandnexthops. Multipath routes retain each next hop’sinterface,ifindex,gatewayandweight; ordinary routes have an empty next-hop array. Metric is the route metric.dns: a list ofaddress,interfaceandifindexrecords, read from /etc/resolv.conf, which can be a local stub with no interface.
raw.sysfs.interfacespreserves interface properties and statistics.raw.iproute.ipv4/ipv6preserve addresses, routes from all tables and policy rules;raw.iproute.availablereports the ip command.raw.resolv_confretains the parsed resolver endpoints. Common routes alone are insufficient to reproduce routing policy.networkdremains an independent collector.Collection is implemented only on Linux. Other platforms return
NetworkInfo(available=False)with None sections, emptyrawand no default exits; the IPv4/IPv6 family objects still exist. A returned Linux result hasavailable=Trueeven when individual sources are absent. Without ip, available sysfs and resolver data are still collected. Unavailable sections are None; successful empty queries return empty lists or mappings. Errors from available sources propagate, never becoming empty facts. Linux uses sysfs and iproute2 JSON. No Python dependencies or network configuration changes are needed.- async static collect(*, raw=True)[source]¶
Collect read-only observations; separate queries are not a transaction.
Without raw the branch keeps every parsed record and drops the sysfs properties, the iproute2 answers and the resolver file.
- Return type:
- static default_route(info, family)[source]¶
Choose a representative main-table exit, not a destination policy lookup.
- Return type:
- async static ip_json(executable, *arguments)[source]¶
Run a read-only ip query with a 30-second deadline and validate its JSON.
- key = 'network'¶
- static model(data)[source]¶
Build typed common records while retaining native data unchanged.
- Return type:
- static normalize(raw, *, keep=True)[source]¶
Keep sysfs/iproute2 details while exposing platform-neutral records.
Without keep the texts are used to build the records and then dropped, so the branch carries the records alone.
- Return type:
- static read_attribute(path)[source]¶
Read an optional sysfs attribute; unsupported/down-device values are None.
Sysfs can report EINVAL or EOPNOTSUPP for speed/duplex/carrier. Disappearing interfaces are normal during collection. Permission and other I/O errors propagate instead of silently looking like missing facts.
- static read_interfaces(root=PosixPath('/sys/class/net'))[source]¶
Read interface properties and statistics directly, without invoking ip.
- static resolv_conf(path=PosixPath('/etc/resolv.conf'))[source]¶
Read resolver endpoints, which may be local stubs rather than upstream DNS.
- result_type¶
alias of
NetworkInfo
- version = 1¶
- class rmote.tools.facts.collectors.network.NetworkFamily(addresses=None, routes=None)[source]¶
Bases:
object- addresses: list[NetworkAddress] | None = None¶
- routes: list[NetworkRoute] | None = None¶
- class rmote.tools.facts.collectors.network.NetworkInfo(available=False, interfaces=None, ipv4=<factory>, ipv6=<factory>, default=<factory>, dns=None, raw=<factory>)[source]¶
Bases:
object- default: NetworkDefaults¶
- interfaces: dict[str, NetworkInterface] | None = None¶
- ipv4: NetworkFamily¶
- ipv6: NetworkFamily¶
- class rmote.tools.facts.collectors.network.NetworkInterface(ifindex, mac, state, mtu, speed_bps)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.network.NetworkNextHop(interface, ifindex, gateway, weight)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.network.NetworkRoute(destination, interface, ifindex, gateway, metric, nexthops)[source]¶
Bases:
object- nexthops: list[NetworkNextHop]¶
- class rmote.tools.facts.collectors.network.NetworkdFacts[source]¶
Bases:
ToolCollect systemd-networkd’s network description through D-Bus.
Owns
networkd, independently ofNetworkFactsand its cache.busctl_availablereports the binary;availablereports a running networkd on the system bus. Without Linux, busctl, a system bus or a running daemon,statusis None. The standard system bus socket or an explicitDBUS_SYSTEM_BUS_ADDRESSis required. Collection never starts networkd.statuspreserves the JSON object returned by network1.Manager.Describe:Interfacescontains link state, configuration sources, addresses, routes, DNS/NTP and DHCP details where reported; global fields include routing policy rules. Field availability depends on systemd and interface configuration. Addresses retain networkd’s byte-array representation. Requires the Describe API (tested with systemd 255 and newer), independently of networkctl’s CLI output formats. Unsupported APIs, malformed output, permission errors and timeouts propagate without replacing cached facts.Uses two read-only busctl calls: owner discovery and one description query. Each has a 30-second limit and disables activation and interactive authorization. No root access is required. This daemon view and the kernel view in
networkare separate observations, not an atomic snapshot.- async static call(executable, signature, *arguments)[source]¶
Read a single typed D-Bus return value from busctl’s JSON envelope.
- Return type:
- async static collect()[source]¶
Describe an already running networkd, retaining its complete JSON.
- Return type:
- key = 'networkd'¶
- result_type¶
alias of
NetworkdInfo
- version = 1¶
- class rmote.tools.facts.collectors.network.NetworkdInfo(available=False, busctl_available=False, status=None)[source]¶
Bases:
object
Operating system facts, collected without external commands.
- class rmote.tools.facts.collectors.system.DistributionInfo(id=None, version_id=None, pretty_name=None)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.system.SystemFacts[source]¶
Bases:
ToolCollect hostname, OS, kernel, architecture and Linux distribution.
Owns the
systemkey. Distribution isNoneoutside Linux or when os-release is absent. Missing distribution fields areNone. No privileges or third-party packages are required.- key = 'system'¶
- static read_distribution(paths=(PosixPath('/etc/os-release'), PosixPath('/usr/lib/os-release')))[source]¶
Read current os-release data without platform’s process-lifetime cache.
The first existing file wins. Only the three published distribution fields are parsed; shell code and variable expansion are never evaluated.
- Return type:
- result_type¶
alias of
SystemInfo
- version = 1¶
- class rmote.tools.facts.collectors.system.SystemInfo(hostname, os, kernel, architecture, distribution)[source]¶
Bases:
object- distribution: DistributionInfo | None¶
Facts about the interpreter serving remote RPC.
- class rmote.tools.facts.collectors.python.PythonFacts[source]¶
Bases:
ToolCollect the remote interpreter’s version, implementation and executable.
Owns the
pythonkey; this describes the interpreter serving RPC.- static collect()[source]¶
Return typed interpreter facts.
Every value comes from the interpreter itself, so this never waits and runs on the event loop of the side that collects it.
- Return type:
- key = 'python'¶
- result_type¶
alias of
PythonInfo
- version = 1¶
- class rmote.tools.facts.collectors.python.PythonInfo(version, implementation, executable)[source]¶
Bases:
object
Read-only Debian package inventory.
- class rmote.tools.facts.collectors.apt.AptFacts[source]¶
Bases:
ToolCollect APT version and installed packages under the
aptkey.Requires both apt-get and dpkg-query. Missing binaries produce
available=Falseand None for version/packages. Each package is indexed by dpkg’s binary package name (including the architecture qualifier when applicable), retaining version, architecture and the full dpkg status. Removed/config-files-only and incompletely installed packages are excluded. Installed packages on hold are included.Queries do not refresh indexes, contact repositories or acquire a write lock. Each command has a 30-second timeout; errors from an installed utility propagate.
versionis the complete apt-get –version output.- async static collect()[source]¶
Return local package state without changing the package database.
- Return type:
- key = 'apt'¶
- static packages(output)[source]¶
Parse tab-separated dpkg records, preserving multiarch identities.
- Return type:
WSGIEnvironment[Text,AptPackage]
- version = 1¶
- class rmote.tools.facts.collectors.apt.AptInfo(available, version=None, packages=None)[source]¶
Bases:
object- packages: dict[str, AptPackage] | None = None¶
- class rmote.tools.facts.collectors.apt.AptPackage(version, architecture, status)[source]¶
Bases:
object
Read-only Arch package inventory.
- class rmote.tools.facts.collectors.pacman.PacmanFacts[source]¶
Bases:
ToolCollect pacman version and installed packages under the
pacmankey.Missing pacman produces
available=Falseand None for version/packages.packagesmaps each installed name to its version, including locally installed/foreign packages.versionis the full pacman –version output. Reads only the local database using -Q: no refresh, upgrades or network. Each command has a 30-second timeout; command errors propagate.- async static collect()[source]¶
Return the installed inventory without modifying pacman’s database.
- Return type:
- key = 'pacman'¶
- static packages(output)[source]¶
Parse pacman’s name/version inventory without altering versions.
- Return type:
WSGIEnvironment[Text,PacmanPackage]
- result_type¶
alias of
PacmanInfo
- version = 1¶
- class rmote.tools.facts.collectors.pacman.PacmanInfo(available, version=None, packages=None)[source]¶
Bases:
object- packages: dict[str, PacmanPackage] | None = None¶
Read-only systemd manager and unit facts.
- class rmote.tools.facts.collectors.systemd.ResolvedInfo(available, manager_available=False, service=None, status=None)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.systemd.SystemdCommand[source]¶
Bases:
objectShared noninteractive command and property parsing for systemd collectors.
- static properties(output)[source]¶
Parse machine-readable key=value properties, preserving empty values.
- class rmote.tools.facts.collectors.systemd.SystemdFacts[source]¶
Bases:
ToolCollect system manager properties, loaded units and installed unit files.
Owns
systemd.availablereports whether systemctl is installed;manager_availablereports the booted system manager via/run/systemd/system. When installed,versioncontains the complete systemctl version output andunit_filesmaps unit names to their installation state and preset (where provided by systemd).With a booted manager,
managercontains allsystemctl showproperties as strings, andunitsmaps names to list-units records including load, active and sub states and description. This includes services, sockets, timers, mounts and other unit types, including inactive loaded units. Installed but unloaded units remain visible throughunit_files.Unavailable sections are None, not empty successful queries. A container with systemctl but no manager can still report its installed unit files. Queries use the system manager, never start/stop units, and require systemd with JSON output for list-units/list-unit-files. Unsupported JSON, timeouts, permission and bus errors propagate. Each command has a 30-second limit.
- async static collect()[source]¶
Return the system manager and unit inventory visible to the RPC user.
- Return type:
- key = 'systemd'¶
- result_type¶
alias of
SystemdInfo
- async static systemctl(executable, *arguments)[source]¶
Run a noninteractive read-only query with a 30-second timeout.
- Return type:
- static unit_records(output, name_key)[source]¶
Index a systemctl JSON array by its unit name, retaining all fields.
- version = 1¶
- class rmote.tools.facts.collectors.systemd.SystemdInfo(available, manager_available=False, version=None, manager=None, units=None, unit_files=None)[source]¶
Bases:
object
- class rmote.tools.facts.collectors.systemd.SystemdResolvedFacts[source]¶
Bases:
ToolCollect systemd-resolved’s global and per-link DNS configuration.
Owns
systemd_resolved. Requires resolvectl and systemctl.servicereports resolved load/active/sub state. When active,statusis the JSON array fromresolvectl --json=short status, preserving global and per-link records, DNS servers, routing/search domains, default-route selection, DNSSEC, DNS-over-TLS, LLMNR and mDNS fields where reported by the daemon.Missing binaries or an unbooted manager are indicated by
availableandmanager_available; unavailable sections are None. A stopped/missing resolved is not started. Queries never resolve names or flush caches. Before systemd 259, resolvectl cannot report JSON status, sostatusis None. Errors from supported queries propagate. Privileged monitoring and statistics are not collected. Each query has a 30-second timeout.- async static collect()[source]¶
Read DNS configuration only when systemd-resolved is already active.
- Return type:
- key = 'systemd_resolved'¶
- result_type¶
alias of
ResolvedInfo
- version = 1¶
- class rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts[source]¶
Bases:
ToolCollect clock/NTP state and systemd-timesyncd properties independently.
Owns
systemd_timesync. Requires timedatectl and systemctl, checked before execution.clockistimedatectl show --all: timezone, RTC mode, whether NTP is enabled and the clock is synchronized.servicecontains timesyncd load/active/sub state;timesynccontains show-timesync properties such as configured/current servers, poll intervals, frequency and the latest NTP message. Property values remain strings, matching the machine-readable CLI interface.availablereports both binaries;manager_availablereports a booted system manager. Missing sources are None. If timesyncd is stopped, missing, or replaced by another NTP service, clock properties remain available buttimesyncis None. Collection does not start timesyncd. Errors querying an active daemon propagate. Every command has a 30-second timeout.- async static collect()[source]¶
Return clock state and, when running, timesyncd’s own observations.
- Return type:
- key = 'systemd_timesync'¶
- result_type¶
alias of
TimesyncInfo
- version = 1¶