QuantumBNG 1.4.0 — Release Notes
This is the first generally available release. Everything before it was
alpha, and these notes describe the product as it ships rather than as a
delta between alpha builds. Where a change matters because the alpha behaved
differently, that is called out against "the alpha" as a whole — individual
pre-GA version numbers are not carried forward.
Covers the QuantumTouch BNG appliance and console together with the Quantum
BNG controller. Controller-layer items are marked [controller];
everything else is the QuantumTouch console, installer and evaluation
appliance.
Overview
The box becomes the system of record, and the console operates it.
The alpha staged two capabilities it could not use: capacity licensing, with a
page that displayed a licence but could not activate one, and usage records
that were held but never produced from live traffic. Both are complete in GA.
Three further capabilities arrive with them. Local AAA becomes the
authoritative subscriber store rather than a mirror of controller
configuration, with enforcement overlays that converge on every reconcile pass.
Data-cap enforcement is delivered end to end, from the policy an operator
sets to the rate applied on the wire. And accounting no longer depends on the
browser: a background service drains the controller's spool into a durable
local ledger every 30 seconds, where records were previously drained only while
a page was open.
The console also adds what an operator needs when something is wrong — a
single-action support bundle that opens with a written summary, and health
reporting that states how often the dataplane connection has dropped rather than
only whether it is currently up.
Component versions
What GA ships. Pre-GA component versions are not listed — the alpha is
treated as a single prior state rather than a series to diff against.
| Component | Version |
|---|---|
| Quantum BNG controller | v1.4.0 |
| quantum_core | v1.6.0 |
| quantumtouch-bng (console plugin) | 0.10.37 |
| quantumtouch-core | v0.10.24 |
| CPESIM plugin | 0.4.18 |
| VPP | 25.10.0-23 (v25.10.0-23~g744d3c715) |
| Installer bundle | 1.4.0-14 |
Requirements
- Ubuntu 24.04 (Noble), x86_64, kernel 6.x. Ubuntu 24.04.4 ships
6.8, which is the kernel this release is validated on; installer preflight
warns below 6.14 because a few DPDK plugins fall back there. If that
fallback matters for your NIC,linux-generic-hwe-24.04provides 6.14 —
it is not required. - TPM 2.0 (
/dev/tpmrm0) on the target is required to activate a capacity
licence. The free tier requires no TPM. - VPP stats-segment sizing. The BNG allocates a forwarding entry and
per-session counters for every subscriber, so the statistics segment must be
sized to the deployment's subscriber count — VPP's 32 MB default is exhausted
well before scale. 1.4.0 sizes this on install; see No validated VPP sizing
profiles under Known issues for what is and is not automatic.
Completed for GA
Capacity-tier session licensing — now operable from the console
The alpha shipped the Licensing page and said plainly that it "cannot yet
operate activation." It can now. Three defects stood between the page and a
working activation, all fixed:
- The three licence routes were missing their
/v1prefix and returned 404
on every call, so the page could not read status or fingerprint at all. - A trailing newline invalidated valid licences. A licence pasted from a
text editor or downloaded to a file failed verification for a reason the
operator could not see. - Activation itself did not work from the console — the shipped CLI carries
the service key, while activation requiresadmin_only.
The page now reads status, shows the fingerprint, and activates a pasted or
uploaded licence. The capacity in force, and its provenance, are stated rather
than inferred from the number.
Full accounting — records now come from live traffic, and nothing waits on a browser
The alpha marked usage records a preview: held, downloadable and prunable,
but not produced from live subscriber traffic.
GA produces them. The console now runs a background drain that claims the controller's
accounting spool every 30 seconds, writes it to a durable SQLite ledger in one
transaction, and acks only after the commit lands. That ordering is the one
non-negotiable property in the design: a crash before the ack costs a harmless
re-drain, while a crash the other way destroys billing records permanently,
because the ack prunes them upstream.
Previously the browser consumed that queue. Records drained only when
someone opened a page, and the ack — which permanently removes what it consumes
— was driven from the UI.
The ledger holds immutable CDRs for completed sessions, a live row per open
session, and per-subscriber daily usage rollups, with a daily prune and a
scheduled export that exports what is about to be pruned rather than the whole
ledger every day.
New features
AAA as system of record [console]
Local AAA is now the authoritative subscriber store, not a view over controller
config. subscriber_id is treated as an exact opaque key everywhere — never
folded, never case-normalised, never inferred from a locator. Enforcement
overlays converge on every reconcile pass rather than drifting until something
rewrites them, and a subscriber the console does not manage has its overlays
left alone.
Data-cap enforcement [console]
An operator sets a monthly cap and a consequence; the box applies it. The
subscriber drawer carries a data-cap panel showing the policy, the usage against
it and the grant history. Disabling enforcement clears every overlay rather than
leaving the last one in force.
Static IP assignment [console]
Assign a fixed address from the subscriber drawer. A conflicting address is
rejected before it is written rather than after. "Not mentioned" and "unassign
me" are now distinguishable, which they were not — an update that simply did not
mention an address used to read as a request to remove it.
Per-subscriber raw rate [console]
A raw per-subscriber rate, expressed as a plan, settable from the subscriber
drawer — with the consequence for that subscriber's cap stated on the same
screen rather than discovered later.
Subscriber sessions [console]
An account's live sessions are listed on its own page, and an operator can end
them from there.
Support bundle [console]
One button produces a diagnostic bundle for a ticket. It leads with a written
summary rather than a directory dump, and does its own first-pass triage: GRUB
drift, a licensed box mislabelled free_baseline, a packet capture that caught
nothing, VPP workers against rx-queues, retained config revisions.
It collects intent as well as running state — what the operator meant to
configure, not only what is currently live — and runs detached, because an
in-worker collection thread used to take the dashboard down with it.
New in this release: the controller's cumulative health counters. GET /bng/health is point-in-time and structurally cannot say the dataplane
connection has dropped four times today; the bundle samples it three times at
ten-second intervals, so it catches a flap only if one happens inside that
window. connection_lost_count states it for the whole uptime.
Add-on lifecycle [console]
Update checking that knows when to stay quiet, install from an uploaded tarball
for an offline box, a catalogue whose URL an offline mirror can own, and
retention of one previous artifact so a failed update can roll back without a
download.
Health page
The VPP card now qualifies "Connected" with how often the connection has
dropped, and goes amber when connected-but-flapping. A green pill over a link
that dropped four times reads as "nothing to see here" to the operator who is on
the phone about it.
Alerting and the metrics reference
Six new Prometheus alert rules ship in the bundle, covering data-cap
enforcement — which previously had none, despite being the subsystem that
throttles paying subscribers:
| alert | fires when |
|---|---|
BngCapEvaluationStopped |
the evaluation loop has died — no caps applied, lifted or rolled over |
BngCapOverlayFailing |
a cap was decided but the push to the controller failed |
BngCapEnforcementSpike |
mass enforcement against the recent baseline |
BngAccountingRecordsQuarantined |
a record could not be parsed, so it will not be billed |
BngAaaUnmanagedSubscribers |
subscribers exist on the box that the console does not manage |
BngAaaPartialPushPersisting |
pending provisioning pushes have stopped draining |
BngSessionCapApproaching is also fixed: in the alpha it could never
fire, which is worse than no alert because it reads as coverage.
docs/metrics/prometheus-metrics.md now documents all 36 exported metric
families, up from 18. Everything the 1.4.0 subsystems added — data caps, the
accounting drain, AAA reconcile — was previously undocumented, which left half
the /metrics surface unusable for building your own dashboards. The reference
now also states which families are emitted on the 60-second accounting-drain
tick rather than the 5-second sampler tick, since that changes what a sensible
rate() window looks like.
From the controller [controller]
Delivered upstream in controller 1.4.0 and available to every console in this
release:
- Faster subscriber onboarding. The DHCP onboarding path runs on a dedicated
worker pipeline with request batching and de-duplication, raising the sustained
rate at which subscribers can join and smoothing bursts. - Reboot resilience. After a bridge or controller restart, DHCP punting
re-subscribes correctly and health reports the real state rather than
reporting OK while delivering nothing. - Session recovery. A session the data plane retained through a recovery
cycle is re-adopted rather than dropped, removing a class of divergence
between the controller and the data plane. - Accounting across an unclean restart. Open accounting records are
reconciled and tagged correctly rather than being left open. - Captive-portal refinements. A short portal lease settles activation
faster, and additional TCP ports can be opened through the walled garden.
Behaviour changes
Read these before upgrading. None is a bug fix; each changes what the box does.
Licence enforcement for add-ons is now LIVE
This is the most consequential change in the release.
The console read licence entitlements from a field named features. The
controller reports them as grants and has never sent features, so every
entitlement resolved as unknown — and unknown failed open. Add-ons installed
regardless of licence, while the console reported that enforcement was not
active.
This is corrected. The consequence: a paid add-on on a box whose licence does
not grant it is now denied. No existing deployment is affected, because the
only add-on shipped to date is the Grafana dashboard pack, which is published as
permanently free and is permitted by name rather than by grant.
Two related changes:
- The licensing API response field
featuresis nowgrants, carrying a
map of name to value rather than a list of names, so an entitlement can
express a quantity as well as a yes or no. - Add-on entitlements are named under *`addon.`**.
Idle timeout now defaults to 1 hour [controller]
Raised from 5 minutes, sized for always-on subscribers so that an ordinary
traffic lull no longer reclaims a live session. Any value can still be set per
subscriber.
The Subscriber column is removed from the sessions table
It could not work on controller 1.4.0. GET /bng/sessions returns no
subscriber_id and no circuit_id/remote_id, so the column's join had nothing
to join on and every session rendered "unprovisioned." A column that is
always wrong is worse than no column: it actively tells the operator their fleet
is unprovisioned.
It returns in 1.5.0, joined on subscriber_id, which the controller adds to the
session summary in that release.
Breaking changes **[controller]**
Controller 1.4.0 reworks the subscriber-identity and provisioning contract. A
management plane integrated against 1.3 must be updated, and the subscriber base
must be re-provisioned — these are not backward compatible.
The console in this release is already built against the new contract, so an
operator using the console needs to take no action beyond the re-provisioning
below. These matter to anyone driving the REST API directly.
- Subscriber records are keyed by an operator-assigned account identity and
carry their locators explicitly, replacing the previous line-id keying. The
subscriber base must be re-provisioned under the account model. - The AAA source is selected at a new
/bng/aaaendpoint. The previous
open/gatedmode field on the local-AAA configuration is removed. - A live plan or status change is made by writing the subscriber record,
which cascades into the running session. The separate per-session plan and
revoke calls are replaced. - Bulk provisioning request and response fields are renamed to the account
identity. PUT /bng/sessionis now a partial merge — fields omitted from a request
are preserved rather than cleared. A caller that relied on omission to clear a
field must now clear it explicitly.
The controller's own release notes carry a migration checklist mapping each old
call to its replacement, and a Feature Integration Guide with the request and
response contract per capability. Both are the authority for integration work;
this section only states what breaks.
Bug fixes
Selected; the ones whose absence changed behaviour an operator would notice.
Subscriber simulator (CPESIM)
- The demo passes traffic again. CPESIM 0.4.17 shipped without the commands
that clear stale veth devices before recreating them, so the simulator's
uplink path came up half-built and the demo onboarded subscribers while moving
no traffic — ~0 Mbps, with the Simulator page showing subscribers connected and
reporting no error, because the engine's failure logging had been dropped in
the same build. 1.4.0-14 pins CPESIM 0.4.18, which restores both, and makes
a burst that moves no bytes report the reason it already had. Measured on an
evaluation appliance: 0.001 Mbps before, 1,097 Mbps after.
Sessions
- An unavailable session list no longer renders as an empty one. A failed
fetch of the session list reported nothing, so the table came up blank and
silent — indistinguishable from "this box has no sessions". That is the most
misleading thing the page can say, because the request is most likely to fail
precisely when there are a great many sessions to return. The page now says
which it is, and a failed refresh keeps the last good list on screen rather
than blanking a table you are reading. This makes a failure legible; it does
not change how the page performs at scale — see Console polling load under
Known issues.
Snapshots
- A restore now actually restores. Per-block PUTs could only restore what had
an endpoint a restore could safely drive; the subscriber base, the
access-interface set and the switch registration had none, so a REPLACE restore
could not put a wiped box back however many blocks it applied. It now applies
the controller's whole captured state. - A restore would have disabled every access port in the snapshot. The
access-port call sent the wrong key names with no address, which the schema
rejected — and that rejection was the only thing preventing worse. The field it
read to decide enabled/disabled does not exist on 1.4.0, so it evaluated false
for every interface.
Accounting and the ledger
- Three residual ways the daily maintenance could destroy billing data.
- A quarantined billing record is no longer invisible.
- An implausible
retention_daysis refused rather than silently deleting a year. - A retained live population no longer starves the drain forever.
- CDRs are filtered in SQL, so an old date range is reachable.
- A failed CDR fetch is distinguished from an empty ledger.
Data caps
- Cross-subscriber leakage in the grant panel.
- Disabling enforcement clears every overlay.
status()queries hoisted out of the per-subscriber loop.
Controller state
- The retained config history is now bounded (upstream bng#41). Nothing read
old config revisions, and unbounded growth cost provisioning latency roughly
tenfold at the same subscriber base — a slowdown that survived a restart. - Importing as non-root no longer dies on the CA probe.
Console
- The captive-portal
portal_ipsave no longer erases the walled garden. - The metrics sampler no longer fetches the whole controller config every tick —
about 1.9 MB per call at 10,000 subscribers, every five seconds, for a key no
controller has ever published. - The cap caption no longer says "Community" on a licensed box, and the sessions
chart draws the cap line at the cap actually in force. - AAA faults surface in the alert centre.
- The wipe notice names
aaa_sourceamong what was not restored.
Known issues
Issue references below are internal tracking identifiers, not public links.
#NN refers to the QuantumTouch console and appliance; bng#NN to the
Quantum BNG controller — the two are numbered separately, so #38 and
bng#38 are different items. Quote them when raising a support ticket.
Session reclamation at high scale [controller] — bng#50
Under sustained churn at 18,750 concurrent sessions, the controller's session
view stops reclaiming released sessions: measured at exactly its high-water mark
for 550 seconds with a drain rate of zero, while clients had released down to
8,191. Nothing is lost — orphans were zero in every run — and the cap, dataplane
and onboarding paths are unaffected.
Traced upstream to two defects, both fixed in controller 1.5: relayed DHCP
RELEASEs were dropped entirely (so reclamation only happened on lease timeout),
and teardown ran per-packet, capping reclaim at a few hundred per second.
Not reproducible at 7,500 and below across three runs.
Session count under sustained churn [controller] — bng#36
A separate effect from the reclamation issue above, and it presents differently:
under sustained high churn the controller's reported session view can
under-count a cohort of otherwise-stable sessions. The sessions themselves
remain bound and carrying traffic; it is the count that is short, and the
affected sessions reappear in the view when they next renew.
Reclamation above is a count that stays too high; this is a count that reads
too low. Both are view-level, and neither loses a session. Investigation is
ongoing upstream, with verification scheduled for a post-1.4 test cycle.
Practical consequence: during a churn event, treat the session count as
indicative and the pool's allocation figure as authoritative.
The Dataplane page reports the BNG's own NICs as unassigned — #49
The console's devbind view sorts NICs into kernel, DPDK and VPP. A NIC is
placed under VPP only when it appears in the stored port-assignment list;
the bucket records the assignment an operator made, not what VPP is driving at
that moment. On a box whose ports were never assigned through the console — a
hand-configured startup.conf, or a commissioning run that stopped before the
Dataplane step — the access and uplink NICs are correctly shown as DPDK-bound
but never attributed to VPP, even while VPP is forwarding on them.
The display is the smaller half. The same stored list is what makes
/api/dpdk/devbind/bind, /unbind and /reset-all-to-kernel refuse an
operation on a port VPP is using: with the list empty those endpoints do not
return 409 VPP_MUST_BE_STOPPED, so a rebind requested against a live
dataplane NIC is carried out instead of refused. The rebind also re-renders
/etc/vpp/startup.conf. Nothing does this on its own — it needs an operator or
a script to ask for it — but the guard that should stop it is not armed.
Workaround. Assign the dataplane ports on the Dataplane page as part of
commissioning, before the box carries subscribers. That records the assignment,
restores the VPP attribution and re-arms the guard. On a box already in service,
do not use the bind / unbind / reset-to-kernel controls to fix the display:
the operation will be allowed and will interrupt forwarding. Assign the ports
during the next planned maintenance window instead.
Affects the console's reporting and that guard only. Subscriber onboarding,
forwarding, QoS and the session cap do not consult this list, and the dataplane
itself is unaffected — this is visible in the 1.4.0 validation run, where the
check for VPP-attributed NICs is the single failing check on a box that passed
every forwarding, onboarding and QoS measurement in the same run.
RADIUS is not active — #17
The /bng/radius surface is inert in this build; local AAA is the AAA backend.
GET reports active: false and PUT is refused, deliberately, so an operator
cannot configure a backend that silently does nothing. An ISP that already runs
an external AAA or OSS cannot integrate with this release.
There is no RADIUS page in the console — it has been unrouted since v1.2, so
nothing in the UI invites you to configure a backend that would not work. The
Health page reports AAA (source, subscriber count, and the unknown-CPE
policy) rather than RADIUS server reachability. Earlier builds showed two red
"RADIUS Unreachable" cards on every box, which was a false alarm for a subsystem
that is inert by design; that is fixed in this release.
External AAA integration is tracked as #17, as a paid plugin covering
FreeRADIUS, Splynx, Sonar, UISP and Powercode — provisioning in, records out.
No live coverage that an add-on installs — #38
The add-on install path is thoroughly unit-tested, but every one of those tests
mocks the fetch, the hash and the unpack. Nothing verifies that a real pack
downloads from its distribution point, installs on a real box and then functions.
Tracked for 1.5.0 as #38.
The Subscriber column — #27, restored by #37
Removed — see Behaviour changes.
Only subscribers reconcile after a controller upgrade — #31
See Upgrading. The controller discards its configuration replica on upgrade,
and nothing re-pushes it automatically. The console is the authority, but it
has no drift-triggered full-state reconciler in 1.4.0: there is no config_hash
drift detection in the console at all, and the only reconciler that exists
operates on the AAA store.
Recovery is therefore operator-initiated: take a snapshot first and restore it.
That works — see Upgrading for the two restore paths and what each covers —
but it is a manual step, and a box upgraded without one has to be reconfigured
by hand. #31 is the full-state reconciler that would make it automatic.
No durable per-subscriber usage history — #33
Accounting interim records are collapsed into one mutable row per open session
plus a daily rollup, which discards the time dimension. So "how much did this
subscriber use, by hour, last week" cannot be answered from the box.
Current and cycle-to-date usage are correct and are what data-cap enforcement
acts on — this affects history only. Note the controller's own per-session rate
history is an in-memory ring (~10 minutes, deliberately lost on restart), so
until #33 lands there is no durable per-subscriber series on either side.
No validated VPP sizing profiles — #29
The stats segment is now sized by the installer. Earlier releases left it
at VPP's default, so it had to be set by hand. GA sets it to 1 GiB on install,
raises a smaller existing value, and never lowers a larger one — if you sized
it up yourself, your value is kept.
The figure is a deliberate margin rather than a calculation. Measured on a
25,000-session-licensed box, usage is a fraction of a percent of that
1 GiB, and it scales with the VPP worker-thread count and the number of
interfaces and graph nodes — not with the number of subscribers online.
A box with more workers needs more at any subscriber count. The margin is
there because the segment is cheap on any supported host and because the
behaviour under a recovery replay, which is where exhaustion would be worst,
is not something these figures cover.
1.4.0 sets processor_statseg_size_mb = 1024 on install, raises a smaller
existing value, and never lowers a larger one. If you sized it up yourself,
your value is kept.
What is still missing is the per-tier part: 1024M is a known-good ceiling
for 25,000 sessions, not a derived minimum, so it may be over-provisioned for
smaller deployments. There are no profiles for other subscriber or throughput
targets, and worker counts and buffer sizing are still derived by the operator
outside the tested configuration. #29 tracks shipping validated profiles.
You can watch it: GET /api/compute/memory_breakdown returns
vpp_stats_segment with total_kb, used_kb and free_kb, and the console's
memory panel renders it — so approaching the limit is visible rather than
something you discover under load.
Console polling load scales with session count and open tabs — #14, #15
Two separate effects, neither of which affects correctness:
- The sessions view fetches the whole session list every 5 s and differences
it client-side to derive rates. At 25,000 sessions that is a large repeated
read. The controller endpoints that let us replace it with a server-side
top-N and per-session rate land in controller 1.5 (bng#38);
retiring the full-list pull is #14. - Each open browser tab runs its own polling loops, so controller load scales
with (tabs x pages x loops). Coalescing them through the backend is #15.
Operationally: on a large deployment, prefer fewer simultaneously-open console
tabs during a scale test or a maintenance window.
A licence activated on the evaluation appliance can revert to the free tier — bng#52
You do not need a licence to evaluate. The evaluation appliance runs on the
free tier, which needs neither a licence nor a TPM, and licence activation is
not part of the supported evaluation path. If you never activate, this issue
cannot affect you.
If you do activate a capacity licence on the evaluation appliance, it can later
report EXPIRED, and the session cap returns to the free tier's 1,000. It has
been seen after a power cycle, but the validity check also re-runs every 60
seconds while the box is up, so the box does not have to be restarted for the
state to change.
What is not happening is as useful as what is. The machine fingerprint is
unchanged across the restart, and the sealed licence material is still read back
successfully — a licence that could not be unsealed would report NOT_ACTIVATED
rather than EXPIRED. Identity and sealing are intact; the validity window itself
is being computed wrongly. That window is derived from the TPM clock, and on the
evaluation appliance the TPM is emulated. Whether a discrete hardware TPM
behaves the same way has not yet been measured; bng#52 tracks that.
Workaround. Re-submit the same licence file on the Licensing page. It
returns to VALID with the full capacity immediately, and no other state is
disturbed. Otherwise evaluate on the free tier, which is unaffected.
Capacity is the only thing this touches. Licence enforcement in this release is
capacity-only: no feature is gated on holding a licence, only quantity is, and a
missing, expired or tampered licence reverts to the free tier rather than
withdrawing function. Configuration, subscriber onboarding, forwarding, QoS,
accounting and the console all continue to work normally at up to 1,000
sessions.
Usage Records is slow to appear on a box with a large ledger — #57
The Usage Records page reads the whole stored CDR ledger in one request. On a
box that has been carrying subscribers for a while that request is large, and
until it finishes the page shows two dashes in place of the Stored CDRs and
Live sessions figures, and an empty table. On a lab box holding 609,000
records it took about 45 seconds; it scales with how many records you are
holding.
Nothing is wrong with the data while this is happening. The figures and the
records are intact and arrive in full once the read completes — the page is
slow to render them, not missing them.
Workaround. Narrow the read before you load it. The Since / Until
and Subscriber ID filters are applied by the server, so a filtered view
returns promptly on any ledger size. Filtering to the period or the subscriber
you are actually reconciling is both faster and usually what you wanted.
Export CSV and Export JSON are also server-side and unaffected.
If you have a very large ledger and the page is unresponsive rather than merely
slow, prefer the exports, and prune the ledger on its normal schedule.
Billing is unaffected. The accounting drain, the stored records, the daily
usage totals and the exports all behave normally; this is a display path only.
Installing and upgrading
A fresh install is the expected path for GA, and the one the release is
validated on. Follow the Baremetal Quick Start;
nothing in this section applies.
The rest of this section is for a box currently running an alpha build.
Note first that the subscriber model changed — see Breaking changes — so an
alpha box's subscriber base must be re-provisioned under the account model
regardless of how the software gets there. For many alpha deployments a
fresh install plus a re-provision is simpler than an upgrade.
Upgrading from an alpha build
The controller wipes its configuration replica on upgrade, by design: the
console is the authority and the controller's copy is a disposable replica.
Take a snapshot before upgrading. This is required, not advisory.
What the wipe discards is the controller's whole replica: pools,
access_interfaces, uplinks, edge_routers, captive_portal, plans,
session, DNS, and the local AAA store (aaa_local, aaa_source).
Nothing is re-pushed automatically on drift in this release — there is no
drift-triggered full-state reconciler (#31). An upgrade without a snapshot
leaves a box that onboards nobody, because its access interfaces and pools are
gone.
The snapshot is what puts it back, and it has two paths:
- Replace (normal) — the snapshot carries the controller's own whole-state
envelope, and the restore applies it via a single
POST /v1/quantum/persistent/state/latest. This returns the entire
configuration, including the subscriber base and the access-interface set. - Merge (fallback, when a snapshot has no envelope) — per-block writes
covering DNS, pools, plans, RADIUS, the captive portal, access ports and
uplinks. This path cannot fully restore a wiped box: the subscriber base,
the access-interface set and the switch registration have no endpoint a
per-block restore can safely drive. The restore reports which path it took.
Both paths are materially better than in the alpha, including a fix
for a restore that could not restore an access interface's address at all
(#30) — which is why the snapshot is now the documented recovery route rather
than a precaution.
Installer bundle 1.4.0-14. Standard procedure, unchanged from the alpha.
Appendix — every change in this release
Every change between the last alpha build and GA — 248 commits,
grouped by kind. The Bug fixes section above is a selection; this is the
complete record. Generated from v1.3.0-09..69e614a; the commit that writes
this appendix is necessarily not listed in it.
Features (60)
a692e82feat(alerts): watch data caps, and fix an alert that could never fire (#35)db09e49feat(health): qualify "Connected" with how often it has dropped (#28 part 2)8766f0cfeat(support): cumulative health counters — a flap was invisible (#28)6547841feat(support): say whether the box is licensed, and why the cap disagrees159900efeat(events): AAA faults in the alert centrea7d3d75feat(controller): bound the retained config history (bng#41)6e66f4cfeat(support): collect intent, not just running state; fix the capture1ddc2cbfeat(support): consolidate the bundle and lead with a written summary6c998affeat(support): one-button diagnostic bundle for a support ticketd27b574feat(aaa): subscriber_id is an exact key everywhere (#25)dd2ad55feat(rates): A-06 in the subscriber drawer, with the cap consequence stateddbf33ecfeat(rates): A-06 — a raw per-subscriber rate, expressed as a plana93def0feat(caps): wire the per-subscriber surfaces an operator could not reach6520bf5feat(billing): customer_ref reaches exported CDRs — closes the G-11 gapf350e5dfeat(caps): data-cap panel on the subscriber drawere938fa5feat(caps): Data Caps page and its client51768fafeat(caps): the operator API for data-cap policyd490741feat(caps): run the evaluator on the drain tick, publish bngcap*4c8c146feat(aaa): converge enforcement overlays every reconcile pass52f51d9feat(caps): declare the data-cap webhook events at registratione12bee1feat(caps): evaluate_once -- usage in, enforcement rows out5c33019feat(caps): resolution and decision, as pure functions92c7914feat(caps): sum usage over a cap cycle windowc807ca0feat(caps): CapPolicyStore -- allowances, overrides, grants, cycle state442dd65feat(caps): the cap cycle calculatora2f89b5feat(aaa): surface unmanaged subscribers with adopt and removefa36791feat(aaa): run the reconciler and publish what it does2800a25feat(aaa): write durably before pushing to the controller9dacdcdfeat(aaa): detect a wiped controller and restore the base5a09424feat(aaa): converge the controller onto our store8c1ba55feat(aaa): one-time adoption of the controller's subscriber base5268a80feat(aaa): durable subscriber storea86eecffeat(accounting): the page becomes a view over the ledger303fc76feat(accounting): publish drain lag, age and errorsb2673e8feat(ledger): daily prune and scheduled exportcf5ca6afeat(accounting): read and export the ledger3c3544dfeat(ledger): 13-month retention and historised identityc6cf03efeat(accounting): drain the spool server-side, ack only after commit5e77571feat(ledger): daily usage by differencing cumulative countersdd94744feat(ledger): durable accounting store7e5c82bfeat(about): stamp the wheel with the commit it was built from1290760feat(subscribers): show an account's sessions, and let an operator end them6be6c4dfeat(static-ip): assign an address from the subscriber drawer39829b6feat(static-ip): reject a conflicting address before it is written02aa0ebfeat(static-ip): CSV columns, and rename the observed address1654edefeat(static-ip): reject an address the pool will later lease0caca48feat(capacity): make a full box impossible to missda92855feat(add-ons): close the update-checking half of the lifecycle90312bbfeat(marketplace): brighter selector, and Check now shows its work92f2797feat(marketplace): restore the page selector between Licensing and Add-ons605f411feat(bundle): pin upstream v1.4.0 and core 0.10.1782cc22cfeat(webui): two AAA controls, and delete the inert recovery-age knob079b383feat(aaa): locators, subscriber_id keying, and the new bulk shapee1e70d0feat(aaa): source selector replaces the removed mode fieldac535aafeat(webui): Licensing leads with the fingerprint and the cap in force86d43d0feat(webui): Marketplace section with the Add-ons pagec6a45affeat(addons): update checking that knows when to stay quiet5b2e3f2feat(addons): both modules install from an uploaded tarball8086199feat(addons): the catalogue, with a URL an offline mirror can owne313512feat(addons): retain one previous artifact, and roll back to it offline
Fixes (91)
66c8283fix(sessions): an unavailable session list must not render as an empty oned05ed72fix(webui): drop two false claims from the topology, and stop the session drawer opening blank5f31de2fix(statseg): the sizing axis is threads, not subscribers6a47925fix(install): the statseg fix wrote a key core never reads — bundle -10 is a no-opccc0c56fix(docs): the stats segment IS measurable — I checked the wrong route21c5c10fix(install): size VPP's stats segment — nothing in the product did (#42)72862b3fix(health): two red RADIUS cards on every healthy 1.4.0 box were a false alarm (#39)00d05dffix(webui): the Topology page had no AAA element at all, and Pools read DNS from a key that does not exist (#39, #40)c2e9128fix(restore): verify an access-port write landed instead of trusting 200 (#30)1389fedfix(sessions): drop the Subscriber column for 1.4.0; restore it in 1.5 (#27)c180ff6fix(licensing): readgrants, not a field the controller never sent (#32)4936c90fix(snapshots): a restore would have disabled every access port (#30)b2d734cfix(webui): the cap caption said "Community" on a licensed box991ab53fix(sessions): draw the cap line at the cap actually in forcec1c9ed3fix(licensing): activation works from the consolef116a40fix(licensing): a trailing newline was invalidating valid licences8259ab9fix(snapshots): make restore actually restore4f6d033fix(captive-portal): stop the portal_ip save erasing the walled gardenaf961cffix(support): run the collection detached — an in-worker thread killed the dashboarde0f8175fix(support): parse BUNDLE_VERSION; bundle 1.4.0-057de3579fix(webui): an unprovisioned subscriber_id is a write key, not a join key (#25)3ad4709fix(tests): stop the suite running production daemons (#21)cb2468ffix(aaa): deleting a capped subscriber no longer strands its overlay012e639fix(controller): importing as non-root must not die on the CA probe348244efix(aaa): leave unmanaged subscribers' enforcement overlays alonec2addbcfix(webui): data-caps client uses fetchWithAuth; input bounds match server6c8b8f6fix(aaa): make post-reprovision overlay replay non-fatal6fa4654fix(caps): hoist status() queries out of the per-subscriber loope7fa291fix(caps): add grants_in_window; drop two dead helpersb4f01bafix(caps): disabling enforcement now clears every overlay5eb75a0fix(caps): stop cross-subscriber leakage in the grant panel0434621fix(caps): surface write failures, drop grant form, protect settings draftfc1a2adfix(caps): fix round 1 -- connection leak, coercion 500s, test gaps7947075fix(aaa): guard enforcement convergence against non-controller failuresc87dac5fix(caps): test coverage and boolean logic for NULL handling1524590fix(tests): stop the suite running a live metrics sampler2bdae92fix(api-keys): survive, and repair, a keys file still in the bare-list shape9ca5d80fix(proxy): close the third door into the AAA subscriber basede1c146fix(aaa): drop a subscriber from the unmanaged list once it becomes oursd52c944fix(aaa): restore the merge base for a record we do not hold52f9e41fix(webui): the wipe notice must name aaa_source among what was not restoredb415e3cfix(aaa): debounce the wipe guard, tell its conditions apart, and correct §5eca818efix(aaa): close the two proxy routes that write only the controllerb2b5c08fix(aaa): merge over the record, not over the controller06475c5fix(aaa): stop an orphan annotation poisoning the whole restore46e073dfix(aaa): say the switch may need re-registering before the plans678e419fix(aaa): stop a refused restore wedging the reconciler, and say whya4d6665fix(aaa): let the circuit decide which line a mint may reuseb72441bfix(aaa): roll back a rejected apply, and stop one bad row killing reconcilec39b7e3fix(aaa): record a bulk import before pushing it, closing the seta5aff25fix(aaa): close the delete and mint holes the inversion opened169b7a2fix(aaa): surface reactivation_history through get_subscriber3ccbb2cfix(ledger): three residual ways the daily maintenance could destroy billing data31c7730fix(accounting): a quarantined billing record must not be invisible2bc628cfix(accounting): a retained live population must not starve the drain forever8e33280fix(ledger): export what is about to be pruned, not the whole ledger daily41f7a9bfix(accounting): filter CDRs in SQL so an old date range is reachable9afd934fix(ledger): refuse an implausible retention_days instead of deleting a yearebcc8c3fix(accounting): distinguish a failed CDR fetch from an empty ledger4b8a749fix(accounting): count a raised drain cycle as an error toof0bf65ffix(ledger): drain thread's sqlite connection was unusable cross-thread863b36efix(accounting): page forward within a cycle instead of re-fetching a static window53bede3fix(accounting): review fix wave -- vacuous tests, reset heuristic, late-interim double-credit7cb61c2fix(ledger): per-record SAVEPOINT so a DB error on delete can't split a record9257da3fix(ledger): make write_records genuinely atomic8b4a2e0fix(build): only tracked changes make a build dirtyef6a848fix(static-ip): narrow the unassign sentinel to framed_ip/subnet_maskecb0da7fix(static-ip): check zero-padded addresses, and two tests that could not fail7084e34fix(subscribers): a failed session fetch is not an empty account92c0810fix(aaa): stop the raw subscriber PUT proxy erasing a static IPb4838f2fix(static-ip): tell "not mentioned" apart from "unassign me"1e42a42fix(static-ip): wire framed_ip/subnet_mask into compute_diff2d27b9bfix(webui): restore the markup two descriptions lost to a narrow typecc8aa10fix(typecheck): clear the 16-error backlog exposed by the tsconfig rootDir fix3dadea1fix(add-ons): the primary Install button never said Installing…75e652efix(add-ons): restore the expander that described what the pack contains6a4062efix: the eval appliance HAS a TPM — correcting a wrong premise9b694effix: two error states that lied, and the CPESIM pind52be84fix(eval-appliance): find the bundle our own build script produces5b1d434fix(aaa): merge subscriber PUT over stored record, not just match6061b2efix(1.4.0): whole-branch defects — locator erasure, bulk crash, false alertfa7e334fix(webui): guard health.radius reads against the 1.4.0 shapecd3e05efix(health): stop fabricating a radius block in degraded fallbacks2958feefix(portal): voucher redemption can finally activate a session35e8775fix(aaa): carry locator columns through the upload pipeline, spec-validate the two rewritten bulk testsfa03fd3fix(aaa): rewrite the two route-level tests Task 3s rewrite broke9f75262fix(aaa): delete the cascade planner that never fired131f376fix(deps): declare jsonschema/referencing as dev extras0568042fix(webui): API Keys page rendered blank on every box; replace two screenshots6a8b3f8fix(appliance): clear intermittent first-boot hang with rcu_normal; ship 1.3.0-13d89225ffix(licensing): the three controller paths were missing /v1 and 404d
Performance (2)
c29a377perf(metrics): stop shelling out every 5s for counters we discard (#36)b8c4d3cperf(metrics): stop fetching the whole controller config every tick (#14)
Documentation (57)
84ae8d6docs: point the dataplane known issue at its real issue number (#49)7aeebbadocs: record the dataplane port-assignment issue and its workaround9a12835docs(release): regenerate the appendix against 433b916 (243 commits)433b916docs: roll the 1.4.0 doc set to bundle 1.4.0-13 / plugin 0.10.3675a7bccdocs(release): frame 1.4.0 as GA, with everything prior as one alpha bucket897c4bbdocs(release): append the complete commit record9e7b755docs(release): the commit count was stale — 229, not 20451ed4d4docs(release): carry the controller and VPP side of 1.4.0 (#43)7d3dd4fdocs(release): remove internal framing and the "shim" terminology84c7f4bdocs(release): bundle 1.4.0-10, and say what the installer now does about statsegefee8aadocs(aaa): correct a comment I generalised from one configuration236b0eedocs(release): say what the console actually shows for RADIUS and AAAc7ace1edocs(release): 1.4.0 notes — known issues now cover the open set, and Upgrading no longer over-promisesd586affdocs(metrics): document all 36 families, and guard against the drift (#34)8f1e614docs(lifecycle): mark §4 and §9.3 superseded by the state-envelope restore10eb38adocs(spec): record the #24 amendmentsa998128docs(spec): §5.2's enabled_ts floor is day-granular — state the limit2ec2311docs(plan): sub-project E implementation plandcb9614docs(spec): sub-project E — data-cap enforcement0e5682adocs(plan): AAA as system of record (sub-project F)d7eb45cdocs(spec): AAA as system of record (sub-project F)96aeea3docs(lifecycle): QuantumTouch takes ownership of AAA8e1f111docs: state ownership and lifecycle — what the controller must persist8a6be39docs(spec): the binding spec described behaviour that was deliberately deletedb186183docs(plan): accounting ledger (sub-project D)1aab859docs(spec): accounting ledger (sub-project D)3ff8da5docs(plan): sub-project C — static IP and subscriber sessionsa939894docs(spec): static IP and subscriber sessions (sub-project C)bc4c816docs: preserve the 1.4.0 port execution ledger; ignore vagrant state7c96972docs(artifacts): the _test endpoints are NOT served in production — correctiond6b4bafdocs(artifacts): correct the health.radius claim — twice wrong366f931docs(plan): implementation plan for the controller 1.4.0 portc818b2edocs(spec): the cascade has never fired, and sessions do carry subscriber_id2504d03docs(spec): controller 1.4.0 port — design for sub-project Ae109e42docs(artifacts): import the 1.4.0 controller API spec and evaluate it against our tree50c3bdbdocs: direct upload for disconnected boxes; URL path is the orchestrator seam5163235docs: rename About to System and reserve it for platform lifecycle12aeee9docs: correct -- capacity licences are perpetual, not subscriptionsca3fdbedocs: draw the licence expiry cliff, and free amber for it2bea687docs: record the three capacity bands, and two consequences of replace978203bdocs: scope the design to QuantumTouch; hand the store findings off99699d5docs: session cap replaces rather than stacks845b417docs: nav group is Marketplace, holding Licensing and Add-ons9e81489docs: CPESIM need not stay pinned -- the blocker is an unversioned engine APIb7a1e27docs: read-only operator gets an "ask an administrator" fingerprint card536dee1docs: machine fingerprint leads the Licensing pagefc25baddocs: speed caps replace rather than stack; interface config is a prerequisite16bcc14docs: design for the add-ons + entitlements console section3bbe2f4docs: publishing instructions for the 1.3.0 docs web implementerb627fa5docs: state survey ahead of lifecycle managementd248b24docs(grafana): customer guide for the dashboard pack; pack is free and unlicensedbcbaf0cdocs: capacity licensing is staged for 1.4.0, not a 1.3.0 capability19b9f6ddocs: correct the licensing documentation — the console flow does not work2e437fcdocs: response to the feature-query interface proposal (#33)7b9796adocs: re-shoot the console screenshots, and fix two things they caught68cbb67docs: update the operator guides for 1.3.03027512docs: review the 19 published QuantumBNG docs against 1.3.0
Tests (14)
56f8baatest(radius): tag lab-dependent tests so a release run can drop themd4707f4test(aaa): cover the branches that had none, and verify each catches its regressiona10af89test(aaa): prove the reconciler is actually reachable from the drain threadc0b397atest(aaa): pin remove_unmanaged's unreachable-controller orderingdd5741atest(accounting): assert the test drain thread does not outlive its teste4c55adtest(ledger): pin the INSERT OR IGNORE rule at the statement itselfd650317test(accounting): pin the real lag computation, and ship spec 8's second alert3225513test(subscribers): cover the session client directly, not through the drawer12140d1test(subscribers): replace a vacuous role="alert" check with a real one17b91b7test(static-ip): pin the conflict/generic-failure routing to the error code6fadfa6test(health): update happy-path fixture to the 1.4.0 aaa shapeabbe19btest(aaa): fix stale mode fixture, cover radius rejection0e75d2atest: a controller double that enforces the real 1.4.0 contract5457c62test(webui): repair two stale Dashboard tests and the pool card router context
Build and chores (7)
fe8dc75chore(deps): lock @quantumtouch/core to the v0.10.23 commitda25223chore(deps): core 0.10.23 for plugin webhook event registrationa6ea37echore(deps): lock @quantumtouch/core to the v0.10.22 commite63c9d9chore(deps): core 0.10.22, and a guard so the two pins cannot drift4cbcab9chore(deps): quantumtouch-core 0.10.21 — accessible Field labelsab40ce3chore(bundle): cut 1.4.0-0254acdd4chore: ignore .worktrees/
Release (9)
69e614arelease: bundle 1.4.0-14, plugin 0.10.37, CPESIM 0.4.18a47b639release: bundle 1.4.0-13, plugin 0.10.36a79fa2arelease: cut installer bundle 1.4.0-1204ed60arelease: bundle 1.4.0-1144a426arelease: plugin 0.10.34 — the version never moved across 1.4.02caaaf9release: bundle 1.4.0-10d4807b8release: record the bundle 1.4.0-09 version bump that shipped to the rig1fcf348release: bundle 1.4.0-048673133release: bundle 1.4.0-03
Other (8)
915dc44Merge release/1.4.0-13: sessions fetch-failure fix + 1.4.0 doc setc5db7c5Revert the Pools DNS change: #40 was not a bug, and my test disproved itbe425ddMerge support-bundle health counters, snapshot access-port fix, cap captioncd0f367Merge A-06: per-subscriber raw rate overrideae00fbfMerge #24 follow-ups: billing interop, wired cap surfaces, overlay ownershipd39660cMerge sub-project E: data-cap enforcement0f0fc5fRestore installedLinks, capability reload, and backstop poller on Add-onscc432d3Merge feat/controller-1.4.0-port: port to controller 1.4.0