openvibers/openvibe.host Commits
Host reports machines with the Link contract and route (#354)
Changes
@@@ -389,7 +389,7 @@ the owner's gates (go-ahead, a DMCA agent, a Public Suffix List entry for openviPart 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@@@ -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"],@@@ -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.NetworkDocumentation=https://github.com/OpenVibers/OpenVibe.Host/blob/main/docs/nodes.mdAfter=network-online.targetWants=network-online.target@@@ -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@@@ -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,timesudo 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.@@@ -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');@@@ -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;@@@ -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",@@@ -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",nnew file mode 100644@@@ -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');@@@ -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/);
@@ -389,7 +389,7 @@ the owner's gates (go-ahead, a DMCA agent, a Public Suffix List entry for openvi
@@ -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).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.338.0
+- openvibe-contracts: v0.372.0
- openvibe-sdk: v0.125.0
- openvibe-sdk: v0.125.0
- openvibe-shared: v4.173.0
- openvibe-shared: v4.173.0
- openvibe-publishing: v1.4.0
- openvibe-publishing: v1.4.0
@@ -13,11 +13,11 @@
@@ -13,11 +13,11 @@
"runtime": "node 22; Express 4 + PostgreSQL 18 (openvibe-sdk/db, async) + optional Valkey",
"runtime": "node 22; Express 4 + PostgreSQL 18 (openvibe-sdk/db, async) + optional Valkey",
"port": 4910,
"port": 4910,
"domain": "openvibe.host",
"domain": "openvibe.host",
- "contracts": "openvibe-contracts v0.338.0",
+ "contracts": "openvibe-contracts v0.372.0",
"sdk": "openvibe-sdk v0.125.0",
"sdk": "openvibe-sdk v0.125.0",
"shared": "openvibe-shared v4.173.0",
"shared": "openvibe-shared v4.173.0",
"packages": [
"packages": [
- "openvibe-contracts v0.338.0",
+ "openvibe-contracts v0.372.0",
"openvibe-sdk v0.125.0",
"openvibe-sdk v0.125.0",
"openvibe-shared v4.173.0"
"openvibe-shared v4.173.0"
],
],
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
[Unit]
[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
Documentation=https://github.com/OpenVibers/OpenVibe.Host/blob/main/docs/nodes.md
After=network-online.target
After=network-online.target
Wants=network-online.target
Wants=network-online.target
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
[Unit]
[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]
[Timer]
OnBootSec=3min
OnBootSec=3min
@@ -4,21 +4,21 @@
@@ -4,21 +4,21 @@
- It is roadmap WS-X1 step 3 and ADR-034 section 12.
- It is roadmap WS-X1 step 3 and ADR-034 section 12.
- `openvibe-nodes.timer` runs it every five minutes.
- `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
## The inventory
`/etc/openvibe/host.json` holds two keys for this:
`/etc/openvibe/host.json` holds two keys for this:
- **`nodeSource`** names this inventory. The default is `primary`.
- **`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
```json
"nodeSource": "oregon",
"nodeSource": "oregon",
"nodes": [
"nodes": [
{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"],
{ "id": "oregon-1", "name": "Oregon 1", "roles": ["web", "app", "data", "ingest"],
"location": { "region": "us-west", "country": "US" }, "provider": "ovh",
"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
@@ -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
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.
- **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.
- **Alerts:** `OpenVibeNodeReportStale` fires when no report has reached Network for an hour. `OpenVibeNodeDown` fires when a beacon has failed for 15 minutes.
@@ -538,7 +538,7 @@ function normaliseDrill(raw, svc, where) {
@@ -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) {
function normaliseNodes(raw) {
if (raw == null) return [];
if (raw == null) return [];
if (!Array.isArray(raw)) throw new InventoryError('nodes must be an array');
if (!Array.isArray(raw)) throw new InventoryError('nodes must be an array');
@@ -1,10 +1,10 @@
@@ -1,10 +1,10 @@
'use strict';
'use strict';
/**
/**
* `ovhost nodes report` (roadmap WS-X1 step 3, ADR-034 §12): the machines this inventory knows, with their health
* `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
* 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
* "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.
* 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) {
@@ -25,7 +25,7 @@ async function measure(exec, beacon) {
return { status: ms > SLOW_MS ? 'degraded' : 'up', detail: `${ms} ms` };
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) {
async function build(exec, inv) {
const source = inv.nodeSource || 'primary';
const source = inv.nodeSource || 'primary';
if (!SOURCE_RE.test(source)) throw new Error(`nodeSource "${source}" must match ${SOURCE_RE}`);
if (!SOURCE_RE.test(source)) throw new Error(`nodeSource "${source}" must match ${SOURCE_RE}`);
@@ -38,8 +38,8 @@ async function build(exec, inv) {
@@ -38,8 +38,8 @@ async function build(exec, inv) {
nodes.push({ ...n, health: { status: m.status, checked_at: at }, updated_at: at });
nodes.push({ ...n, health: { status: m.status, checked_at: at }, updated_at: at });
}
}
const body = { source, nodes };
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 };
return { body, probes };
}
}
@@ -77,12 +77,12 @@ async function report(ctx, { envFile = null, env = {}, dryRun = false, metrics =
@@ -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 };
if (dryRun) return { ok: true, dryRun: true, source: body.source, probes, body };
const creds = await loadCredentials(exec, inv, { file: envFile, env });
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 });
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 });
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;
let token = null;
try { token = t.status === 200 ? JSON.parse(t.body).access_token : null; } catch { 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'})` });
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,
method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify(body), timeoutMs: 10000,
});
});
let result = null;
let result = null;
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
@@ -1534,9 +1534,9 @@
@@ -1534,9 +1534,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -31,7 +31,7 @@
@@ -31,7 +31,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -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');
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
'use strict';
'use strict';
/**
/**
* The node registry reporter (lib/nodes.js, WS-X1 step 3): `ovhost nodes report` measures each inventory node's
* 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.
* The inventory refuses addresses and duplicate ids: the registry is public.
*/
*/
const assert = require('assert');
const assert = require('assert');
@@ -14,7 +14,7 @@ const TOKEN = 'tok_NODES_TOKEN_MUST_NEVER_APPEAR_9c2f';
@@ -14,7 +14,7 @@ const TOKEN = 'tok_NODES_TOKEN_MUST_NEVER_APPEAR_9c2f';
const HOST_SECRET = `${SECRET}-host`;
const HOST_SECRET = `${SECRET}-host`;
const PROM_FILE = '/var/lib/prometheus/node-exporter/openvibe_nodes_report.prom';
const PROM_FILE = '/var/lib/prometheus/node-exporter/openvibe_nodes_report.prom';
const NODES = [
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-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' },
{ 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' } = {}) {
@@ -37,7 +37,7 @@ function withNetwork({ network = 'ok' } = {}) {
if (form.client_secret !== HOST_SECRET) return { status: 401, body: { error: 'invalid_client' } };
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 } };
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(method, 'POST');
assert.strictEqual(headers.Authorization, `Bearer ${TOKEN}`);
assert.strictEqual(headers.Authorization, `Bearer ${TOKEN}`);
const doc = JSON.parse(body);
const doc = JSON.parse(body);
@@ -64,7 +64,7 @@ runTests([
@@ -64,7 +64,7 @@ runTests([
assert.throws(() => normalise({ services: {}, nodes: [{ ...NODES[0], beacon: 'http://x.test/b' }] }), /https:\/\//);
assert.throws(() => normalise({ services: {}, nodes: [{ ...NODES[0], beacon: 'http://x.test/b' }] }), /https:\/\//);
assert.throws(() => normalise({ services: {}, nodes: {} }), /must be an array/);
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 host = withNetwork();
const r = await host.cli('nodes', 'report');
const r = await host.cli('nodes', 'report');
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(r.code, 0, r.out);
@@ -72,9 +72,9 @@ runTests([
@@ -72,9 +72,9 @@ runTests([
assert.match(r.out, /up +oregon-1 +\d+ ms/);
assert.match(r.out, /up +oregon-1 +\d+ ms/);
assert.match(r.out, /degraded +probe-fra +2500 ms/);
assert.match(r.out, /degraded +probe-fra +2500 ms/);
assert.match(r.out, /down +probe-sgp +beacon answered 502/);
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];
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']]);
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);
const prom = host.read(PROM_FILE);
assert.match(prom, /openvibe_nodes_report_last_run_ok 1/);
assert.match(prom, /openvibe_nodes_report_last_run_ok 1/);