@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,420 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('mdk:provisioning')
|
|
4
|
+
const { v4: uuidv4 } = require('uuid')
|
|
5
|
+
const gLibUtilBase = require('@bitfinex/lib-js-util-base')
|
|
6
|
+
const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
|
|
7
|
+
const { applyFilters, getJsonChanges, redactSecrets, stripRedacted, isSecretKey } = require('../utils')
|
|
8
|
+
const { validateAddress, validatePort } = require('../address-policy')
|
|
9
|
+
|
|
10
|
+
// Drops every secret-named key (deep) — used when a device is repointed to a
|
|
11
|
+
// new address so the stored credentials are not sent to the new host.
|
|
12
|
+
function omitSecrets (value) {
|
|
13
|
+
if (value === null || typeof value !== 'object') return value
|
|
14
|
+
if (Array.isArray(value)) return value.map(omitSecrets)
|
|
15
|
+
const out = {}
|
|
16
|
+
for (const [k, v] of Object.entries(value)) {
|
|
17
|
+
if (isSecretKey(k)) continue
|
|
18
|
+
out[k] = omitSecrets(v)
|
|
19
|
+
}
|
|
20
|
+
return out
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// info.container / info.pos become stats group keys (groupBy('info.container'))
|
|
24
|
+
// and `container-*` / `pos-*` tags, so they are kept to short labels. The
|
|
25
|
+
// prototype-reaching names are refused even though the stats ops skip them.
|
|
26
|
+
const INFO_LABEL_MAX_LENGTH = 64
|
|
27
|
+
const INFO_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9 _.:#-]*$/
|
|
28
|
+
const INFO_LABEL_FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype'])
|
|
29
|
+
|
|
30
|
+
function validateInfoLabel (value, code) {
|
|
31
|
+
if (value === undefined || value === null || value === '') return
|
|
32
|
+
if (
|
|
33
|
+
typeof value !== 'string' ||
|
|
34
|
+
value.length > INFO_LABEL_MAX_LENGTH ||
|
|
35
|
+
!INFO_LABEL_RE.test(value) ||
|
|
36
|
+
INFO_LABEL_FORBIDDEN.has(value)
|
|
37
|
+
) {
|
|
38
|
+
throw new Error(code)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Caller-supplied tags become stats group keys and each gets its own time-log
|
|
43
|
+
// store per stats interval, so they get the same label rules and a count cap.
|
|
44
|
+
const MAX_AUX_TAGS = 32
|
|
45
|
+
|
|
46
|
+
function validateAuxTags (tags) {
|
|
47
|
+
if (!Array.isArray(tags) || tags.length > MAX_AUX_TAGS) {
|
|
48
|
+
throw new Error('ERR_THING_TAGS_INVALID')
|
|
49
|
+
}
|
|
50
|
+
for (const tag of tags) {
|
|
51
|
+
if (typeof tag !== 'string' || tag === '') throw new Error('ERR_THING_TAGS_INVALID')
|
|
52
|
+
validateInfoLabel(tag, 'ERR_THING_TAGS_INVALID')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateInfo (info) {
|
|
57
|
+
if (info === undefined || info === null) return
|
|
58
|
+
if (typeof info !== 'object' || Array.isArray(info)) {
|
|
59
|
+
throw new Error('ERR_THING_INFO_INVALID')
|
|
60
|
+
}
|
|
61
|
+
validateInfoLabel(info.container, 'ERR_THING_INFO_CONTAINER_INVALID')
|
|
62
|
+
validateInfoLabel(info.pos, 'ERR_THING_INFO_POS_INVALID')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Persisted device configs for a runtime-hosted worker. Replaces the
|
|
67
|
+
* registerThing/updateThing/forgetThings provisioning path of the manager
|
|
68
|
+
* stack: writes go to the injected bee; the boot script builds the runtime's
|
|
69
|
+
* fixed `devices` array from buildRuntimeDevices(). Changing the device set
|
|
70
|
+
* is a persisted-config change + process restart — this service never touches
|
|
71
|
+
* live connections.
|
|
72
|
+
*
|
|
73
|
+
* Record shape (kept compatible with the legacy thing record so comments,
|
|
74
|
+
* tailLog transforms and stats tags keep working):
|
|
75
|
+
* { id, type, opts, info, code, tags, comments }
|
|
76
|
+
*
|
|
77
|
+
* Caller-facing reads (getThingConf, getDeviceMeta, listDevices) return
|
|
78
|
+
* redacted copies: secret fields (password, apiKey, token, pools[].pass, ...)
|
|
79
|
+
* read as '[REDACTED]', and list filters run over that redacted projection.
|
|
80
|
+
* Remote writes validate opts.address/opts.port (see lib/address-policy.js);
|
|
81
|
+
* conf.allowedAddressCidrs is the operator allowlist that replaces the
|
|
82
|
+
* default loopback/link-local/metadata blocklist. Changing a device's address
|
|
83
|
+
* drops its stored credentials unless the update supplies new ones.
|
|
84
|
+
*/
|
|
85
|
+
class DeviceProvisioningService {
|
|
86
|
+
constructor ({ db, logs = null, deviceType, deviceTags = [], conf = {}, lookup = null }) {
|
|
87
|
+
if (!db) throw new Error('ERR_PROVISIONING_DB_REQUIRED')
|
|
88
|
+
if (!deviceType || typeof deviceType !== 'string') throw new Error('ERR_PROVISIONING_DEVICE_TYPE_REQUIRED')
|
|
89
|
+
this.db = db
|
|
90
|
+
this.logs = logs
|
|
91
|
+
this.deviceType = deviceType
|
|
92
|
+
this.deviceTags = deviceTags
|
|
93
|
+
this.conf = conf
|
|
94
|
+
this._lookup = lookup
|
|
95
|
+
this._devices = {}
|
|
96
|
+
this._initialized = false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async init () {
|
|
100
|
+
if (this._initialized) return
|
|
101
|
+
const stream = this.db.createReadStream()
|
|
102
|
+
for await (const data of stream) {
|
|
103
|
+
const entry = JSON.parse(data.value.toString())
|
|
104
|
+
this._devices[entry.id] = entry
|
|
105
|
+
}
|
|
106
|
+
this._initialized = true
|
|
107
|
+
debug('loaded %d device records (type %s)', Object.keys(this._devices).length, this.deviceType)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
generateId () {
|
|
111
|
+
return uuidv4()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getDevice (id) {
|
|
115
|
+
return this._devices[id]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
listDeviceIds () {
|
|
119
|
+
return Object.keys(this._devices)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_redactedDevices () {
|
|
123
|
+
return Object.values(this._devices).map((dev) => redactSecrets(dev))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
listDevices (req = {}) {
|
|
127
|
+
return applyFilters(this._redactedDevices(), req, true)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getDeviceMeta (id) {
|
|
131
|
+
const dev = this._devices[id]
|
|
132
|
+
if (!dev) return null
|
|
133
|
+
return redactSecrets(gLibUtilBase.pick(dev, ['id', 'tags', 'info', 'opts', 'type', 'code']))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
buildRuntimeDevices () {
|
|
137
|
+
return Object.values(this._devices).map((dev) => ({
|
|
138
|
+
deviceId: dev.id,
|
|
139
|
+
config: { ...dev.opts, type: dev.type }
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getThingConf (query = {}) {
|
|
144
|
+
if (query.requestType === 'poolConfig') return redactSecrets(this.conf.pools)
|
|
145
|
+
const dev = query.thingId ? this._devices[query.thingId] : null
|
|
146
|
+
return dev ? redactSecrets(gLibUtilBase.pick(dev, ['id', 'type', 'opts', 'info', 'code', 'tags', 'comments'])) : null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Remote-write validation of the dialable part of opts. Throws
|
|
150
|
+
// ERR_THING_ADDRESS_* / ERR_THING_PORT_INVALID.
|
|
151
|
+
async _validateOptsTarget (opts) {
|
|
152
|
+
if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
|
|
153
|
+
throw new Error('ERR_THING_VALIDATE_OPTS_INVALID')
|
|
154
|
+
}
|
|
155
|
+
if (opts.address !== undefined) {
|
|
156
|
+
await validateAddress(opts.address, {
|
|
157
|
+
allowedCidrs: this.conf.allowedAddressCidrs,
|
|
158
|
+
...(this._lookup ? { lookup: this._lookup } : {})
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
if (opts.port !== undefined) opts.port = validatePort(opts.port)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_prepDeviceTags (dev, aux, devPrev) {
|
|
165
|
+
const parts = this.deviceType.split('-')
|
|
166
|
+
parts.push('_')
|
|
167
|
+
|
|
168
|
+
let tags = []
|
|
169
|
+
|
|
170
|
+
for (let i = 1; i < parts.length; i++) {
|
|
171
|
+
tags.push(`t-${parts.slice(0, -i).join('-')}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
tags = tags.concat(this.deviceTags)
|
|
175
|
+
|
|
176
|
+
if (aux) {
|
|
177
|
+
validateAuxTags(aux)
|
|
178
|
+
tags = tags.concat(aux)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Array.isArray(devPrev?.tags)) {
|
|
182
|
+
tags = tags.concat(devPrev.tags)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
tags.push(`id-${dev.id}`)
|
|
186
|
+
tags.push(`code-${dev.code}`)
|
|
187
|
+
|
|
188
|
+
tags = tags.filter(val => !val.includes('pos-') && !val.includes('container-'))
|
|
189
|
+
if (dev.info?.pos) tags.push(`pos-${dev.info.pos}`)
|
|
190
|
+
if (dev.info?.container) tags.push(`container-${dev.info.container}`)
|
|
191
|
+
|
|
192
|
+
return gLibUtilBase.getArrayUniq(tags)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_getMaxDeviceCode () {
|
|
196
|
+
const devices = Object.values(this._devices).filter(dev => dev.code)
|
|
197
|
+
return devices.reduce((acc, cur) => {
|
|
198
|
+
const code = parseInt(cur.code.split('-').pop(), 10) || 0
|
|
199
|
+
return Math.max(acc, code)
|
|
200
|
+
}, 0)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_generateDeviceCode (seed) {
|
|
204
|
+
const prefix = this.deviceType.replace(/^[^-]+-/, '').toUpperCase()
|
|
205
|
+
const last = this._getMaxDeviceCode()
|
|
206
|
+
const nextCode = (seed ?? (last + 1)).toString().padStart(4, '0')
|
|
207
|
+
return `${prefix}-${nextCode}`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
validateRegisterThing (data) {
|
|
211
|
+
if (data.id && this._devices[data.id]) {
|
|
212
|
+
throw new Error('ERR_THING_WITH_ID_ALREADY_EXISTS')
|
|
213
|
+
}
|
|
214
|
+
if (data.code && !(/-\d+$/.test(data.code))) {
|
|
215
|
+
throw new Error('ERR_THING_CODE_INVALID')
|
|
216
|
+
}
|
|
217
|
+
if (data.code && Object.values(this._devices).some(dev => dev.code === data.code)) {
|
|
218
|
+
throw new Error('ERR_THING_WITH_CODE_ALREADY_EXISTS')
|
|
219
|
+
}
|
|
220
|
+
if (!data.opts) {
|
|
221
|
+
throw new Error('ERR_THING_VALIDATE_OPTS_INVALID')
|
|
222
|
+
}
|
|
223
|
+
validateInfo(data.info)
|
|
224
|
+
this._validateDeviceDataChange(data)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_validateDeviceDataChange (data) {
|
|
228
|
+
for (const k in this._devices) {
|
|
229
|
+
const t = this._devices[k]
|
|
230
|
+
if (t.id === data.id) continue
|
|
231
|
+
if (t.info?.serialNum && t.info.serialNum === data.info?.serialNum) {
|
|
232
|
+
throw new Error('ERR_THING_SERIALNUM_EXISTS')
|
|
233
|
+
}
|
|
234
|
+
if (t.info?.macAddress && t.info.macAddress.toLowerCase() === data.info?.macAddress?.toLowerCase()) {
|
|
235
|
+
throw new Error('ERR_THING_MACADDRESS_EXISTS')
|
|
236
|
+
}
|
|
237
|
+
if (t.info?.pos && t.info.pos === data.info?.pos && t.info?.container && t.info.container === data.info?.container) {
|
|
238
|
+
throw new Error('ERR_THING_POS_EXISTS')
|
|
239
|
+
}
|
|
240
|
+
if (!this.conf.allowDuplicateIPs && t.opts?.address && t.opts.address === data.opts?.address) {
|
|
241
|
+
throw new Error('ERR_THING_IP_ADDRESS_EXISTS')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async _storeDeviceDb (dev) {
|
|
247
|
+
await this.db.put(dev.id, Buffer.from(JSON.stringify(dev)))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} req registerThing payload ({ id?, code?, opts, info, tags, ... })
|
|
252
|
+
* @param {object} [options]
|
|
253
|
+
* @param {boolean} [options.trusted] skip the address policy — for
|
|
254
|
+
* operator-supplied seed configs only, never for remote callers.
|
|
255
|
+
*/
|
|
256
|
+
async registerThing (req, { trusted = false } = {}) {
|
|
257
|
+
this.validateRegisterThing(req)
|
|
258
|
+
req = { ...req, opts: stripRedacted(req.opts) }
|
|
259
|
+
if (!trusted) await this._validateOptsTarget(req.opts)
|
|
260
|
+
|
|
261
|
+
let info = req.info || {}
|
|
262
|
+
const comments = []
|
|
263
|
+
|
|
264
|
+
if (req.comment && req.user) {
|
|
265
|
+
comments.push({
|
|
266
|
+
ts: Date.now(),
|
|
267
|
+
comment: req.comment,
|
|
268
|
+
user: req.user
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const createdAt = Date.now()
|
|
273
|
+
info = { ...info, createdAt, updatedAt: createdAt }
|
|
274
|
+
|
|
275
|
+
const id = req.id ?? this.generateId()
|
|
276
|
+
const code = req.code ?? this._generateDeviceCode()
|
|
277
|
+
const dev = {
|
|
278
|
+
id,
|
|
279
|
+
type: this.deviceType,
|
|
280
|
+
opts: req.opts,
|
|
281
|
+
info,
|
|
282
|
+
code,
|
|
283
|
+
tags: this._prepDeviceTags({ ...req, id, code, info }, req.tags),
|
|
284
|
+
comments
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await this._storeDeviceDb(dev)
|
|
288
|
+
this._devices[id] = dev
|
|
289
|
+
|
|
290
|
+
debug('registered device %s (%s)', id, code)
|
|
291
|
+
return 1
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
checkThingExists (req) {
|
|
295
|
+
if (!req?.thingId || !this._devices[req.thingId]) {
|
|
296
|
+
throw new Error('ERR_THING_NOTFOUND')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async updateThing (req) {
|
|
301
|
+
if (!req.id || !this._devices[req.id]) {
|
|
302
|
+
throw new Error('ERR_THING_NOTFOUND')
|
|
303
|
+
}
|
|
304
|
+
validateInfo(req.info)
|
|
305
|
+
this._validateDeviceDataChange(req)
|
|
306
|
+
|
|
307
|
+
let dev = await this.db.get(req.id)
|
|
308
|
+
dev = JSON.parse(dev.value.toString())
|
|
309
|
+
const devPrev = { opts: { ...dev.opts }, info: { ...dev.info }, tags: [...(dev.tags || [])] }
|
|
310
|
+
|
|
311
|
+
if (!dev.code) {
|
|
312
|
+
dev.code = this._generateDeviceCode()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (req.opts) {
|
|
316
|
+
const reqOpts = stripRedacted(req.opts)
|
|
317
|
+
await this._validateOptsTarget(reqOpts)
|
|
318
|
+
// Repointing a device must not carry its stored credentials to the new
|
|
319
|
+
// host: keep them only when the address is unchanged or the update
|
|
320
|
+
// supplies its own.
|
|
321
|
+
const prevOpts = reqOpts.address !== undefined && reqOpts.address !== dev.opts?.address
|
|
322
|
+
? omitSecrets(dev.opts || {})
|
|
323
|
+
: dev.opts
|
|
324
|
+
dev.opts = req.forceOverwrite ? reqOpts : { ...prevOpts, ...reqOpts }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!Array.isArray(dev.comments)) {
|
|
328
|
+
dev.comments = []
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (req.comment && req.user) {
|
|
332
|
+
dev.comments.push({
|
|
333
|
+
ts: Date.now(),
|
|
334
|
+
comment: req.comment,
|
|
335
|
+
user: req.user
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (req.info) {
|
|
340
|
+
const lastActionId = req.actionId
|
|
341
|
+
dev.info = req.forceOverwrite
|
|
342
|
+
? req.info
|
|
343
|
+
: {
|
|
344
|
+
...dev.info,
|
|
345
|
+
...req.info,
|
|
346
|
+
...(lastActionId ? { lastActionId } : {})
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
dev.tags = this._prepDeviceTags(dev, req.tags, devPrev)
|
|
351
|
+
dev.info = { ...dev.info, updatedAt: Date.now() }
|
|
352
|
+
|
|
353
|
+
await this._storeInfoChanges(devPrev, dev)
|
|
354
|
+
|
|
355
|
+
await this._storeDeviceDb(dev)
|
|
356
|
+
this._devices[dev.id] = dev
|
|
357
|
+
|
|
358
|
+
return 1
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async _storeInfoChanges (devPrev, dev) {
|
|
362
|
+
if (!this.logs) return
|
|
363
|
+
const log = await this.logs.getBeeTimeLog('thing-history-log', 0, true)
|
|
364
|
+
try {
|
|
365
|
+
const now = Date.now()
|
|
366
|
+
const existingEntry = await log.get(utilsStore.convIntToBin(now))
|
|
367
|
+
const existingInfoHistory =
|
|
368
|
+
existingEntry?.value
|
|
369
|
+
? JSON.parse(existingEntry.value.toString())
|
|
370
|
+
: []
|
|
371
|
+
const updatedInfoHistory = [
|
|
372
|
+
...existingInfoHistory,
|
|
373
|
+
{
|
|
374
|
+
ts: now,
|
|
375
|
+
changes: getJsonChanges(devPrev.info, dev.info),
|
|
376
|
+
id: dev.id
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
await log.put(
|
|
380
|
+
utilsStore.convIntToBin(now),
|
|
381
|
+
Buffer.from(JSON.stringify(updatedInfoHistory))
|
|
382
|
+
)
|
|
383
|
+
} catch (e) {
|
|
384
|
+
debug('ERR_THING_SAVE_INFO_HISTORY %s', e.message)
|
|
385
|
+
}
|
|
386
|
+
await this.logs.releaseBeeTimeLog(log)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async forgetThings (req = {}) {
|
|
390
|
+
if (!req.query) {
|
|
391
|
+
req.query = { id: { $in: [] } }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (req.all) {
|
|
395
|
+
req.query = { id: { $exists: true } }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const ids = applyFilters(Object.values(this._devices), req)
|
|
399
|
+
|
|
400
|
+
for (const id of ids) {
|
|
401
|
+
await this.db.del(id)
|
|
402
|
+
delete this._devices[id]
|
|
403
|
+
debug('forgot device %s', id)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return ids.length
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async loadThing (req) {
|
|
410
|
+
const raw = await this.db.get(req.thingId)
|
|
411
|
+
return JSON.parse(raw.value.toString())
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async saveThing (dev) {
|
|
415
|
+
await this._storeDeviceDb(dev)
|
|
416
|
+
this._devices[dev.id] = dev
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = DeviceProvisioningService
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class SettingsService {
|
|
4
|
+
constructor ({ settingsDb }) {
|
|
5
|
+
if (!settingsDb) throw new Error('ERR_SETTINGS_DB_REQUIRED')
|
|
6
|
+
this.settingsDb = settingsDb
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async getSettings () {
|
|
10
|
+
const meta = await this.settingsDb.get('settings_00')
|
|
11
|
+
return meta ? JSON.parse(meta.value) : {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async saveSettingsEntries (entries) {
|
|
15
|
+
if (!entries || typeof entries !== 'object') {
|
|
16
|
+
throw new Error('ERR_ENTRIES_INVALID')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const existingSettings = await this.getSettings()
|
|
20
|
+
const newSettings = { ...existingSettings, ...entries }
|
|
21
|
+
await this.settingsDb.put('settings_00', JSON.stringify(newSettings))
|
|
22
|
+
return newSettings
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = SettingsService
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('mdk:snaps')
|
|
4
|
+
const async = require('async')
|
|
5
|
+
const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
|
|
6
|
+
const { promiseTimeout } = require('@bitfinex/lib-js-util-promise')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Periodic snapshot collection. Owns the in-memory `last` map
|
|
10
|
+
* ({ snap, alerts, err, ts } per device) that StatsService/AlertsService
|
|
11
|
+
* aggregate over, and persists snaps to `thing-5m-<deviceId>` every
|
|
12
|
+
* conf.storeSnapItvMs. The device round-trip is the injected
|
|
13
|
+
* collectSnap(deviceId); a device in maintenance
|
|
14
|
+
* (getDeviceMeta(id).info.container === 'maintenance') is not contacted.
|
|
15
|
+
*/
|
|
16
|
+
class SnapsService {
|
|
17
|
+
constructor ({ listDeviceIds, getDeviceMeta, collectSnap, processAlerts, saveAlerts, logs, conf }) {
|
|
18
|
+
if (typeof collectSnap !== 'function') throw new Error('ERR_SNAPS_COLLECT_SNAP_REQUIRED')
|
|
19
|
+
if (!logs) throw new Error('ERR_SNAPS_LOGS_REQUIRED')
|
|
20
|
+
this.listDeviceIds = listDeviceIds || (() => [])
|
|
21
|
+
this.getDeviceMeta = getDeviceMeta || ((id) => ({ id }))
|
|
22
|
+
this.collectSnap = collectSnap
|
|
23
|
+
this.processAlerts = processAlerts || (() => null)
|
|
24
|
+
this.saveAlerts = saveAlerts || (async () => {})
|
|
25
|
+
this.logs = logs
|
|
26
|
+
this.conf = conf || {}
|
|
27
|
+
this._last = {}
|
|
28
|
+
this._collecting = {}
|
|
29
|
+
this._timer = null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getLast (deviceId) {
|
|
33
|
+
return this._last[deviceId]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
listLast () {
|
|
37
|
+
return this.listDeviceIds().map((id) => ({
|
|
38
|
+
...(this.getDeviceMeta(id) || { id }),
|
|
39
|
+
last: this._last[id] || {}
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
start () {
|
|
44
|
+
if (this._timer) return
|
|
45
|
+
this._timer = setInterval(() => {
|
|
46
|
+
this.collectSnaps().catch((e) => debug('ERR_COLLECT_SNAPS %s', e.message))
|
|
47
|
+
}, this.conf.collectSnapsItvMs || 60000)
|
|
48
|
+
this._timer.unref()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stop () {
|
|
52
|
+
if (this._timer) {
|
|
53
|
+
clearInterval(this._timer)
|
|
54
|
+
this._timer = null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_getOfflineSnap () {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
stats: {
|
|
62
|
+
status: 'offline'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async _storeThingSnap ({ deviceId, thingLastCollectionTs, err, snap }) {
|
|
68
|
+
try {
|
|
69
|
+
const log = await this.logs.getBeeTimeLog(`thing-5m-${deviceId}`, 0, true)
|
|
70
|
+
const kts = utilsStore.convIntToBin(thingLastCollectionTs)
|
|
71
|
+
|
|
72
|
+
await log.put(kts, Buffer.from(JSON.stringify({
|
|
73
|
+
ts: thingLastCollectionTs,
|
|
74
|
+
err: err ? err.message : null,
|
|
75
|
+
snap
|
|
76
|
+
})))
|
|
77
|
+
|
|
78
|
+
await this.logs.releaseBeeTimeLog(log)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
debug('ERR_SAVE_SNAP %s %s', deviceId, e.message)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async _collectSnap (deviceId) {
|
|
85
|
+
if (this._collecting[deviceId]?.isCollectingSnap) return
|
|
86
|
+
|
|
87
|
+
this._collecting[deviceId] = {
|
|
88
|
+
...this._collecting[deviceId],
|
|
89
|
+
isCollectingSnap: true
|
|
90
|
+
}
|
|
91
|
+
const thingLastCollectionTs = Date.now()
|
|
92
|
+
|
|
93
|
+
if (!this._collecting[deviceId].tsThingCollectSnap) {
|
|
94
|
+
this._collecting[deviceId].tsThingCollectSnap = 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const storeSnapItvMs = this.conf.storeSnapItvMs ?? 300000
|
|
98
|
+
const shouldStore =
|
|
99
|
+
thingLastCollectionTs - this._collecting[deviceId].tsThingCollectSnap >= storeSnapItvMs
|
|
100
|
+
|
|
101
|
+
const meta = this.getDeviceMeta(deviceId) || { id: deviceId }
|
|
102
|
+
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
async.retry(
|
|
105
|
+
this.conf.collectSnapRetry || 3,
|
|
106
|
+
async () => {
|
|
107
|
+
let snap = null
|
|
108
|
+
let err = null
|
|
109
|
+
|
|
110
|
+
if (meta.info?.container === 'maintenance') {
|
|
111
|
+
snap = this._getOfflineSnap()
|
|
112
|
+
} else {
|
|
113
|
+
try {
|
|
114
|
+
snap = await promiseTimeout(
|
|
115
|
+
this.collectSnap(deviceId),
|
|
116
|
+
this.conf.collectSnapTimeoutMs || 120000
|
|
117
|
+
)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (e.message === 'ERR_PROMISE_TIMEOUT') {
|
|
120
|
+
snap = this._getOfflineSnap()
|
|
121
|
+
} else {
|
|
122
|
+
err = e
|
|
123
|
+
debug('ERR_COLLECT %s %s', deviceId, e.message)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const last = this._last[deviceId] || {}
|
|
129
|
+
last.snap = snap
|
|
130
|
+
|
|
131
|
+
if (snap) {
|
|
132
|
+
last.alerts = this.processAlerts({ ...meta, last })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
last.err = err ? err.message : null
|
|
136
|
+
last.ts = thingLastCollectionTs
|
|
137
|
+
this._last[deviceId] = last
|
|
138
|
+
|
|
139
|
+
if (shouldStore) {
|
|
140
|
+
await this._storeThingSnap({ deviceId, err, snap, thingLastCollectionTs })
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
(retryErr) => {
|
|
144
|
+
this._collecting[deviceId] = {
|
|
145
|
+
isCollectingSnap: false,
|
|
146
|
+
thingLastCollectionTs,
|
|
147
|
+
tsThingCollectSnap: shouldStore
|
|
148
|
+
? thingLastCollectionTs
|
|
149
|
+
: this._collecting[deviceId].tsThingCollectSnap
|
|
150
|
+
}
|
|
151
|
+
if (retryErr) return reject(retryErr)
|
|
152
|
+
resolve()
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async collectSnaps () {
|
|
159
|
+
await new Promise((resolve, reject) => {
|
|
160
|
+
async.eachLimit(
|
|
161
|
+
this.listDeviceIds(),
|
|
162
|
+
this.conf.thingQueryConcurrency || 25,
|
|
163
|
+
async (deviceId) => {
|
|
164
|
+
await this._collectSnap(deviceId)
|
|
165
|
+
},
|
|
166
|
+
(err) => {
|
|
167
|
+
if (err) return reject(err)
|
|
168
|
+
resolve()
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await this.saveAlerts()
|
|
175
|
+
} catch (err) {
|
|
176
|
+
debug('ERR_SAVE_ALERTS %s', err.message)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = SnapsService
|