openvibers/openvibe.host Commits

Host reports machines with the Link contract and route (#354)

OP OpenVibers committed 4 days ago · 5f934071cf · 11 files, +42 −30

* Host reports machines with the Link contract and route

Network renamed the registry capability and route in the Link rename. Host still asked for network.node.report and posted to /internal/nodes/report, so its report could not be authorized or delivered. It now uses network.link.report, /internal/links/report, the network.link contracts and /api/v1/links, pinned to Contracts v0.372.0. A guard test covers the old brand, binary name and ids.

* Host's documented pins follow Contracts v0.372.0

The docs-status check reads the README versions block and STATUS.json against the installed pins.

Changes

ddiff --git a/README.md b/README.md
iindex bf5fde7..808663b 100644
---- a/README.md
++++ b/README.md
@@@ -389,7 +389,7 @@ the owner's gates (go-ahead, a DMCA agent, a Public Suffix List entry for openvi
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
--- openvibe-contracts: v0.338.0
++- openvibe-contracts: v0.372.0
- openvibe-sdk: v0.125.0
- openvibe-shared: v4.173.0
- openvibe-publishing: v1.4.0
ddiff --git a/STATUS.json b/STATUS.json
iindex 5971f7d..b43d900 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -13,11 +13,11 @@
"runtime": "node 22; Express 4 + PostgreSQL 18 (openvibe-sdk/db, async) + optional Valkey",
"port": 4910,
"domain": "openvibe.host",
-- "contracts": "openvibe-contracts v0.338.0",
++ "contracts": "openvibe-contracts v0.372.0",
"sdk": "openvibe-sdk v0.125.0",
"shared": "openvibe-shared v4.173.0",
"packages": [
-- "openvibe-contracts v0.338.0",
++ "openvibe-contracts v0.372.0",
"openvibe-sdk v0.125.0",
"openvibe-shared v4.173.0"
],
ddiff --git a/deploy/systemd/openvibe-nodes.service b/deploy/systemd/openvibe-nodes.service
iindex 82127d5..3b7e01a 100644
---- a/deploy/systemd/openvibe-nodes.service
++++ b/deploy/systemd/openvibe-nodes.service
@@@ -1,5 +1,5 @@
[Unit]
--Description=OpenVibe node registry: this inventory's machines and their measured health, reported to OpenVibe.Network
++Description=OpenVibe Link registry: this inventory's machines and their measured health, reported to OpenVibe.Network
Documentation=https://github.com/OpenVibers/OpenVibe.Host/blob/main/docs/nodes.md
After=network-online.target
Wants=network-online.target
ddiff --git a/deploy/systemd/openvibe-nodes.timer b/deploy/systemd/openvibe-nodes.timer
iindex 43498ee..1bf1321 100644
---- a/deploy/systemd/openvibe-nodes.timer
++++ b/deploy/systemd/openvibe-nodes.timer
@@@ -1,5 +1,5 @@
[Unit]
--Description=Report this inventory's machines to the OpenVibe node registry every five minutes (WS-X1)
++Description=Report this inventory's machines to the OpenVibe Link registry every five minutes (WS-X1)
[Timer]
OnBootSec=3min
ddiff --git a/docs/nodes.md b/docs/nodes.md
iindex 1fd809c..8232fd3 100644
---- a/docs/nodes.md
++++ b/docs/nodes.md
@@@ -4,21 +4,21 @@
- It is roadmap WS-X1 step 3 and ADR-034 section 12.
- `openvibe-nodes.timer` runs it every five minutes.
--- Products read the result at `GET https://openvibe.network/api/v1/nodes`, which returns `network.node-list-result@1`. It can be filtered with `?role=` and `?region=`. `openvibe-sdk/geo` uses it to pick the nearest node for a role, for example a game-server list or a ping tool with several locations.
++- Products read the result at `GET https://openvibe.network/api/v1/links`, which returns `network.link-list-result@1`. It can be filtered with `?role=` and `?region=`. `openvibe-sdk/geo` uses it to pick the nearest node for a role, for example a game-server list or a ping tool with several locations.
## The inventory
`/etc/openvibe/host.json` holds two keys for this:
- **`nodeSource`** names this inventory. The default is `primary`.
--- **`nodes`** lists each machine's public description, which is `network.node@1` without `health` and `updated_at`.
++- **`nodes`** lists each machine's public description, which is `network.link@1` without `health` and `updated_at`.
```json
"nodeSource": "oregon",
"nodes": [
{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"],
"location": { "region": "us-west", "country": "US" }, "provider": "ovh",
-- "beacon": "https://openvibe.network/api/v1/nodes/oregon-1/beacon" }
++ "beacon": "https://openvibe.network/api/v1/links/oregon-1/beacon" }
]
```
@@@ -44,6 +44,6 @@ sudo cp /usr/local/lib/openvibe-host/deploy/systemd/openvibe-nodes.{service,time
sudo systemctl daemon-reload && sudo systemctl enable --now openvibe-nodes.timer
```
--- **Credentials:** it uses Host's principal from `/etc/openvibe/host.env`, the same one `ovhost alerts relay` uses, with the scope `network.node.report`. Neither the secret nor the token is ever printed.
++- **Credentials:** it uses Host's principal from `/etc/openvibe/host.env`, the same one `ovhost alerts relay` uses, with the scope `network.link.report`. Neither the secret nor the token is ever printed.
- **Metrics:** each run writes `openvibe_nodes_report.prom` to the textfile collector.
- **Alerts:** `OpenVibeNodeReportStale` fires when no report has reached Network for an hour. `OpenVibeNodeDown` fires when a beacon has failed for 15 minutes.
ddiff --git a/lib/inventory.js b/lib/inventory.js
iindex fda6451..6fa109d 100644
---- a/lib/inventory.js
++++ b/lib/inventory.js
@@@ -538,7 +538,7 @@ function normaliseDrill(raw, svc, where) {
};
}
--/** The public description of each machine (network.node@1 without health; lib/nodes.js validates the rest). */
++/** The public description of each machine (network.link@1 without health; lib/nodes.js validates the rest). */
function normaliseNodes(raw) {
if (raw == null) return [];
if (!Array.isArray(raw)) throw new InventoryError('nodes must be an array');
ddiff --git a/lib/nodes.js b/lib/nodes.js
iindex 66e92b0..84b4518 100644
---- a/lib/nodes.js
++++ b/lib/nodes.js
@@@ -1,10 +1,10 @@
'use strict';
/**
* `ovhost nodes report` (roadmap WS-X1 step 3, ADR-034 §12): the machines this inventory knows, with their health
-- * now, sent to Network's node registry (POST /internal/nodes/report, network.node.report, Host's principal).
-- * openvibe-nodes.timer runs it every five minutes; products read the result at GET /api/v1/nodes.
++ * now, sent to Network's node registry (POST /internal/links/report, network.link.report, Host's principal).
++ * openvibe-nodes.timer runs it every five minutes; products read the result at GET /api/v1/links.
*
-- * The inventory's "nodes" array holds each machine's public description (network.node@1 without health and
++ * The inventory's "nodes" array holds each machine's public description (network.link@1 without health and
* updated_at: id, name, roles, region-level location, provider, beacon); "nodeSource" names this inventory (default
* "primary"). Health is measured, never assumed: each node's beacon is fetched; a 2xx within 2 s is up, a slower 2xx
* degraded, anything else down. A node removed from the inventory is marked down by Network, not deleted.
@@@ -25,7 +25,7 @@ async function measure(exec, beacon) {
return { status: ms > SLOW_MS ? 'degraded' : 'up', detail: `${ms} ms` };
}
--/** The report body (network.node-report-request@1) from the inventory, with measured health. */
++/** The report body (network.link-report-request@1) from the inventory, with measured health. */
async function build(exec, inv) {
const source = inv.nodeSource || 'primary';
if (!SOURCE_RE.test(source)) throw new Error(`nodeSource "${source}" must match ${SOURCE_RE}`);
@@@ -38,8 +38,8 @@ async function build(exec, inv) {
nodes.push({ ...n, health: { status: m.status, checked_at: at }, updated_at: at });
}
const body = { source, nodes };
-- const v = require('openvibe-contracts').validate('network.node-report-request@1', body);
-- if (!v.valid) throw new Error(`the inventory's nodes do not match network.node-report-request@1: ${(v.errors || []).slice(0, 3).map((e) => `${e.path} ${e.message}`).join('; ')}`);
++ const v = require('openvibe-contracts').validate('network.link-report-request@1', body);
++ if (!v.valid) throw new Error(`the inventory's nodes do not match network.link-report-request@1: ${(v.errors || []).slice(0, 3).map((e) => `${e.path} ${e.message}`).join('; ')}`);
return { body, probes };
}
@@@ -77,12 +77,12 @@ async function report(ctx, { envFile = null, env = {}, dryRun = false, metrics =
if (dryRun) return { ok: true, dryRun: true, source: body.source, probes, body };
const creds = await loadCredentials(exec, inv, { file: envFile, env });
if (creds.error) return done({ ok: false, stage: 'credentials', source: body.source, probes, error: creds.error });
-- const form = new URLSearchParams({ grant_type: 'client_credentials', client_id: creds.clientId, client_secret: creds.secret, audience: 'openvibe.network', scope: 'network.node.report' });
++ const form = new URLSearchParams({ grant_type: 'client_credentials', client_id: creds.clientId, client_secret: creds.secret, audience: 'openvibe.network', scope: 'network.link.report' });
const t = await exec.request(`${creds.networkUrl}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, body: form.toString(), timeoutMs: 5000 });
let token = null;
try { token = t.status === 200 ? JSON.parse(t.body).access_token : null; } catch { token = null; }
if (typeof token !== 'string' || !token) return done({ ok: false, stage: 'token', source: body.source, probes, error: t.status ? `the token endpoint answered ${t.status}` : `Network unreachable (${t.error || 'no answer'})` });
-- const r = await exec.request(`${creds.networkUrl}/internal/nodes/report`, {
++ const r = await exec.request(`${creds.networkUrl}/internal/links/report`, {
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(body), timeoutMs: 10000,
});
let result = null;
ddiff --git a/package-lock.json b/package-lock.json
iindex b87fae7..c32fe19 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
@@@ -1534,9 +1534,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.338.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
-- "integrity": "sha512-75FD6IxMsXoud3NFXKDs0/D0rhJtF2D+WL8M92ArIyabkUnM/sZvw6E6L97EqwQ3O7M/kB+uwsNSxGudbil+ng==",
++ "version": "0.372.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
++ "integrity": "sha512-HnfBnkhfMXYNbW4NnpTAMqZc0uV9d4XvwA20VaZXZFbBTPZiClYfoZ0/TWWWh24BFANx9TSzC+4jE0WDHKuNhA==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 707dee6..6638b4f 100644
---- a/package.json
++++ b/package.json
@@@ -31,7 +31,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
ddiff --git a/test/link-brand.test.js b/test/link-brand.test.js
nnew file mode 100644
iindex 0000000..770c55b
---- /dev/null
++++ b/test/link-brand.test.js
@@@ -0,0 +1,12 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { spawnSync } = require('node:child_process');
++const path = require('node:path');
++const root = path.resolve(__dirname, '..');
++const oldName = ['OpenVibe[ .-]No' + 'de', 'openvibe-' + 'node', 'no' + 'd_', 'node[.]openvibe[.]network', 'node:lnk_', 'network[.]node', 'platform[.]node'].join('|');
++const result = spawnSync('git', ['grep', '-n', '-I', '-i', '-E', oldName, '--', '.', ':!migrations/**', ':!test/link-brand.test.js'], { cwd: root, encoding: 'utf8' });
++assert.ok(result.status === 0 || result.status === 1, result.stderr || 'git grep failed');
++// `openvibe-nodes` is Host's own registry-sync unit and `platform.nodes` is the inventory's list.
++const stale = (result.stdout || '').split('\n').filter((line) => line && !/openvibe-nodes|platform[.]nodes/i.test(line));
++assert.equal(stale.length, 0, `Old Link brand or principal returned:\n${stale.slice(0, 10).join('\n')}`);
++console.log(' ok old Link brand and principal IDs are absent');
ddiff --git a/test/nodes.test.js b/test/nodes.test.js
iindex 7bdaced..251d20a 100644
---- a/test/nodes.test.js
++++ b/test/nodes.test.js
@@@ -1,8 +1,8 @@
'use strict';
/**
* The node registry reporter (lib/nodes.js, WS-X1 step 3): `ovhost nodes report` measures each inventory node's
-- * beacon (2xx fast = up, 2xx slow = degraded, anything else = down), sends network.node-report-request@1 to
-- * Network with Host's token (network.node.report), never shows a secret, and leaves openvibe_nodes_report.prom.
++ * beacon (2xx fast = up, 2xx slow = degraded, anything else = down), sends network.link-report-request@1 to
++ * Network with Host's token (network.link.report), never shows a secret, and leaves openvibe_nodes_report.prom.
* The inventory refuses addresses and duplicate ids: the registry is public.
*/
const assert = require('assert');
@@@ -14,7 +14,7 @@ const TOKEN = 'tok_NODES_TOKEN_MUST_NEVER_APPEAR_9c2f';
const HOST_SECRET = `${SECRET}-host`;
const PROM_FILE = '/var/lib/prometheus/node-exporter/openvibe_nodes_report.prom';
const NODES = [
-- { id: 'oregon-1', name: 'Oregon 1', roles: ['web', 'app', 'data', 'ingest'], location: { region: 'us-west', country: 'US', city: 'Hillsboro' }, provider: 'ovh', beacon: 'https://openvibe.network/api/v1/nodes/oregon-1/beacon' },
++ { id: 'oregon-1', name: 'Oregon 1', roles: ['web', 'app', 'data', 'ingest'], location: { region: 'us-west', country: 'US', city: 'Hillsboro' }, provider: 'ovh', beacon: 'https://openvibe.network/api/v1/links/oregon-1/beacon' },
{ id: 'probe-fra', name: 'Frankfurt probe', roles: ['edge-probe'], location: { region: 'eu-central', country: 'DE' }, beacon: 'https://fra.example.test/beacon' },
{ id: 'probe-sgp', name: 'Singapore probe', roles: ['edge-probe'], location: { region: 'ap-southeast', country: 'SG' }, beacon: 'https://sgp.example.test/beacon' },
];
@@@ -37,7 +37,7 @@ function withNetwork({ network = 'ok' } = {}) {
if (form.client_secret !== HOST_SECRET) return { status: 401, body: { error: 'invalid_client' } };
return { status: 200, body: { access_token: TOKEN, token_type: 'Bearer', expires_in: 300 } };
});
-- host.http.set('http://127.0.0.1:4000/internal/nodes/report', ({ method, headers, body }) => {
++ host.http.set('http://127.0.0.1:4000/internal/links/report', ({ method, headers, body }) => {
assert.strictEqual(method, 'POST');
assert.strictEqual(headers.Authorization, `Bearer ${TOKEN}`);
const doc = JSON.parse(body);
@@@ -64,7 +64,7 @@ runTests([
assert.throws(() => normalise({ services: {}, nodes: [{ ...NODES[0], beacon: 'http://x.test/b' }] }), /https:\/\//);
assert.throws(() => normalise({ services: {}, nodes: {} }), /must be an array/);
}),
-- test('nodes report: beacons measured, the complete set sent with network.node.report, metrics written', async () => {
++ test('nodes report: beacons measured, the complete set sent with network.link.report, metrics written', async () => {
const host = withNetwork();
const r = await host.cli('nodes', 'report');
assert.strictEqual(r.code, 0, r.out);
@@@ -72,9 +72,9 @@ runTests([
assert.match(r.out, /up +oregon-1 +\d+ ms/);
assert.match(r.out, /degraded +probe-fra +2500 ms/);
assert.match(r.out, /down +probe-sgp +beacon answered 502/);
-- assert.deepStrictEqual(host.tokenRequests.map((t) => [t.grant_type, t.client_id, t.audience, t.scope]), [['client_credentials', 'host', 'openvibe.network', 'network.node.report']]);
++ assert.deepStrictEqual(host.tokenRequests.map((t) => [t.grant_type, t.client_id, t.audience, t.scope]), [['client_credentials', 'host', 'openvibe.network', 'network.link.report']]);
const doc = host.delivered[0];
-- assert.ok(contracts.validate('network.node-report-request@1', doc).valid);
++ assert.ok(contracts.validate('network.link-report-request@1', doc).valid);
assert.deepStrictEqual(doc.nodes.map((n) => [n.id, n.health.status]), [['oregon-1', 'up'], ['probe-fra', 'degraded'], ['probe-sgp', 'down']]);
const prom = host.read(PROM_FILE);
assert.match(prom, /openvibe_nodes_report_last_run_ok 1/);