Skip to content

About

Network reliability toolkit: TLS expiry monitoring, capacity forecasting, and change-validation diffs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

14 Commits

Folders and files

Repository files navigation

relnet-toolkit

A network reliability toolkit: TLS expiry monitoring, capacity forecasting, and change-validation diffs.

CI Ruff mypy: strict Python License: MIT

30-second pitch

Three reliability problems show up in nearly every network team's backlog: certs that expire on a Friday afternoon, links that hit 100% utilization weeks before anyone notices the slope, and config changes that quietly break things downstream. relnet runs all three checks on a schedule against your gear, persists every observation to a single SQLite file, and fires state-transition alerts to stdout or Slack only when something actually changes. Built for SRE and netops teams who want one auditable tool with no external services, no message bus, and no agents.

Quick demo

$ relnet init
Wrote starter config to /home/op/relnet/config.yaml.
Initialised database at /home/op/relnet/relnet.db.

$ relnet check tls
tls: ok

$ relnet forecast --device edge1 --interface Gi0/1
edge1/Gi0/1: ~18.3 days to 85% (slope 1.92%/day)

$ relnet alerts --active
2026-05-28T08:14:02+00:00  critical  tls/expired.badssl.com:443  expired.badssl.com:443 expires in -1290 days. [active]
2026-05-28T11:02:17+00:00  warn      capacity/edge1/Gi0/1        edge1/Gi0/1 utilization in=78.4% out=72.1% (warn). [active]

Architecture overview

                    +-------------------+
                    |     relnet CLI    |  (click; one-shot + daemon entry points)
                    +---------+---------+
                              |
            +-----------------+-----------------+
            |                 |                 |
    +-------v------+  +-------v------+  +-------v-------+
    | TLS collector|  | Capacity     |  | Change        |
    | (ssl/socket) |  | collector    |  | collector     |
    |              |  | (snmp/mock)  |  | (netmiko/mock)|
    +-------+------+  +------+-------+  +-------+-------+
            |                |                  |
            |  +-------------+--------------+   |
            |  |  forecasting/linear.py     |   |  (numpy polyfit, used by capacity)
            |  +----------------------------+   |
            v                v                  v
        +---+----------------+------------------+---+
        |        core/alert_dispatcher.py            |  (state-transition gate)
        +---------+-------------------------+--------+
                  |                         |
          +-------v------+         +--------v------+
          | alerters/    |         | alerters/     |
          | stdout.py    |         | slack.py      |
          +--------------+         +---------------+
                  |
                  v
        +---------+----------------------+
        |  core/repository.py  (SQLite)  |
        +--------------------------------+
        |  scheduler.py  +  metrics.py   |
        +--------------------------------+

core/ holds the framework primitives: config loader, JSON logger, SQLite schema and connection helper, the typed repository, the state-transition alert dispatcher, the scheduler, and the in-memory metrics registry. collectors/ holds the three collectors, each backed by a pluggable source layer (mock or real). alerters/ holds the two shipped backends. forecasting/ holds the linear-fit saturation predictor used by the capacity collector. cli.py wires everything together and is the only module that calls click.echo or print directly.

Installation & quickstart

git clone https://github.com/RajParmar2003/relnet-toolkit.git
cd relnet-toolkit
pip install -e ".[dev]"
relnet init
relnet daemon

That's it. The default config runs end-to-end with mock data: no real network or hardware required. The capacity collector generates seeded random-walk counters, the change collector reads bundled cisco_ios fixtures, and the TLS collector hits the three example hosts in config.example.yaml (one of which is expired.badssl.com, included so the first run produces a recognisable critical alert).

Drop --editable if you only need to run the tool. Use Python 3.11 or 3.12.

Configuration

The full annotated example lives in config.example.yaml. The key sections:

database_path: ./relnet.db

log:
  level: INFO         # DEBUG | INFO | WARNING | ERROR
  # file: ./relnet.log

collectors:
  tls:
    enabled: true
    interval_seconds: 21600
    warn_days: 30
    critical_days: 7
    targets:
      - {host: example.com, port: 443}

  capacity:
    enabled: true
    interval_seconds: 300
    warn_pct_immediate: 75.0
    critical_pct_immediate: 90.0
    warn_days_predicted: 21
    critical_days_predicted: 7
    predicted_target_pct: 85.0
    devices:
      - name: edge1
        source: mock
        interfaces: [Gi0/1, Gi0/2]

  change:
    enabled: true
    interval_seconds: 14400
    warn_lines_threshold: 20
    devices:
      - name: edge1
        device_type: cisco_ios
        source: mock
        fixture_dir: ./tests/fixtures/cisco_ios

