@tetherto/mdk-core 0.0.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +572 -0
- package/index.js +519 -0
- package/lib/address-policy.js +124 -0
- package/lib/local-discovery.js +321 -0
- package/lib/services/actions.service.js +68 -0
- package/lib/services/alerts.service.js +207 -0
- package/lib/services/comments.service.js +118 -0
- package/lib/services/log-history.service.js +307 -0
- package/lib/services/logs.service.js +249 -0
- package/lib/services/pool-utils/constants.js +20 -0
- package/lib/services/pool-utils/time.js +49 -0
- package/lib/services/pool.service.js +397 -0
- package/lib/services/provisioning.service.js +420 -0
- package/lib/services/settings.service.js +26 -0
- package/lib/services/snaps.service.js +181 -0
- package/lib/services/stats.service.js +205 -0
- package/lib/stats/README.md +80 -0
- package/lib/stats/index.js +57 -0
- package/lib/stats/ops/alerts_aggr.js +40 -0
- package/lib/stats/ops/alerts_group_cnt.js +43 -0
- package/lib/stats/ops/arr_concat.js +27 -0
- package/lib/stats/ops/array_obj_calc.js +41 -0
- package/lib/stats/ops/avg.js +42 -0
- package/lib/stats/ops/cnt.js +19 -0
- package/lib/stats/ops/group.js +38 -0
- package/lib/stats/ops/group_avg.js +53 -0
- package/lib/stats/ops/group_cnt.js +38 -0
- package/lib/stats/ops/group_max.js +40 -0
- package/lib/stats/ops/group_multiple_stats.js +40 -0
- package/lib/stats/ops/group_sum.js +40 -0
- package/lib/stats/ops/index.js +35 -0
- package/lib/stats/ops/nested_obj_concat.js +54 -0
- package/lib/stats/ops/obj_concat.js +33 -0
- package/lib/stats/ops/sum.js +27 -0
- package/lib/stats/utils.js +99 -0
- package/lib/templates/alerts.js +128 -0
- package/lib/templates/stats.js +476 -0
- package/lib/things/constants.js +65 -0
- package/lib/things/container.js +84 -0
- package/lib/things/miner.js +190 -0
- package/lib/things/powermeter.js +30 -0
- package/lib/things/sensor.js +18 -0
- package/lib/things/thing.js +93 -0
- package/lib/utils.js +603 -0
- package/lib/worker-infra.js +221 -0
- package/package.json +64 -1
- package/services.js +246 -0
- package/utils/compose-yaml.js +56 -0
- package/utils/constants.js +30 -0
- package/utils/index.js +29 -0
- package/utils/initialize.js +148 -0
- package/utils/service-bootstrap.js +271 -0
- package/worker.js +9 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Same-device worker discovery by direct key handoff — an optional, faster
|
|
4
|
+
// alternative to DHT topic discovery (the default) for setups where every
|
|
5
|
+
// component runs on one machine.
|
|
6
|
+
//
|
|
7
|
+
// Each worker publishes its stable (seed-derived) RPC public key to a shared
|
|
8
|
+
// keys dir; the Kernel reads each key and connects BY KEY, running the normal
|
|
9
|
+
// identity → capability → Ready flow. This skips the DHT topic announce/lookup
|
|
10
|
+
// round-trip, so registration is immediate as soon as the key file appears.
|
|
11
|
+
//
|
|
12
|
+
// This is the `discovery: { mode: 'local' }` path. The dir is resolved the same
|
|
13
|
+
// way on both sides — getKernel and startWorker default it to keysDir(root) and
|
|
14
|
+
// share the same root default — so the two processes agree with no config.
|
|
15
|
+
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const fs = require('fs')
|
|
18
|
+
const crypto = require('crypto')
|
|
19
|
+
const debug = require('debug')('mdk:local-discovery')
|
|
20
|
+
|
|
21
|
+
function keysDir (root) {
|
|
22
|
+
return path.join(root, '.worker-keys')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WORKER_KEY_FILE = /^[A-Za-z0-9._-]+\.key$/
|
|
26
|
+
|
|
27
|
+
// Worker side: publish this worker's RPC public key to `dir`. The key is stable
|
|
28
|
+
// across restarts (derived from the persisted seedRpc), so re-publishing is a
|
|
29
|
+
// no-op for the Kernel (already-known peers are deduped by the listener).
|
|
30
|
+
// The Kernel dials whatever it finds here, so the dir and files get the same
|
|
31
|
+
// protections as the trusted-keys dir: a safe file name, a 0700 dir, a 0600
|
|
32
|
+
// file written atomically, and a 32-byte hex key.
|
|
33
|
+
function publishWorkerKey (dir, workerId, rpcKeyHex) {
|
|
34
|
+
const hex = _keyHex(rpcKeyHex)
|
|
35
|
+
const file = path.join(dir, `${_safeName(workerId)}.key`)
|
|
36
|
+
_ensurePrivateDir(dir)
|
|
37
|
+
_writeKeyFile(file, hex)
|
|
38
|
+
debug('published %s key %s…', workerId, hex.slice(0, 16))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Kernel side: offer every published worker key to the discovery listener, then
|
|
42
|
+
// keep watching `dir` for new ones. The listener dedupes already-known peers and
|
|
43
|
+
// drops a key on connect failure (so a periodic rescan retries workers whose RPC
|
|
44
|
+
// server was not yet reachable, and re-discovers all workers after an Kernel
|
|
45
|
+
// restart). Returns stop() to tear the watcher + timer down.
|
|
46
|
+
// Only `<name>.key` files that pass the trusted-keys checks (regular file, not a
|
|
47
|
+
// symlink, owned by this user, not group/world-writable, 64-hex content) are
|
|
48
|
+
// offered; anything else in the dir is ignored.
|
|
49
|
+
function discoverWorkerKeys (kernel, dir, { rescanMs = 4000 } = {}) {
|
|
50
|
+
_ensurePrivateDir(dir)
|
|
51
|
+
|
|
52
|
+
const offer = (file) => {
|
|
53
|
+
if (typeof file !== 'string' || !WORKER_KEY_FILE.test(file)) return
|
|
54
|
+
const key = _readKeyFile(path.join(dir, file))
|
|
55
|
+
if (!key) return
|
|
56
|
+
Promise.resolve(kernel.dhtListener.discoverWorker(key))
|
|
57
|
+
.catch((e) => debug('discover %s failed: %s', file, e.message))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const scan = () => {
|
|
61
|
+
let files = []
|
|
62
|
+
try { files = fs.readdirSync(dir) } catch { return }
|
|
63
|
+
for (const f of files) offer(f)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
scan()
|
|
67
|
+
let watcher = null
|
|
68
|
+
try {
|
|
69
|
+
watcher = fs.watch(dir, (_event, filename) => offer(filename))
|
|
70
|
+
watcher.on('error', (err) => {
|
|
71
|
+
debug('watch %s failed: %s', dir, err.message)
|
|
72
|
+
try { watcher.close() } catch {}
|
|
73
|
+
watcher = null
|
|
74
|
+
})
|
|
75
|
+
} catch (err) {
|
|
76
|
+
debug('watch %s failed: %s', dir, err.message)
|
|
77
|
+
}
|
|
78
|
+
const timer = setInterval(scan, rescanMs)
|
|
79
|
+
timer.unref()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
stop () {
|
|
83
|
+
if (watcher) {
|
|
84
|
+
try { watcher.close() } catch {}
|
|
85
|
+
watcher = null
|
|
86
|
+
}
|
|
87
|
+
clearInterval(timer)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Trusted caller keys (fail-closed RPC admission) ──────────────────────
|
|
93
|
+
//
|
|
94
|
+
// The Kernel HRPC listener and every Worker RPC server reject unknown public
|
|
95
|
+
// keys. Same-machine processes that cannot hand each other a key in memory
|
|
96
|
+
// meet in a trusted-keys dir instead: each publishes `<name>.pub` (hex of the
|
|
97
|
+
// DHT key it dials with) and the other side admits what it finds there.
|
|
98
|
+
//
|
|
99
|
+
// Kernel publishes kernel.pub (kernel.getClientPublicKey(), the key it dials
|
|
100
|
+
// workers with) and admits every other key as an HRPC caller.
|
|
101
|
+
// Gateway publishes gateway.pub (status.rpcClientKey); MCP/CLI clients
|
|
102
|
+
// publish their persistent caller key the same way.
|
|
103
|
+
// Worker allows every key in the dir (the Kernel, and the Gateway, whose
|
|
104
|
+
// client also calls workers directly).
|
|
105
|
+
//
|
|
106
|
+
// Whoever can write the dir is trusted: it is created 0700, and files that are
|
|
107
|
+
// group/world-writable or owned by another user are ignored.
|
|
108
|
+
|
|
109
|
+
const KEY_HEX = /^[0-9a-f]{64}$/
|
|
110
|
+
const KERNEL_KEY_NAME = 'kernel'
|
|
111
|
+
|
|
112
|
+
function trustedKeysDir (root) {
|
|
113
|
+
return path.join(root, '.trusted-keys')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _keyHex (key) {
|
|
117
|
+
const hex = Buffer.isBuffer(key) ? key.toString('hex') : String(key || '').trim().toLowerCase()
|
|
118
|
+
if (!KEY_HEX.test(hex)) throw new Error('ERR_TRUSTED_KEY_INVALID: expected a 32-byte hex public key')
|
|
119
|
+
return hex
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _safeName (name) {
|
|
123
|
+
if (typeof name !== 'string' || !/^[A-Za-z0-9._-]+$/.test(name)) throw new Error(`ERR_TRUSTED_KEY_NAME_INVALID: ${name}`)
|
|
124
|
+
return name
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function publishTrustedKey (dir, name, key) {
|
|
128
|
+
const hex = _keyHex(key)
|
|
129
|
+
const file = path.join(dir, `${_safeName(name)}.pub`)
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
131
|
+
_writeKeyFile(file, hex)
|
|
132
|
+
debug('published trusted key %s %s…', name, hex.slice(0, 16))
|
|
133
|
+
return hex
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Creates `dir` 0700 when missing; an existing one must be a real directory
|
|
137
|
+
// (not a symlink) owned by this user and not group/world-writable.
|
|
138
|
+
function _ensurePrivateDir (dir) {
|
|
139
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
140
|
+
const st = fs.lstatSync(dir)
|
|
141
|
+
if (!st.isDirectory()) throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is not a directory`)
|
|
142
|
+
if (process.platform !== 'win32') {
|
|
143
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
|
|
144
|
+
throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is owned by another user`)
|
|
145
|
+
}
|
|
146
|
+
if (st.mode & 0o022) throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is group/world-writable`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Atomic 0600 write through an unpredictable temp name created exclusively.
|
|
151
|
+
function _writeKeyFile (file, hex) {
|
|
152
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`
|
|
153
|
+
try {
|
|
154
|
+
fs.writeFileSync(tmp, hex, { encoding: 'utf8', mode: 0o600, flag: 'wx' })
|
|
155
|
+
fs.renameSync(tmp, file)
|
|
156
|
+
} catch (err) {
|
|
157
|
+
try { fs.unlinkSync(tmp) } catch {}
|
|
158
|
+
throw err
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0
|
|
163
|
+
|
|
164
|
+
function _readKeyFile (file) {
|
|
165
|
+
let fd = null
|
|
166
|
+
try {
|
|
167
|
+
const lst = fs.lstatSync(file)
|
|
168
|
+
if (!lst.isFile()) return null
|
|
169
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | O_NOFOLLOW)
|
|
170
|
+
const st = fs.fstatSync(fd)
|
|
171
|
+
if (!st.isFile() || st.ino !== lst.ino || st.dev !== lst.dev) return null
|
|
172
|
+
if (process.platform !== 'win32') {
|
|
173
|
+
if (st.mode & 0o022) { debug('ignoring group/world-writable key file %s', file); return null }
|
|
174
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) { debug('ignoring key file owned by another user %s', file); return null }
|
|
175
|
+
}
|
|
176
|
+
const buf = Buffer.alloc(128)
|
|
177
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0)
|
|
178
|
+
const hex = buf.subarray(0, n).toString('utf8').trim().toLowerCase()
|
|
179
|
+
return KEY_HEX.test(hex) ? hex : null
|
|
180
|
+
} catch {
|
|
181
|
+
return null
|
|
182
|
+
} finally {
|
|
183
|
+
if (fd !== null) { try { fs.closeSync(fd) } catch {} }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Returns [{ name, key }] for every valid `<name>.pub` in dir.
|
|
188
|
+
function readTrustedKeys (dir) {
|
|
189
|
+
let files = []
|
|
190
|
+
try { files = fs.readdirSync(dir) } catch { return [] }
|
|
191
|
+
const out = []
|
|
192
|
+
for (const f of files) {
|
|
193
|
+
if (!f.endsWith('.pub')) continue
|
|
194
|
+
const key = _readKeyFile(path.join(dir, f))
|
|
195
|
+
if (key) out.push({ name: f.slice(0, -4), key })
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Calls onKey(hex, name) once per new key now and whenever one appears.
|
|
201
|
+
function watchTrustedKeys (dir, onKey, { rescanMs = 2000 } = {}) {
|
|
202
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
203
|
+
const seen = new Set()
|
|
204
|
+
const scan = () => {
|
|
205
|
+
for (const { name, key } of readTrustedKeys(dir)) {
|
|
206
|
+
if (seen.has(key)) continue
|
|
207
|
+
seen.add(key)
|
|
208
|
+
try { onKey(key, name) } catch (err) { debug('trusted key %s rejected: %s', name, err.message) }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
scan()
|
|
212
|
+
let watcher = null
|
|
213
|
+
try {
|
|
214
|
+
watcher = fs.watch(dir, () => scan())
|
|
215
|
+
watcher.on('error', () => { try { watcher.close() } catch {} watcher = null })
|
|
216
|
+
} catch {}
|
|
217
|
+
const timer = setInterval(scan, rescanMs)
|
|
218
|
+
timer.unref()
|
|
219
|
+
return {
|
|
220
|
+
stop () {
|
|
221
|
+
if (watcher) { try { watcher.close() } catch {} watcher = null }
|
|
222
|
+
clearInterval(timer)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Kernel side: admit every published caller key (full access) except its own.
|
|
228
|
+
function admitTrustedKeys (kernel, dir, opts) {
|
|
229
|
+
const own = kernel.getClientPublicKey && kernel.getClientPublicKey()
|
|
230
|
+
const ownHex = own ? own.toString('hex') : null
|
|
231
|
+
return watchTrustedKeys(dir, (key, name) => {
|
|
232
|
+
if (key === ownHex) return
|
|
233
|
+
kernel.allowKey(key)
|
|
234
|
+
debug('kernel admitted trusted caller %s', name)
|
|
235
|
+
}, opts)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Worker side (separate process): allow every published key. Call before
|
|
239
|
+
// runtime.start() — it throws when nothing is trusted yet.
|
|
240
|
+
function trustKeysFromDir (runtime, dir, opts) {
|
|
241
|
+
return watchTrustedKeys(dir, (key, name) => {
|
|
242
|
+
runtime.allowKey(key)
|
|
243
|
+
debug('worker %s trusts %s', runtime.workerId, name)
|
|
244
|
+
}, opts)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Wait until the Kernel has published its client key to dir.
|
|
248
|
+
async function waitForKernelKey (dir, { timeoutMs = 30000, intervalMs = 250 } = {}) {
|
|
249
|
+
const deadline = Date.now() + timeoutMs
|
|
250
|
+
for (;;) {
|
|
251
|
+
const hit = readTrustedKeys(dir).find((k) => k.name === KERNEL_KEY_NAME)
|
|
252
|
+
if (hit) return hit.key
|
|
253
|
+
if (Date.now() >= deadline) {
|
|
254
|
+
throw new Error(`ERR_KERNEL_CLIENT_KEY_NOT_FOUND: no ${KERNEL_KEY_NAME}.pub in ${dir} — start the Kernel first (it publishes the key its workers must allow), or pass allowedKeys explicitly`)
|
|
255
|
+
}
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Worker side (same process as the Kernel): allow the Kernel's client key and
|
|
261
|
+
// every write-capable caller it admits, now and later. Call before start().
|
|
262
|
+
function trustKernel (runtime, kernel) {
|
|
263
|
+
const client = kernel.getClientPublicKey()
|
|
264
|
+
if (!client) throw new Error('ERR_KERNEL_CLIENT_KEY_UNAVAILABLE: start the Kernel (with HRPC) before its workers')
|
|
265
|
+
runtime.allowKey(client)
|
|
266
|
+
for (const key of kernel.listAllowedKeys ? kernel.listAllowedKeys('write') : []) runtime.allowKey(key)
|
|
267
|
+
const onAllowed = ({ key, roles }) => {
|
|
268
|
+
if ((roles || []).some((r) => r === 'write' || r === 'admin')) runtime.allowKey(key)
|
|
269
|
+
}
|
|
270
|
+
const onRevoked = ({ key }) => { try { runtime.revokeKey(key) } catch {} }
|
|
271
|
+
kernel.on('caller-allowed', onAllowed)
|
|
272
|
+
kernel.on('caller-revoked', onRevoked)
|
|
273
|
+
return {
|
|
274
|
+
stop () {
|
|
275
|
+
kernel.removeListener('caller-allowed', onAllowed)
|
|
276
|
+
kernel.removeListener('caller-revoked', onRevoked)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// A persistent caller identity (MCP server, CLI client): an ed25519 keypair
|
|
282
|
+
// in hyperdht's format derived from a 32-byte seed kept at <dir>/seed (0600).
|
|
283
|
+
function callerKeyPair (dir) {
|
|
284
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
285
|
+
const seedPath = path.join(dir, 'seed')
|
|
286
|
+
let seed = null
|
|
287
|
+
try {
|
|
288
|
+
const buf = fs.readFileSync(seedPath)
|
|
289
|
+
if (buf.length === 32) seed = buf
|
|
290
|
+
else throw new Error(`ERR_CALLER_SEED_MALFORMED: ${seedPath}`)
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (err.code !== 'ENOENT') throw err
|
|
293
|
+
}
|
|
294
|
+
if (!seed) {
|
|
295
|
+
seed = crypto.randomBytes(32)
|
|
296
|
+
fs.writeFileSync(seedPath, seed, { mode: 0o600, flag: 'wx' })
|
|
297
|
+
}
|
|
298
|
+
const priv = crypto.createPrivateKey({
|
|
299
|
+
key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]),
|
|
300
|
+
format: 'der',
|
|
301
|
+
type: 'pkcs8'
|
|
302
|
+
})
|
|
303
|
+
const publicKey = crypto.createPublicKey(priv).export({ format: 'der', type: 'spki' }).subarray(-32)
|
|
304
|
+
return { publicKey, secretKey: Buffer.concat([seed, publicKey]) }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
keysDir,
|
|
309
|
+
publishWorkerKey,
|
|
310
|
+
discoverWorkerKeys,
|
|
311
|
+
KERNEL_KEY_NAME,
|
|
312
|
+
trustedKeysDir,
|
|
313
|
+
publishTrustedKey,
|
|
314
|
+
readTrustedKeys,
|
|
315
|
+
watchTrustedKeys,
|
|
316
|
+
admitTrustedKeys,
|
|
317
|
+
trustKeysFromDir,
|
|
318
|
+
waitForKernelKey,
|
|
319
|
+
trustKernel,
|
|
320
|
+
callerKeyPair
|
|
321
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const async = require('async')
|
|
4
|
+
const { applyFilters } = require('../utils')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Write-call approval surface for the Kernel action-approver flow
|
|
8
|
+
* (`write.calls.request`). Actions must be whitelisted with their required
|
|
9
|
+
* vote count; per-device validation is the injected
|
|
10
|
+
* validateWriteAction(device, action, params) — wire it to the vendor
|
|
11
|
+
* client's validateWriteAction. Non-whitelisted actions answer zero calls.
|
|
12
|
+
*/
|
|
13
|
+
class ActionsService {
|
|
14
|
+
constructor ({ listDevices, validateWriteAction, maxParallelWriteValidations }) {
|
|
15
|
+
this.listDevices = listDevices || (() => [])
|
|
16
|
+
this.validateWriteAction = validateWriteAction || (async () => 1)
|
|
17
|
+
this.maxParallelWriteValidations = maxParallelWriteValidations || 5
|
|
18
|
+
this.allowedActions = new Map()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
whitelistActions (actions) {
|
|
22
|
+
for (const [action, reqVotes] of actions) {
|
|
23
|
+
this.allowedActions.set(action, reqVotes)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
delistActions (actions) {
|
|
28
|
+
for (const action of actions) {
|
|
29
|
+
this.allowedActions.delete(action)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getWriteCalls (data) {
|
|
34
|
+
const { query, action, params = [], rackActionId = null } = data
|
|
35
|
+
|
|
36
|
+
const res = {
|
|
37
|
+
calls: [],
|
|
38
|
+
reqVotes: 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!this.allowedActions.has(action)) {
|
|
42
|
+
return res
|
|
43
|
+
}
|
|
44
|
+
res.reqVotes = this.allowedActions.get(action)
|
|
45
|
+
|
|
46
|
+
if (rackActionId) {
|
|
47
|
+
res.calls.push({
|
|
48
|
+
id: rackActionId,
|
|
49
|
+
tags: []
|
|
50
|
+
})
|
|
51
|
+
return res
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const devices = applyFilters(this.listDevices(), { query, limit: 100000 }, true)
|
|
55
|
+
|
|
56
|
+
await async.eachLimit(devices, this.maxParallelWriteValidations, async (dev) => {
|
|
57
|
+
await this.validateWriteAction(dev, action, params)
|
|
58
|
+
res.calls.push({
|
|
59
|
+
id: dev.id,
|
|
60
|
+
tags: dev.tags || []
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return res
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = ActionsService
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('mdk:alerts')
|
|
4
|
+
const { v4: uuidv4 } = require('uuid')
|
|
5
|
+
const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
|
|
6
|
+
|
|
7
|
+
const createAlert = ({
|
|
8
|
+
name,
|
|
9
|
+
code,
|
|
10
|
+
description,
|
|
11
|
+
severity,
|
|
12
|
+
createdAt,
|
|
13
|
+
uuid = uuidv4(),
|
|
14
|
+
message = undefined
|
|
15
|
+
}) => {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
code,
|
|
19
|
+
description,
|
|
20
|
+
severity,
|
|
21
|
+
createdAt,
|
|
22
|
+
uuid,
|
|
23
|
+
message
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const updateExistingAlerts = (alerts, alertsPrev) => {
|
|
28
|
+
if (!Array.isArray(alertsPrev)) return
|
|
29
|
+
alerts.forEach(alert => {
|
|
30
|
+
const alertExists = alertsPrev.find(val => val.name === alert.name && val.description === alert.description && val.message === alert.message)
|
|
31
|
+
alert.createdAt = alertExists?.createdAt ?? alert.createdAt
|
|
32
|
+
alert.uuid = alertExists?.uuid ?? alert.uuid
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getErrorsFromSnap = (snap) => {
|
|
37
|
+
if (!snap) return null
|
|
38
|
+
if (!snap.raw_errors) return null
|
|
39
|
+
return snap.raw_errors
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Alert evaluation + persistence. `lib` is the alerts spec module
|
|
44
|
+
* ({ specs: { <specTag>: { <alertKey>: { valid, probe } } } }) passed in
|
|
45
|
+
* directly — no loadLib indirection. `listDevices()` must return
|
|
46
|
+
* [{ id, last }] for saveAlerts; processThingAlerts takes one device record
|
|
47
|
+
* ({ id, type, info, last }).
|
|
48
|
+
*/
|
|
49
|
+
class AlertsService {
|
|
50
|
+
constructor ({ logs, lib, conf, specTags, listDevices }) {
|
|
51
|
+
if (!logs) throw new Error('ERR_ALERTS_LOGS_REQUIRED')
|
|
52
|
+
this.logs = logs
|
|
53
|
+
this.lib = lib || null
|
|
54
|
+
this.conf = conf || {}
|
|
55
|
+
this.specTags = specTags || []
|
|
56
|
+
this.listDevices = listDevices || (() => [])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async saveAlerts () {
|
|
60
|
+
const log = await this.logs.getBeeTimeLog('thing-alerts', 0, true)
|
|
61
|
+
for (const dev of this.listDevices()) {
|
|
62
|
+
if (dev.last?.alerts) {
|
|
63
|
+
try {
|
|
64
|
+
await this._storeThgAlertsToDb(dev.id, log, dev.last.alerts)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
debug('ERR_STORE_ALERTS_TO_DB thg:%s %s', dev.id, error.message)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await this.logs.releaseBeeTimeLog(log)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async _storeThgAlertsToDb (thingId, log, alerts = []) {
|
|
74
|
+
if (!Array.isArray(alerts)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const groupedAlerts = alerts.reduce((grouped, alert) => {
|
|
78
|
+
const { createdAt, uuid } = alert
|
|
79
|
+
grouped[createdAt] ??= {}
|
|
80
|
+
grouped[createdAt][uuid] = { ...alert, thingId }
|
|
81
|
+
return grouped
|
|
82
|
+
}, {})
|
|
83
|
+
|
|
84
|
+
const timestamps = Object.keys(groupedAlerts)
|
|
85
|
+
|
|
86
|
+
for (const timestamp of timestamps) {
|
|
87
|
+
const existingLogEntry = await log.get(utilsStore.convIntToBin(timestamp))
|
|
88
|
+
const existingAlerts = existingLogEntry?.value ? JSON.parse(existingLogEntry.value.toString()) : {}
|
|
89
|
+
|
|
90
|
+
const updatedAlerts = { ...existingAlerts, ...groupedAlerts[timestamp] }
|
|
91
|
+
await log.put(
|
|
92
|
+
utilsStore.convIntToBin(timestamp),
|
|
93
|
+
Buffer.from(JSON.stringify(updatedAlerts))
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
processThingAlerts (thg) {
|
|
99
|
+
if (!this.lib) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let alertsFromConfig = this.conf.thing?.alerts
|
|
104
|
+
const baseType = thg.type.split('-')[0]
|
|
105
|
+
const thingConf = this.conf.thing?.[baseType]
|
|
106
|
+
|
|
107
|
+
if (!alertsFromConfig) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
alertsFromConfig = alertsFromConfig[thg.type]
|
|
111
|
+
|
|
112
|
+
if (!alertsFromConfig) {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const specs = this.lib.specs
|
|
117
|
+
const snap = thg.last?.snap
|
|
118
|
+
|
|
119
|
+
if (!snap) {
|
|
120
|
+
const alert = createAlert({
|
|
121
|
+
name: 'error_snap',
|
|
122
|
+
code: 'error_snap',
|
|
123
|
+
description: thg.last?.err || 'No snap',
|
|
124
|
+
severity: 'medium',
|
|
125
|
+
createdAt: Date.now()
|
|
126
|
+
})
|
|
127
|
+
updateExistingAlerts([alert], thg.last?.alerts)
|
|
128
|
+
return [alert]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const acc = []
|
|
132
|
+
|
|
133
|
+
const alertsContext = {
|
|
134
|
+
conf: alertsFromConfig,
|
|
135
|
+
info: thg.info,
|
|
136
|
+
thingConf,
|
|
137
|
+
id: thg.id
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const errorsFromSnap = getErrorsFromSnap(snap)
|
|
141
|
+
|
|
142
|
+
if (errorsFromSnap) {
|
|
143
|
+
acc.push(...errorsFromSnap.map(error => {
|
|
144
|
+
const alertConfig = alertsFromConfig[error.name]
|
|
145
|
+
|
|
146
|
+
return createAlert({
|
|
147
|
+
name: error.name,
|
|
148
|
+
code: error.code,
|
|
149
|
+
description: alertConfig?.description || 'unknown_error',
|
|
150
|
+
severity: alertConfig?.severity || 'high',
|
|
151
|
+
createdAt: error.timestamp || Date.now(),
|
|
152
|
+
uuid: uuidv4(),
|
|
153
|
+
message: error.message || undefined
|
|
154
|
+
})
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.specTags.forEach(stype => {
|
|
159
|
+
if (!specs[stype]) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const aks = Object.keys(specs[stype])
|
|
164
|
+
|
|
165
|
+
aks.forEach(ak => {
|
|
166
|
+
const check = specs[stype][ak]
|
|
167
|
+
let at = null
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (!check.valid(alertsContext, snap)) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
at = check.probe(alertsContext, snap)
|
|
175
|
+
} catch (e) {
|
|
176
|
+
const alert = createAlert({
|
|
177
|
+
name: ak,
|
|
178
|
+
code: ak,
|
|
179
|
+
description: e.message,
|
|
180
|
+
severity: 'medium',
|
|
181
|
+
createdAt: Date.now()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
acc.push(alert)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (at) {
|
|
188
|
+
const alertConf = alertsFromConfig[ak]
|
|
189
|
+
const alert = createAlert({
|
|
190
|
+
name: alertConf?.name || ak,
|
|
191
|
+
code: alertConf?.code || ak,
|
|
192
|
+
description: alertConf?.description || ak,
|
|
193
|
+
severity: alertConf?.severity || 'medium',
|
|
194
|
+
createdAt: Date.now()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
acc.push(alert)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
updateExistingAlerts(acc, thg.last?.alerts)
|
|
203
|
+
return acc.length ? acc : null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = AlertsService
|