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

cpu

architecture, usable, topology.logical

platform.machine, os.cpu_count, sysconf

memory

total_bytes, and free_bytes where the name exists

sysconf

storage

root, the space of the root filesystem

os.statvfs

system, python

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.

Choose collectors

facts.DEFAULT_COLLECTORS is the tuple of built-in collector classes used by fetch, gather and validate when collectors is omitted:

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

available

Whether the collector supports this platform; currently true only on Linux

interfaces

Mapping from interface name to ifindex, mac, state, mtu, speed_bps

ipv4.addresses, ipv6.addresses

Flat lists of interface, ifindex, address, prefixlen

ipv4.routes, ipv6.routes

Lists of destination, interface, ifindex, gateway, metric, nexthops

default.ipv4, default.ipv6

One primary exit: interface, ifindex, address, prefixlen, gateway, metric, or null

dns

Resolver endpoints: address, interface, ifindex

raw.iproute

available and native ipv4/ipv6 addresses, routes and rules from iproute2

raw.sysfs.interfaces

Interface properties and statistics read from sysfs

raw.resolv_conf

Parsed resolver endpoints from /etc/resolv.conf

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:

FactsData

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:

FactsData

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:

FactsData

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 version whenever the cached shape or meaning changes. collect runs on the target and returns the complete branch, not a patch.

static collect()[source]
Return type:

Union[TypeVar(T_co, bound= Dataclass, covariant=True), Coroutine[Any, Any, TypeVar(T_co, bound= Dataclass, covariant=True)]]

key: str
property result_type: type[T_co]
version: int
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: object

Validate collector definitions and select independent fact branches.

fetch, gather and validate construct a registry for each call. Custom collection code can also use one directly. It performs no collection, transport or cache I/O. The collectors attribute 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'))
collectors: dict[str, Collector[Any]]
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:

tuple[Text, ...]

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:

None

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: TypedDict

Optional 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.

apt: AptInfo
cpu: CpuInfo
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: object

One cache of the first processor, with the processors that share it.

kind: str
level: int
shared_with: str | None
size_bytes: int | None
class rmote.tools.facts.collectors.cpu.CpuFacts[source]

Bases: Tool

Collect the processor model, architecture, counts and mitigations.

Owns the cpu key. Model, vendor and flags come from /proc/cpuinfo, counts and topology from /sys/devices/system/cpu, and the state of each hardware mitigation from its vulnerabilities directory.

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/cpuinfo repeats 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 attribute(path)[source]

Read one sysfs attribute without its trailing newline.

Return type:

Text | None

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:

CpuInfo

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(path)[source]

Read one numeric sysfs attribute.

Return type:

getint | None

static number_of(value)[source]

Convert a /proc/cpuinfo value to an integer, or None.

Return type:

getint | None

static parse_cpuinfo(text)[source]

Split /proc/cpuinfo into one record for each processor.

Return type:

list[WSGIEnvironment[Text, Text]]

static parse_size(value)[source]

Convert a sysfs size such as 16384K into bytes.

Return type:

getint | None

static portable_topology()[source]

Report the online processors of a target without the kernel table.

SC_NPROCESSORS_ONLN counts 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:

CpuTopology | None

static read_caches(root=PosixPath('/sys/devices/system/cpu'))[source]

List the caches of the first processor, ordered by level.

Return type:

list[CpuCache] | None

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:

CpuFrequency

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_text(path)[source]

Read one kernel file, treating an absent or refused source as None.

Return type:

Text | 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/cpuinfo when it reports them. An architecture that omits them leaves those counts None instead of a guess.

Return type:

CpuTopology

static read_vulnerabilities(root=PosixPath('/sys/devices/system/cpu'))[source]

Report the state of every hardware mitigation the kernel knows.

Return type:

WSGIEnvironment[Text, Text] | None

result_type

alias of CpuInfo

static smt(root=PosixPath('/sys/devices/system/cpu'))[source]

Report whether simultaneous multithreading is active.

Return type:

bool | None

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: object

Frequency policy of the first processor, in kilohertz.

current_mhz: float | None
driver: str | None
governor: str | None
maximum_khz: int | None
minimum_khz: int | None
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: object

Processor facts of the target.

available reports whether the kernel exposes its processor table. A target without it still reports the architecture, the usable count and the online count in topology, so False means “no Linux processor table” and not “no data at all”. usable is how many processors this process may run on, which a CPU affinity or a control group can reduce below the installed count. quota is the control group limit expressed as a number of processors. It is the strictest cpu.max of the groups the target shows, and None means that none of them limits the process.