alerters:
  stdout:
    enabled: true
  # slack:
  #   enabled: true
  #   webhook_url: ${SLACK_WEBHOOK_URL}

Environment variables. Secrets are kept out of YAML: ${VAR} references are resolved against os.environ at config-load time, and a missing required variable raises a clear error before any collector runs. The shipped variables you may need:

  • SLACK_WEBHOOK_URL: webhook for the Slack alerter.
  • SSH credentials and SNMP communities flow through device-specific config blocks when those sources are wired in; the default configuration uses local mocks throughout.

Following twelve-factor, configuration that varies between environments (URLs, credentials) belongs in the environment, not the YAML.

CLI reference

Command One-line Example
relnet init Create the SQLite DB and config.yaml from the example. Idempotent. relnet init
relnet daemon Run the scheduling loop foreground until SIGTERM/SIGINT. relnet daemon --config /etc/relnet/config.yaml
relnet check tls One-shot TLS collector run. relnet check tls
relnet check capacity One-shot capacity collector run. relnet check capacity
relnet check change One-shot change collector run, auto-labeled. relnet check change
relnet snapshot --label NAME Change collector run with an operator-supplied label. relnet snapshot --label pre-change-2026-05-28-1430
relnet status Table of collectors with last_run_at, next_run_at, last_status. relnet status
relnet alerts [--active] [--severity LEVEL] List recent alerts. relnet alerts --active --severity critical
relnet diff LABEL-A LABEL-B [--device NAME] Diff two stored snapshots by label. relnet diff pre-change-... post-change-...
relnet forecast [--device NAME] [--interface NAME] Current saturation predictions for configured interfaces. relnet forecast --device edge1
relnet metrics Internal tool metrics as a single JSON object on stdout. relnet metrics | jq .

Every command accepts --config <path> to override ./config.yaml.

Design decisions

The five tradeoffs most worth flagging up front. The long-form rationale lives in docs/architecture.md.

SQLite over Postgres. A single embedded file is enough for one operator team's worth of data, removes the operational burden of a database server, and makes the tool deployable as a single binary plus a config file. The repository pattern keeps the swap to Postgres a config change, not a rewrite.

Sleep-loop scheduler over APScheduler / Celery / cron. State lives in the collector_state table, not in scheduler memory, so the daemon is restart-safe by construction. Adding a dependency on a job queue would buy us multi-process scheduling we don't need and an in-memory state model we'd then have to reconcile with the table.

State-transition alerts, not threshold pings. Every observation produces a current-state event handed to the dispatcher; the dispatcher only emits when the state for (collector, target) actually changes. This makes on-call paging quiet by default and makes resolution events explicit (severity: ok) so the operator knows when something self-healed.

Stdlib logging with a JSON formatter, not loguru/structlog. A single 60-line formatter beats taking on a dependency for the same outcome. Every log line is one JSON object with ts, level, logger, msg, plus any structured extra fields, which composes well with jq, journald, and standard ingestion pipelines.

Linear forecasting only in v1. numpy.polyfit(deg=1) over a configurable lookback window is enough to catch the weekly-and-monthly slopes that drive capacity planning; Holt-Winters and ARIMA come back in proportion to the variance you can't explain with a line. Future work is to add weighted/seasonal predictors behind the same Forecast | None interface.

Future work

  • Weighted and seasonal forecasting (Holt-Winters, ARIMA, Prophet) behind the existing forecast_time_to_threshold contract.
  • Structured per-command parsers for the change collector (e.g., BGP-neighbor state from show ip bgp summary) so diffs can be classified semantically, not by line count.
  • Additional alerter backends: PagerDuty, Opsgenie, generic webhook with templated payload.
  • Postgres backend behind the repository abstraction for teams running multi-node operators or wanting OLAP-style queries.
  • Per-host config overrides (group inheritance, profile composition) for fleets where every device has bespoke commands or thresholds.
  • Prometheus /metrics HTTP endpoint exposing the same in-memory counters relnet metrics returns today.
  • Distributed multi-instance coordination (leader election, leased ticks) for high-availability deployments.

License & author

Released under the MIT License.

Author: Rajwinder Parmar (@RajParmar2003), a network automation engineer with a Verizon NMC background and an MBA in MIS & AI in progress; building an AI governance platform on the side.

About

Network reliability toolkit: TLS expiry monitoring, capacity forecasting, and change-validation diffs.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages