Provider packages¶
A provider package holds everything Tapyr knows about an API: the Provider (base URL, auth, pagination, limits, errors), job templates, a simulator, scenarios, a contract test, a dashboard and examples. Packages are declarative: YAML and CEL, no Go, except for the rare auth flows written as plugins. The layout is described in 03-configuration.md.
Built-in packages¶
| Package | API | Validation | Notes |
|---|---|---|---|
teltonika-rms |
Teltonika RMS cloud API | Real platform (e2e) and simulator | Monthly quota synced from the X-RateLimit-* headers |
teltonika-rutos |
Local RutOS API of Teltonika routers (07.06+) | Real router (e2e, RutOS 07.24.5) and simulator | One target per router, session_login with logout, self-signed TLS |
meraki-dashboard |
Cisco Meraki Dashboard API v1 | Simulator only | Organization-wide endpoints, per-organization rate |
aruba-central-classic |
HPE Aruba Networking Central (classic) | Simulator only | Daily quota, aruba_central_classic auth plugin (rotating refresh tokens) |
example-sim |
Vendor-neutral example | Simulator | Reference for package authors |
tapyr provider list lists them with the packages of providerPaths, which override built-in packages of the same name.
Writing a package¶
- Read the vendor documentation and write
provider.yaml. Every limit cites its source in itsdescriptionand in the README, or is marked as measured or assumed. Prefer endpoints that return many objects per request (organization-wide lists, large pages): the budget engine spends quota deliberately, the templates should too. - Write the job templates (
templates/*.yaml) withestimate.pagesPerRun. Metric names follow<prefix>_<object>_<measure>_<unit>in base units, descriptive attributes go to_infometrics, and no high-cardinality label (client MAC addresses, IPs) is used. - Build the simulator (
sim/sim.yaml): a synthetic inventory with template fixtures (see08-simulator.md), or static fixtures from a recording. Ship thedefault,quota-tightandbad-dayscenarios. - Write the contract test (
tests/contract.yaml,tests/contract.env) and generatetests/expected.promwithtapyr provider lint --update <dir>. Review the samples: they are the contract of the package. - Add a Grafana dashboard (
dashboards/*.json, with adatasourcevariable, anAPI quotarow when the API has a quota, and a link toTapyr / Overview), examples (examples/*.yaml) and the README. Every package README follows the same skeleton, as inexample-sim: - an introduction (the API, what is collected) with the Validation status (real platform or simulator, versions, dates);
Requirements, when there are (firmware versions, permissions, network access);Coverage: a tableTemplate | Endpoint | Default interval | Requests | Metrics(requests per run), then the labels, joins and conversions;Auth setup: how to get the credentials and give them to Tapyr;Limits: a tableLimit | Value | Source, then the quota maths with the default intervals;Simulator: the command, with the port the simulator gets undertapyr sim --all --listen-base 127.0.0.1:8081(8081 plus itsportOffset), the credentials it accepts and aScenario | Effecttable;Dashboard,Known caveats, andEnd-to-end testwhen there is one.tapyr provider lint <dir>must pass.
CEL mapping recipes¶
Patterns taken from the built-in templates. The CEL environment and its functions are described in 06-mapping-and-outputs.md.
Enveloped lists. extract.items points to the list inside the envelope (teltonika-rms, aruba-central-classic):
extract:
items: response.body.data # {"success": true, "data": [...], "meta": {...}}
Paginate such a list in the simulator with response.itemsField (see 08-simulator.md).
Flattening nested lists. One item per appliance and uplink (meraki-appliance-uplink-statuses):
items: >-
response.body.map(a, default(a.uplinks, []).map(u, {'serial': a.serial, 'interface': u.interface, 'status': default(u.status, '')})).flatten()
Absent or null fields. default(value, fallback) covers a missing or null field; has() tests a key before reading below it; skipIf drops a sample rather than inventing a value (meraki-dashboard, aruba-central-classic):
labels:
name: "default(item.name, '')"
network_id: "has(item.network) ? default(item.network.id, '') : ''"
metrics:
- name: aruba_ap_clients
value: toFloat(item.client_count)
skipIf: "!has(item.client_count) || item.client_count == null"
Timestamps. parseTimestamp reads RFC 3339 by default, or a Go layout; a timestamp metric is a number of seconds (rms-devices):
value: "double(int(parseTimestamp(item.last_connection_at, '2006-01-02 15:04:05')))"
As the timestamp of a sample, it carries the source time of the value (example-sim-devices): timestamp: parseTimestamp(item.updatedAt).
_info metrics. Descriptive attributes go to an _info metric (value 1) that shares an identifier label with the measures, so that queries join them (meraki-devices):
info:
- name: meraki_device_info
labels:
serial: item.serial
name: "default(item.name, '')"
model: "default(item.model, '')"
(meraki_device_up == 0) * on (serial) group_left (name, model) meraki_device_info
Recording real responses¶
tapyr record runs jobs once against the real API, through the budget engine, and writes their responses anonymized:
tapyr record --config ./config --job devices --out ./recording [--anonymize ./anonymize.yaml]
- Serials, MAC and IP addresses, identifiers, names, emails, phone numbers, coordinates and addresses are replaced by consistent pseudonyms that keep their format (a MAC stays a MAC, an identifier keeps its length and its digits); the same value gets the same pseudonym in every page and in the request variables, so the recording stays consistent.
anonymize.yamladds rules and exceptions:
replace:
- { path: "data[*].company_name", as: name } # serial, digits, mac, ipv4, ipv6, email, hostname, name, id, coordinate, address, redact
keep:
- "data[*].model"
report.txtlists every replaced path and rule, never a value. Review the recording before committing anything: free-text fields can still hold identifying data.- Only the quota headers of the provider and
Retry-Afterare recorded; request credentials never are.
The teltonika-rms fixtures follow the shape of such a recording.
Linting and contract tests¶
tapyr provider lint providers/teltonika-rms # one package
tapyr provider lint providers # every package of a directory
tapyr provider lint --update providers/my-api # accept the new samples
The lint loads the package, compiles every template, checks the dashboards (valid JSON, a datasource variable, only metrics of the package or tapyr_ self-metrics), the examples, the required scenarios, then runs the contract test: the jobs of tests/contract.yaml run once against the simulator, on a fake clock starting at 2026-01-01T00:00:00Z, and their samples must equal tests/expected.prom. Every template must be exercised. make lint and the CI lint the built-in packages.
End-to-end tests¶
The packages tested against real platforms have e2e tests in test/e2e/ (build tag e2e), skipped unless their credentials are set:
TAPYR_E2E_RMS_TOKEN=... make e2e
TAPYR_E2E_RUTOS_URL=https://192.0.2.10 TAPYR_E2E_RUTOS_USERNAME=... TAPYR_E2E_RUTOS_PASSWORD=... make e2e
They run tapyr run for one or two minutes and spend a few requests.
Auth plugins¶
When an auth flow cannot be declared (Aruba Central rotates its refresh tokens), it is written in Go under internal/auth/plugins/<name> and registered in internal/auth/plugins. A plugin validates its auth.plugin.config (secret references as {secret: name}), creates an authenticator per account with a sender of auth requests through the budget engine and a sealed store for its state, and provides its simulator side. See 05-collection.md.