model is absent on an architecture whose /proc/cpuinfo gives no model name, for example arm64. vendor then carries the CPU implementer identifier.

Each section is None when its source is absent, which separates an unsupported target from an empty but successful read. raw keeps the text of every source that was read.

architecture: str
available: bool
bugs: list[str] | None = None
caches: list[CpuCache] | None = None
family: str | None = None
flags: list[str] | None = None
frequency: CpuFrequency | None = None
microcode: str | None = None
model: str | None = None
quota: float | None = None
raw: dict[str, Any]
stepping: str | None = None
topology: CpuTopology | None = None
usable: int | None = None
vendor: str | None = None
vulnerabilities: dict[str, str] | 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: object

How the online processors are arranged.

logical counts the online processors. The range strings keep the kernel’s own notation, so a sparse set stays readable.

cores_per_socket: int | None
logical: int | None
numa_nodes: int | None
offline: str | None
online: str | None
possible: str | None
present: str | None
smt: bool | None
sockets: int | None
threads_per_core: int | None

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: object

Huge page pool of the default size.

free: int | None
reserved: int | None
size_bytes: int | None
total: int | None
class rmote.tools.facts.collectors.memory.MemoryFacts[source]

Bases: Tool

Collect installed, free and available memory, swap and huge pages.

Owns the memory key. /proc/meminfo reports its values in kibibytes, and every field here is bytes. used_bytes is 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/meminfo is kept in raw, so a field this model does not name is still available.

A target without /proc/meminfo reports what sysconf gives: 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:

MemoryInfo

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_PAGES counts the pages the kernel manages, which is what MemTotal reports on Linux. SC_AVPHYS_PAGES counts the pages that are free now; macOS has no such name and gives None.

Return type:

MemoryInfo

static read_hugepages(values)[source]

Report the huge page pool, or None when the target has none.

Return type:

HugePages | None

static read_swap(values)[source]

Report swap totals, or None when the target has no swap table.

Return type:

SwapMemory | None

static read_text(path=PosixPath('/proc/meminfo'))[source]

Read the memory table, treating an absent source as None.

Return type:

Text | 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: object

Memory facts of the target, in bytes.

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 is larger than free_bytes because reclaimable caches count towards it.

limit_bytes and usage_bytes come from the control group, so a container reports what it may use next to what the machine has. limit_bytes is the strictest memory.max of the groups the target shows, and None means that none of them limits the process. usage_bytes is the memory.current of the process’s own group, so it never counts the processes of a parent group.

available reports whether /proc/meminfo was read. A target without it still reports total_bytes, and free_bytes when its sysconf names the free pages, so False means “no Linux memory table” and not “no data at all”.

available: bool
available_bytes: int | None = None
buffers_bytes: int | None = None
cached_bytes: int | None = None
commit_limit_bytes: int | None = None
committed_bytes: int | None = None
free_bytes: int | None = None
hugepages: HugePages | None = None
limit_bytes: int | None = None
raw: dict[str, Any]
shared_bytes: int | None = None
swap: SwapMemory | None = None
total_bytes: int | None = None
usage_bytes: int | None = None
used_bytes: int | None = None
class rmote.tools.facts.collectors.memory.SwapMemory(total_bytes, free_bytes, used_bytes, cached_bytes)[source]

Bases: object

Swap totals, in bytes.

cached_bytes: int | None
free_bytes: int
total_bytes: int
used_bytes: int

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: object

One whole block device from sysfs, with the sizes of its partitions.

logical_block_size: int | None
model: str | None
name: str
partitions: dict[str, int | None]
removable: bool | None
rotational: bool | None
size_bytes: int | None
class rmote.tools.facts.collectors.storage.FilesystemUsage(total_bytes, free_bytes, available_bytes, total_inodes, free_inodes, available_inodes)[source]

Bases: object

Space and inodes of one mounted filesystem, in bytes and counts.

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.

available_bytes: int
available_inodes: int | None
free_bytes: int
free_inodes: int | None
total_bytes: int
total_inodes: int | None
class rmote.tools.facts.collectors.storage.MountPoint(target, source, fstype, options, super_options, device, root, read_only, pseudo, usage)[source]

Bases: object

One mounted filesystem, as the kernel lists it.

usage is None for a pseudo filesystem, which is not queried, or when os.statvfs raises 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.

device: str | None
fstype: str
options: list[str]
pseudo: bool
read_only: bool
root: str
source: str
super_options: list[str]
target: str
usage: FilesystemUsage | None
class rmote.tools.facts.collectors.storage.StorageFacts[source]

Bases: Tool

Collect mounted filesystems, their space, block devices and swap.

Owns the storage key. available reports whether the mount table of the kernel could be read; the space of the root filesystem is reported on any POSIX target, because os.statvfs is 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/mounts is the fallback. Space comes from os.statvfs for each mount, devices from /sys/block and swap from /proc/swaps.

Every mount stays in the result, including a pseudo filesystem, which carries pseudo so 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.statvfs raises OSError retains its record with usage=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:

StorageInfo

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.

Return type:

list[WSGIEnvironment[Text, Any]]

static parse_mounts(text)[source]

Parse the older /proc/mounts, which has no device number or root.

Return type:

list[WSGIEnvironment[Text, Any]]

static read_attribute(path)[source]

Read one sysfs attribute, or None when it is absent or refused.

Return type:

Text | None

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_flag(path)[source]

Read one boolean sysfs attribute written as 0 or 1.

Return type:

bool | None

static read_fstab(path=PosixPath('/etc/fstab'))[source]

Read the configured filesystem table, without interpreting it.

Return type:

tuple[list[WSGIEnvironment[Text, Text]] | None, Text | None]

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.

Return type:

tuple[list[MountPoint] | None, WSGIEnvironment[Text, Any]]

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.

Return type:

tuple[list[SwapArea] | None, Text | None]

static read_text(path)[source]

Read one kernel file, treating an absent or closed source as None.

Return type:

Text | None

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:

Text

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:

FilesystemUsage | None

version = 1
class rmote.tools.facts.collectors.storage.StorageInfo(available, root=None, mounts=None, devices=None, swaps=None, fstab=None, raw=<factory>)[source]

Bases: object

Storage of the target: mounts with their space, devices and swap.

available reports 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. raw keeps the text of every source that was read, by source name.

root is the space of the root filesystem, which every POSIX target reports. It is filled whatever available says, so a target without the mount table of Linux still reports its space. On Linux the same numbers are the usage of the / mount.

available: bool
devices: dict[str, BlockDevice] | None = None
fstab: list[dict[str, str]] | None = None
mounts: list[MountPoint] | None = None
raw: dict[str, Any]
root: FilesystemUsage | None = None
swaps: list[SwapArea] | None = None
class rmote.tools.facts.collectors.storage.SwapArea(path, kind, size_bytes, used_bytes, priority)[source]

Bases: object

One active swap area. Sizes come from /proc/swaps, in bytes.

kind: str
path: str
priority: int
size_bytes: int
used_bytes: int

Platform-neutral network models collected from Linux sources, plus networkd.

class rmote.tools.facts.collectors.network.DNSServer(address, interface, ifindex)[source]

Bases: object

address: str
ifindex: int | None
interface: str | None
class rmote.tools.facts.collectors.network.DefaultRoute(interface, ifindex, address, prefixlen, gateway, metric)[source]

Bases: object

address: str | None
gateway: str | None
ifindex: int
interface: str
metric: int
prefixlen: int | None
class rmote.tools.facts.collectors.network.NetworkAddress(interface, ifindex, address, prefixlen)[source]

Bases: object

address: str
ifindex: int
interface: str
prefixlen: int
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: Tool

Collect Linux network state in a JSON-compatible schema.

Owns network, schema version 1. interfaces maps names to ifindex, mac (lowercase colon-separated), state, mtu (bytes) and speed_bps (transmit link speed). Missing properties are None. ipv4/ipv6 contain flat addresses and routes arrays:

  • Address: interface, ifindex, address, prefixlen.

  • Route: destination (CIDR), interface, ifindex, gateway (None for on-link), metric and nexthops. Multipath routes retain each next hop’s interface, ifindex, gateway and weight; ordinary routes have an empty next-hop array. Metric is the route metric.

  • dns: a list of address, interface and ifindex records, read from /etc/resolv.conf, which can be a local stub with no interface.

raw.sysfs.interfaces preserves interface properties and statistics. raw.iproute.ipv4/ipv6 preserve addresses, routes from all tables and policy rules; raw.iproute.available reports the ip command. raw.resolv_conf retains the parsed resolver endpoints. Common routes alone are insufficient to reproduce routing policy. networkd remains an independent collector.

Collection is implemented only on Linux. Other platforms return NetworkInfo(available=False) with None sections, empty raw and no default exits; the IPv4/IPv6 family objects still exist. A returned Linux result has available=True even 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:

NetworkInfo

static default_route(info, family)[source]

Choose a representative main-table exit, not a destination policy lookup.

Return type:

DefaultRoute | None

static empty_data()[source]

Return the same shape even when a source is unavailable.

Return type:

WSGIEnvironment[Text, Any]

static interface(index)[source]

Unknown properties stay null, not zero or an invented down state.

Return type:

WSGIEnvironment[Text, Any]

async static ip_json(executable, *arguments)[source]

Run a read-only ip query with a 30-second deadline and validate its JSON.

Return type:

list[WSGIEnvironment[Text, Any]]

key = 'network'
static model(data)[source]

Build typed common records while retaining native data unchanged.

Return type:

NetworkInfo

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:

NetworkInfo

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.

Return type:

Text | None

static read_interfaces(root=PosixPath('/sys/class/net'))[source]

Read interface properties and statistics directly, without invoking ip.

Return type:

WSGIEnvironment[Text, Any] | None

static resolv_conf(path=PosixPath('/etc/resolv.conf'))[source]

Read resolver endpoints, which may be local stubs rather than upstream DNS.

Return type:

list[WSGIEnvironment[Text, Any]] | None

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

available: bool = False
default: NetworkDefaults
dns: list[DNSServer] | None = None
interfaces: dict[str, NetworkInterface] | None = None
ipv4: NetworkFamily
ipv6: NetworkFamily
raw: dict[str, Any]
class rmote.tools.facts.collectors.network.NetworkInterface(ifindex, mac, state, mtu, speed_bps)[source]

Bases: object

ifindex: int
mac: str | None
mtu: int | None
speed_bps: int | None
state: str | None
class rmote.tools.facts.collectors.network.NetworkNextHop(interface, ifindex, gateway, weight)[source]

Bases: object

gateway: str | None
ifindex: int | None
interface: str | None
weight: int | None
class rmote.tools.facts.collectors.network.NetworkRoute(destination, interface, ifindex, gateway, metric, nexthops)[source]

Bases: object

destination: str
gateway: str | None
ifindex: int | None
interface: str | None
metric: int
nexthops: list[NetworkNextHop]
class rmote.tools.facts.collectors.network.NetworkdFacts[source]

Bases: Tool

Collect systemd-networkd’s network description through D-Bus.

Owns networkd, independently of NetworkFacts and its cache. busctl_available reports the binary; available reports a running networkd on the system bus. Without Linux, busctl, a system bus or a running daemon, status is None. The standard system bus socket or an explicit DBUS_SYSTEM_BUS_ADDRESS is required. Collection never starts networkd.

status preserves the JSON object returned by network1.Manager.Describe: Interfaces contains 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 network are 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:

Any

async static collect()[source]

Describe an already running networkd, retaining its complete JSON.

Return type:

NetworkdInfo

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

available: bool = False
busctl_available: bool = False
status: dict[str, Any] | None = None

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

id: str | None = None
pretty_name: str | None = None
version_id: str | None = None
class rmote.tools.facts.collectors.system.SystemFacts[source]

Bases: Tool

Collect hostname, OS, kernel, architecture and Linux distribution.

Owns the system key. Distribution is None outside Linux or when os-release is absent. Missing distribution fields are None. No privileges or third-party packages are required.

static collect()[source]

Return facts from the machine executing this method.

Return type:

SystemInfo

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:

DistributionInfo | None

result_type

alias of SystemInfo

version = 1
class rmote.tools.facts.collectors.system.SystemInfo(hostname, os, kernel, architecture, distribution)[source]

Bases: object

architecture: str
distribution: DistributionInfo | None
hostname: str
kernel: str
os: str

Facts about the interpreter serving remote RPC.

class rmote.tools.facts.collectors.python.PythonFacts[source]

Bases: Tool

Collect the remote interpreter’s version, implementation and executable.

Owns the python key; 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:

PythonInfo

key = 'python'
result_type

alias of PythonInfo

version = 1
class rmote.tools.facts.collectors.python.PythonInfo(version, implementation, executable)[source]

Bases: object

executable: str
implementation: str
version: str

Read-only Debian package inventory.

