Repository navigation
Conversation
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/config/structured.py
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/config/structured.py
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/config/structured.py
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/config/structured.py
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/constants.py # hyperglass/models/config/structured.py
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/config/structured.py # hyperglass/models/parsing/mikrotik.py # hyperglass/plugins/_builtin/__init__.py # hyperglass/ui/components/table/main.tsx # hyperglass/ui/types/globals.d.ts
# Conflicts: # docs/pages/configuration/config/structured-output.mdx # hyperglass/models/data/bgp_route.py
# Conflicts: # docs/pages/configuration/config/web-ui.mdx
RouterOS 7.21 emits BGP route detail differently over the looking-glass SSH driver, which broke the structured parser: - Active/best routes learned from a commented peer put the flag column and the ";;; comment" on a line of their own (afi=ip wraps to the next line), so the "A" flag was lost and no best route was highlighted. Derive active/filtered from the always-present "contribution=" field instead, falling back to the flag column for ROS v6. - Long ".communities" lists wrap across terminal-width lines; everything after the wrap was silently dropped. De-wrap comma-separated lists before tokenizing. - The garbage cleaner stripped any legend-looking line without "=", which also ate commented route flag lines. Match the "<char> - " legend shape instead. - Parse the v7 dotted ".ext-communities" key. Also restore the empty-BGP-table retry (4 attempts, 10s delay, MikroTik BGP queries only) that re-runs when RouterOS answers before its routing table has finished assembling. Test fixtures use RFC 5737 (IPv4) documentation addresses and RFC 5398 example ASNs only; no real network data.
Adds a `logging.samples` config block that, when enabled, appends one JSON object per query to a JSONL file capturing the request, the raw device output as hyperglass received it, and the parsed/structured result — including failures (empty responses, timeouts, parse errors) with the error and runtime. Motivation: parser issues (e.g. RouterOS version differences) were hard to debug because we had no record of the exact device output the app received. The JSONL stream is intended to be tailed by a log shipper (Grafana Alloy) into Loki, and to build a corpus of real raw+parsed samples for parser regression fixtures. - models/config/logging.py: QuerySamples model (enable, path, include_raw, include_parsed, max_size) wired into Logging.samples; disabled by default. - util/query_samples.py: best-effort JSONL writer with size-based rotation and raw/parsed normalization. Writing never affects the query. - execution/main.py: capture at the point where request, raw output and parsed result coexist, on both success and failure; driven per-query from params so it is multi-worker safe and near-zero cost when disabled. - tests: disabled-noop, success, failure + include flags, custom path/append, rotation. Default output: <logging.directory>/query_samples.jsonl. Note raw output can contain internal addresses/communities/interface names.
…mpty reads MikroTik can take a couple seconds to start emitting output for larger queries (the command executes before any bytes return). netmiko send_command_timing's default last_read (2.0s) elapses during that initial silence and returns an empty string, so hyperglass parses 0 routes even though the device would return a full table. It is latency-sensitive: it shows up for devices tens of ms away (e.g. a Cape Town router queried from a Johannesburg node) while nearby devices squeak through, and the empty-table retry cannot help since every attempt hits the same window. Set last_read=5.0 for mikrotik_routeros/switchos so the read waits for output to begin. Empirically last_read>=4.0 reliably returns the full output where 2.0 returns empty; trade-off is ~5s added per MikroTik query (last_read is also the trailing quiet-wait). send_command (prompt-based) was tried and rejected: RouterOS prompt detection truncates output through the driver, which is why send_command_timing is used.
All runtime configuration (params, directives, devices, ui_params) and the
plugin registry were written to Redis in exactly one place, run() at
startup, and never rewritten. Redis is deliberately cache-only, so anything
that emptied it while hyperglass was running left the process with no state
and no way back: every query raised
StateError: 'devices' ('hyperglass.state.devices') does not exist
until hyperglass was restarted. A replaced Redis container does exactly
this, and `docker compose up -d` recreates Redis on its own whenever that
image changes, without touching hyperglass. The retry in api/state.py
caught the error but nothing repopulated state, so it only turned a fast
failure into a 2.5s slow one.
Rebuild in place instead. rebuild_state() re-runs init_user_config() and
register_all_plugins() under a Redis SET NX EX lock, so one process does the
work and the others wait for it rather than all re-reading and re-validating
the configuration, with a cooldown so a configuration that cannot be loaded
is not re-validated on every request. It deliberately does not call
state.clear(): flushing is correct once at startup, but mid-flight it would
wipe state other workers are using.
Two things this got wrong on the first attempt, both found by testing
against a real deployment rather than a fixture:
The recovery belongs on the HyperglassState properties, not on use_state().
The request path does not go through use_state: models/api/query.py reads
self._state.devices and the query route reads _state.params straight off a
state instance. use_state also memoizes, so a worker that has already read
devices never asks Redis again, which keeps /api/devices returning 200 with
the store empty and hides the failure.
The plugin registry lives in the same store and init_user_config() does not
write it, so recovering configuration alone brought hyperglass back with no
output plugins: queries returned raw text instead of structured results,
silently. register_all_plugins() moves from main.py into hyperglass.plugins
so recovery can call it, and _wait_for_rebuild waits for the lock to be
released as well as the keys to appear, because plugins are registered after
the configuration is written.
Also here, since they share a cause: clear() now scans and deletes only
hyperglass.state.* instead of flushing the database, which was deleting keys
belonging to anything else sharing it; init_user_config() writes a
generation UUID last, so its presence means state is complete and a rebuilt
store is distinguishable from the original in the logs; and the compose
healthcheck points at /api/info, which reads state, rather than the UI root,
which does not and so reported healthy through a total query outage.
Verified against a live deployment: destroy the Redis container and its data
volume, leave hyperglass running, and a BGP route query still returns
structured output with the store repopulated.
`docker compose build` failed in the UI stage. The line npm --prefix /usr/lib/node_modules/npm install glob@11.1.0 installs into npm's own directory, which makes npm reconcile npm's package.json. That file lists @npmcli/docs as a devDependency, and it is an unpublished workspace stub, so the install ends in a 404 and the stage exits 1. --omit=dev does not avoid it. The patch is also pointless now. It was added to lift glob past a CVE, but the npm that Alpine ships already bundles glob 13.x, so the line would have downgraded it. It targets /usr/lib/node_modules/npm (Alpine's copy) while the npm on PATH after this RUN is the one under /usr/local, so it was not patching the npm actually in use either.
The version was declared in three places and all three disagreed: constants.py said 2.0.4 (upstream's last release), pyproject.toml and ui/package.json said 2.1.2, and the install docs told you to check out v2.1.0. None of them described what this fork actually is. Set all of them to 3.0.0 and record the upstream base separately as __upstream_version__, alongside __upstream_url__ and __fork_url__. The fork's version line is its own and is not comparable to upstream's; the major bump reflects the raised Python floor (3.13), the removal of hyperglass.external.bgptools, and changed default directive commands for FRRouting, Huawei and MikroTik. version.py (rye run upgrade) matches only __version__, so it will not touch the new upstream constants. It does not know about manual.mdx or those constants, which stay manual. manual.mdx also cloned thatmattlove/hyperglass and then asked you to switch to a v3.0.0 tag, which will only ever exist on the fork. It now clones the fork.
The footer credit read "Powered by hyperglass version X", which gave a version number with no indication of whose it was. Now that the fork has its own version line, that is actively misleading: 3.0.0 is not an upstream release. The credit now names the fork, its version, the upstream version it is based on, and links to both repositories. Upstream's authorship and the BSD 3-Clause Clear licence line are unchanged. No frontend code changed. The credit was already built server-side from the CREDIT template and delivered through ui_params, so the strings stay in constants.py rather than moving into a component.
The changelog still described upstream's releases and carried an [Unreleased] section of upstream issues, so nothing in it reflected the work done in this fork since 2.0.4. Rewrite the header to say plainly that this file documents the fork, and add a 3.0.0 section covering the changes made on top of upstream: structured BGP output for MikroTik and Huawei, structured traceroute for FRRouting, Huawei and MikroTik, IP and ASN enrichment, the AS-path enrichment endpoint and tooltips, pluggable RPKI backends, community names, filtered-route display, query-sample logging, the worker count fix, error sanitization, the dependency modernization, and the state, clear() and build fixes from this series. Upstream's 2.0.4 and earlier sections are kept verbatim below as inherited history. The ten entries from [Unreleased] were each checked against the tree rather than copied: all are present, because the fork's base is upstream main rather than the 2.0.4 tag, so they sit in an "Inherited from upstream" block with their original contributor credits. thatmattlove#292 is marked superseded, since the MikroTik BGP directives were rewritten onto RouterOS 7 syntax that keeps supernet matching by other means. Two branches are deliberately absent because they are not merged: experimental/query-sample-logging and experimental/arista-juniper-traceroute.
Merge the 3.1.0 security, dependency, Docker, documentation and UI platform release. Validation completed with 70 Python tests, 39 UI tests, a 33-route docs export, a successful Docker build, a clean HIGH/CRITICAL Trivy scan, and a live /mnt/AppData HTTP 200 check.
* fix(security): patch runtime dependencies and image tooling * wip(docs): checkpoint Nextra 4 migration * fix(docs): patch Nextra layout schema * fix(ui): restore strict types and deterministic tests * release: prepare hyperglass 3.1.0 * ci: test and publish Docker releases * ci: create application directory before tests
Dockerfile and compose.yaml both carried HYPEGLASS_DISABLE_UI, missing the R. Settings reads the HYPERGLASS_ prefix, so nothing ever read either one: the Dockerfile's intent never took effect and the compose knob was inert. Set to false rather than repairing the spelling in place. The base stage is inherited by ui and by the final stage, so a literal fix to HYPERGLASS_DISABLE_UI=true would have disabled the UI in every published image. false matches what compose.yaml already defaults to.
* fix: make packaging portable across supported hosts * fix: make bundled UI dependencies available at runtime * fix: skip pnpm install for bundled dependencies * fix: retry incomplete UI builds * fix: include frontend build dependencies in image
* fix: normalize CIDR targets before validation (#8) * test: isolate query construction state * test: preserve parser state after construction tests
* fix: add asynchronous IP enrichment * fix: make async enrichment cache updates generation-safe * test: correct async enrichment coverage * refactor: isolate async enrichment eligibility
Author
|
Closing this accidental upstream PR; the change belongs in WilhelmZA/ultraglass. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Verification
Fixes #15