class rmote.tools.facts.collectors.apt.AptFacts[source]

Bases: Tool

Collect APT version and installed packages under the apt key.

Requires both apt-get and dpkg-query. Missing binaries produce available=False and 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. version is the complete apt-get –version output.

async static collect()[source]

Return local package state without changing the package database.

Return type:

AptInfo

key = 'apt'
static packages(output)[source]

Parse tab-separated dpkg records, preserving multiarch identities.

Return type:

WSGIEnvironment[Text, AptPackage]

result_type

alias of AptInfo

version = 1
class rmote.tools.facts.collectors.apt.AptInfo(available, version=None, packages=None)[source]

Bases: object

available: bool
packages: dict[str, AptPackage] | None = None
version: str | None = None
class rmote.tools.facts.collectors.apt.AptPackage(version, architecture, status)[source]

Bases: object

architecture: str
status: str
version: str

Read-only Arch package inventory.

class rmote.tools.facts.collectors.pacman.PacmanFacts[source]

Bases: Tool

Collect pacman version and installed packages under the pacman key.

Missing pacman produces available=False and None for version/packages. packages maps each installed name to its version, including locally installed/foreign packages. version is 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:

PacmanInfo

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

available: bool
packages: dict[str, PacmanPackage] | None = None
version: str | None = None
class rmote.tools.facts.collectors.pacman.PacmanPackage(version)[source]

Bases: object

version: str

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

available: bool
manager_available: bool = False
service: dict[str, str] | None = None
status: list[dict[str, Any]] | None = None
class rmote.tools.facts.collectors.systemd.SystemdCommand[source]

Bases: object

Shared noninteractive command and property parsing for systemd collectors.

static properties(output)[source]

Parse machine-readable key=value properties, preserving empty values.

Return type:

WSGIEnvironment[Text, Text]

async static run(executable, *arguments)[source]

Run a query with a 30-second timeout and stable locale.

Return type:

Text

async static service(executable, unit)[source]

Inspect a daemon without starting it or prompting for authentication.

Return type:

WSGIEnvironment[Text, Text]

class rmote.tools.facts.collectors.systemd.SystemdFacts[source]

Bases: Tool

Collect system manager properties, loaded units and installed unit files.

Owns systemd. available reports whether systemctl is installed; manager_available reports the booted system manager via /run/systemd/system. When installed, version contains the complete systemctl version output and unit_files maps unit names to their installation state and preset (where provided by systemd).

With a booted manager, manager contains all systemctl show properties as strings, and units maps 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 through unit_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:

SystemdInfo

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:

Text

static unit_records(output, name_key)[source]

Index a systemctl JSON array by its unit name, retaining all fields.

Return type:

WSGIEnvironment[Text, WSGIEnvironment[Text, Any]]

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

available: bool
manager: dict[str, str] | None = None
manager_available: bool = False
unit_files: dict[str, dict[str, Any]] | None = None
units: dict[str, dict[str, Any]] | None = None
version: str | None = None
class rmote.tools.facts.collectors.systemd.SystemdResolvedFacts[source]

Bases: Tool

Collect systemd-resolved’s global and per-link DNS configuration.

Owns systemd_resolved. Requires resolvectl and systemctl. service reports resolved load/active/sub state. When active, status is the JSON array from resolvectl --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 available and manager_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, so status is 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:

ResolvedInfo

key = 'systemd_resolved'
result_type

alias of ResolvedInfo

version = 1
class rmote.tools.facts.collectors.systemd.SystemdTimesyncFacts[source]

Bases: Tool

Collect clock/NTP state and systemd-timesyncd properties independently.

Owns systemd_timesync. Requires timedatectl and systemctl, checked before execution. clock is timedatectl show --all: timezone, RTC mode, whether NTP is enabled and the clock is synchronized. service contains timesyncd load/active/sub state; timesync contains 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.

available reports both binaries; manager_available reports a booted system manager. Missing sources are None. If timesyncd is stopped, missing, or replaced by another NTP service, clock properties remain available but timesync is 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:

TimesyncInfo

key = 'systemd_timesync'
result_type

alias of TimesyncInfo

version = 1
class rmote.tools.facts.collectors.systemd.TimesyncInfo(available, manager_available=False, clock=None, service=None, timesync=None)[source]

Bases: object

available: bool
clock: dict[str, str] | None = None
manager_available: bool = False
service: dict[str, str] | None = None
timesync: dict[str, str] | None = None