nodejs-poolcontroller 7.3.0 → 7.6.0
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/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/Changelog +23 -0
- package/README.md +5 -5
- package/app.ts +2 -0
- package/config/Config.ts +3 -0
- package/config/VersionCheck.ts +8 -4
- package/controller/Constants.ts +88 -0
- package/controller/Equipment.ts +246 -66
- package/controller/Errors.ts +24 -1
- package/controller/Lockouts.ts +423 -0
- package/controller/State.ts +314 -54
- package/controller/boards/EasyTouchBoard.ts +107 -59
- package/controller/boards/IntelliCenterBoard.ts +186 -125
- package/controller/boards/IntelliTouchBoard.ts +104 -30
- package/controller/boards/NixieBoard.ts +721 -159
- package/controller/boards/SystemBoard.ts +2370 -1108
- package/controller/comms/Comms.ts +85 -10
- package/controller/comms/messages/Messages.ts +10 -4
- package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
- package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
- package/controller/comms/messages/config/CoverMessage.ts +1 -0
- package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
- package/controller/comms/messages/config/ExternalMessage.ts +44 -26
- package/controller/comms/messages/config/FeatureMessage.ts +8 -1
- package/controller/comms/messages/config/GeneralMessage.ts +8 -0
- package/controller/comms/messages/config/HeaterMessage.ts +15 -9
- package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
- package/controller/comms/messages/config/OptionsMessage.ts +13 -1
- package/controller/comms/messages/config/PumpMessage.ts +4 -20
- package/controller/comms/messages/config/RemoteMessage.ts +4 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
- package/controller/comms/messages/config/SecurityMessage.ts +1 -0
- package/controller/comms/messages/config/ValveMessage.ts +13 -3
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
- package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
- package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
- package/controller/nixie/Nixie.ts +18 -16
- package/controller/nixie/bodies/Body.ts +4 -1
- package/controller/nixie/chemistry/ChemController.ts +80 -77
- package/controller/nixie/chemistry/Chlorinator.ts +9 -8
- package/controller/nixie/circuits/Circuit.ts +55 -6
- package/controller/nixie/heaters/Heater.ts +192 -32
- package/controller/nixie/pumps/Pump.ts +146 -84
- package/controller/nixie/schedules/Schedule.ts +3 -2
- package/controller/nixie/valves/Valve.ts +1 -1
- package/defaultConfig.json +32 -1
- package/issue_template.md +1 -1
- package/logger/DataLogger.ts +37 -22
- package/package.json +20 -18
- package/web/Server.ts +520 -29
- package/web/bindings/influxDB.json +96 -8
- package/web/bindings/mqtt.json +151 -40
- package/web/bindings/mqttAlt.json +114 -4
- package/web/interfaces/httpInterface.ts +2 -0
- package/web/interfaces/influxInterface.ts +36 -19
- package/web/interfaces/mqttInterface.ts +14 -3
- package/web/services/config/Config.ts +171 -44
- package/web/services/state/State.ts +49 -5
- package/web/services/state/StateSocket.ts +18 -1
package/web/Server.ts
CHANGED
|
@@ -14,43 +14,46 @@ GNU Affero General Public License for more details.
|
|
|
14
14
|
You should have received a copy of the GNU Affero General Public License
|
|
15
15
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
16
|
*/
|
|
17
|
-
import * as
|
|
17
|
+
import * as dns from "dns";
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
18
19
|
import * as fs from "fs";
|
|
19
|
-
import express = require('express');
|
|
20
|
-
import { utils } from "../controller/Constants";
|
|
21
|
-
import { config } from "../config/Config";
|
|
22
|
-
import { logger } from "../logger/Logger";
|
|
23
|
-
import { Namespace, RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
|
|
24
|
-
import { io as sockClient } from "socket.io-client";
|
|
25
|
-
import { ConfigRoute } from "./services/config/Config";
|
|
26
|
-
import { StateRoute } from "./services/state/State";
|
|
27
|
-
import { StateSocket } from "./services/state/StateSocket";
|
|
28
|
-
import { UtilitiesRoute } from "./services/utilities/Utilities";
|
|
29
|
-
import * as http2 from "http2";
|
|
30
20
|
import * as http from "http";
|
|
21
|
+
import * as http2 from "http2";
|
|
31
22
|
import * as https from "https";
|
|
32
|
-
import { state } from "../controller/State";
|
|
33
|
-
import { conn } from "../controller/comms/Comms";
|
|
34
|
-
import { Inbound, Outbound } from "../controller/comms/messages/Messages";
|
|
35
|
-
import { EventEmitter } from 'events';
|
|
36
|
-
import { sys } from '../controller/Equipment';
|
|
37
23
|
import * as multicastdns from 'multicast-dns';
|
|
38
24
|
import * as ssdp from 'node-ssdp';
|
|
39
25
|
import * as os from 'os';
|
|
26
|
+
import * as path from "path";
|
|
27
|
+
import { RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
|
|
28
|
+
import { io as sockClient } from "socket.io-client";
|
|
40
29
|
import { URL } from "url";
|
|
30
|
+
import { config } from "../config/Config";
|
|
31
|
+
import { conn } from "../controller/comms/Comms";
|
|
32
|
+
import { Inbound, Outbound } from "../controller/comms/messages/Messages";
|
|
33
|
+
import { Timestamp, utils } from "../controller/Constants";
|
|
34
|
+
import { sys } from '../controller/Equipment';
|
|
35
|
+
import { state } from "../controller/State";
|
|
36
|
+
import { logger } from "../logger/Logger";
|
|
41
37
|
import { HttpInterfaceBindings } from './interfaces/httpInterface';
|
|
42
38
|
import { InfluxInterfaceBindings } from './interfaces/influxInterface';
|
|
43
39
|
import { MqttInterfaceBindings } from './interfaces/mqttInterface';
|
|
44
|
-
import {
|
|
45
|
-
import extend = require("extend");
|
|
40
|
+
import { ConfigRoute } from "./services/config/Config";
|
|
46
41
|
import { ConfigSocket } from "./services/config/ConfigSocket";
|
|
42
|
+
import { StateRoute } from "./services/state/State";
|
|
43
|
+
import { StateSocket } from "./services/state/StateSocket";
|
|
44
|
+
import { UtilitiesRoute } from "./services/utilities/Utilities";
|
|
45
|
+
import express = require('express');
|
|
46
|
+
import extend = require("extend");
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
// This class serves data and pages for
|
|
50
50
|
// external interfaces as well as an internal dashboard.
|
|
51
51
|
export class WebServer {
|
|
52
|
+
public autoBackup = false;
|
|
53
|
+
public lastBackup;
|
|
52
54
|
private _servers: ProtoServer[] = [];
|
|
53
55
|
private family = 'IPv4';
|
|
56
|
+
private _autoBackupTimer: NodeJS.Timeout;
|
|
54
57
|
constructor() { }
|
|
55
58
|
public async init() {
|
|
56
59
|
try {
|
|
@@ -86,6 +89,7 @@ export class WebServer {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
this.initInterfaces(cfg.interfaces);
|
|
92
|
+
|
|
89
93
|
} catch (err) { logger.error(`Error initializing web server ${err.message}`) }
|
|
90
94
|
}
|
|
91
95
|
public async initInterfaces(interfaces: any) {
|
|
@@ -101,6 +105,7 @@ export class WebServer {
|
|
|
101
105
|
let type = c.type || 'http';
|
|
102
106
|
logger.info(`Init ${type} interface: ${c.name}`);
|
|
103
107
|
switch (type) {
|
|
108
|
+
case 'rest':
|
|
104
109
|
case 'http':
|
|
105
110
|
int = new HttpInterfaceServer(c.name, type);
|
|
106
111
|
int.init(c);
|
|
@@ -182,7 +187,7 @@ export class WebServer {
|
|
|
182
187
|
if (this._servers[i].uuid === uuid) this._servers.splice(i, 1);
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
|
-
public async updateServerInterface(obj: any) {
|
|
190
|
+
public async updateServerInterface(obj: any): Promise<any> {
|
|
186
191
|
let int = config.setInterface(obj);
|
|
187
192
|
let srv = this.findServerByGuid(obj.uuid);
|
|
188
193
|
// if server is not enabled; stop & remove it from local storage
|
|
@@ -197,6 +202,321 @@ export class WebServer {
|
|
|
197
202
|
}
|
|
198
203
|
else srv.init(obj);
|
|
199
204
|
}
|
|
205
|
+
return config.getInterfaceByUuid(obj.uuid);
|
|
206
|
+
}
|
|
207
|
+
public async initAutoBackup() {
|
|
208
|
+
try {
|
|
209
|
+
let bu = config.getSection('controller.backups');
|
|
210
|
+
this.autoBackup = false;
|
|
211
|
+
// These will be returned in reverse order with the newest backup first.
|
|
212
|
+
let files = await this.readBackupFiles();
|
|
213
|
+
let afiles = files.filter(elem => elem.options.automatic === true);
|
|
214
|
+
this.lastBackup = (afiles.length > 0) ? Date.parse(afiles[0].options.backupDate).valueOf() || 0 : 0;
|
|
215
|
+
// Set the last backup date.
|
|
216
|
+
this.autoBackup = utils.makeBool(bu.automatic);
|
|
217
|
+
if (this.autoBackup) {
|
|
218
|
+
let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
|
|
219
|
+
logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next Backup: ${Timestamp.toISOLocal(new Date(nextBackup))}`);
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
|
|
223
|
+
// Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
|
|
224
|
+
setTimeout(() => { this.checkAutoBackup(); }, 20000);
|
|
225
|
+
}
|
|
226
|
+
catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
|
|
227
|
+
}
|
|
228
|
+
public async stopAutoBackup() {
|
|
229
|
+
this.autoBackup = false;
|
|
230
|
+
if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
|
|
231
|
+
}
|
|
232
|
+
public async readBackupFiles(): Promise<BackupFile[]> {
|
|
233
|
+
try {
|
|
234
|
+
let backupDir = path.join(process.cwd(), 'backups');
|
|
235
|
+
let files = fs.readdirSync(backupDir);
|
|
236
|
+
let backups = [];
|
|
237
|
+
if (typeof files !== 'undefined') {
|
|
238
|
+
for (let i = 0; i < files.length; i++) {
|
|
239
|
+
let file = files[i];
|
|
240
|
+
if (path.extname(file) === '.zip') {
|
|
241
|
+
let bf = await BackupFile.fromFile(path.join(backupDir, file));
|
|
242
|
+
if (typeof bf !== 'undefined') backups.push(bf);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
backups.sort((a, b) => { return Date.parse(b.options.backupDate) - Date.parse(a.options.backupDate) });
|
|
247
|
+
return backups;
|
|
248
|
+
}
|
|
249
|
+
catch (err) { logger.error(`Error reading backup file directory: ${err.message}`); }
|
|
250
|
+
}
|
|
251
|
+
protected async extractBackupOptions(file: string | Buffer): Promise<{ file: string, options: any }> {
|
|
252
|
+
try {
|
|
253
|
+
let opts = { file: Buffer.isBuffer(file) ? 'Buffer' : file, options: {} as any };
|
|
254
|
+
let jszip = require("jszip");
|
|
255
|
+
let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
|
|
256
|
+
await jszip.loadAsync(buff).then(async (zip) => {
|
|
257
|
+
await zip.file('options.json').async('string').then((data) => {
|
|
258
|
+
opts.options = JSON.parse(data);
|
|
259
|
+
if (typeof opts.options.backupDate === 'undefined' && typeof file === 'string') {
|
|
260
|
+
let name = path.parse(file).name;
|
|
261
|
+
if (name.length === 19) {
|
|
262
|
+
let date = name.substring(0, 10).replace(/-/g, '/');
|
|
263
|
+
let time = name.substring(11).replace(/-/g, ':');
|
|
264
|
+
let dt = Date.parse(`${date} ${time}`);
|
|
265
|
+
if (!isNaN(dt)) opts.options.backupDate = Timestamp.toISOLocal(new Date(dt));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return opts;
|
|
271
|
+
} catch (err) { logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
|
|
272
|
+
}
|
|
273
|
+
public async pruneAutoBackups(keepCount: number) {
|
|
274
|
+
try {
|
|
275
|
+
// We only automatically prune backups that njsPC put there in the first place so only
|
|
276
|
+
// look at auto-backup files.
|
|
277
|
+
let files = await this.readBackupFiles();
|
|
278
|
+
let afiles = files.filter(elem => elem.options.automatic === true);
|
|
279
|
+
if (afiles.length > keepCount) {
|
|
280
|
+
// Prune off the oldest backups until we get to our keep count. When we read in the files
|
|
281
|
+
// these were sorted newest first.
|
|
282
|
+
while (afiles.length > keepCount) {
|
|
283
|
+
let afile = afiles.pop();
|
|
284
|
+
logger.info(`Pruning auto-backup file: ${afile.filePath}`);
|
|
285
|
+
try {
|
|
286
|
+
fs.unlinkSync(afile.filePath);
|
|
287
|
+
} catch (err) { logger.error(`Error deleting auto-backup file: ${afile.filePath}`); }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (err) { logger.error(`Error pruning auto-backups: ${err.message}`); }
|
|
291
|
+
}
|
|
292
|
+
public async backupServer(opts: any): Promise<BackupFile> {
|
|
293
|
+
let ret = new BackupFile();
|
|
294
|
+
ret.options = extend(true, {}, opts, { version: 1.1, errors: [] });
|
|
295
|
+
//{ file: '', options: extend(true, {}, opts, { version: 1.0, errors: [] }) };
|
|
296
|
+
let jszip = require("jszip");
|
|
297
|
+
function pad(n) { return (n < 10 ? '0' : '') + n; }
|
|
298
|
+
let zip = new jszip();
|
|
299
|
+
let ts = new Date();
|
|
300
|
+
let baseDir = process.cwd();
|
|
301
|
+
ret.filename = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + '.zip';
|
|
302
|
+
ret.filePath = path.join(baseDir, 'backups', ret.filename);
|
|
303
|
+
if (opts.njsPC === true) {
|
|
304
|
+
zip.folder('njsPC');
|
|
305
|
+
zip.folder('njsPC/data');
|
|
306
|
+
// Create the backup file and copy it into it.
|
|
307
|
+
zip.file('njsPC/config.json', fs.readFileSync(path.join(baseDir, 'config.json')));
|
|
308
|
+
zip.file('njsPC/data/poolConfig.json', fs.readFileSync(path.join(baseDir, 'data', 'poolConfig.json')));
|
|
309
|
+
zip.file('njsPC/data/poolState.json', fs.readFileSync(path.join(baseDir, 'data', 'poolState.json')));
|
|
310
|
+
}
|
|
311
|
+
if (typeof ret.options.servers !== 'undefined' && ret.options.servers.length > 0) {
|
|
312
|
+
// Back up all our servers.
|
|
313
|
+
for (let i = 0; i < ret.options.servers.length; i++) {
|
|
314
|
+
let srv = ret.options.servers[i];
|
|
315
|
+
if (typeof srv.errors === 'undefined') srv.errors = [];
|
|
316
|
+
if (srv.backup === false) continue;
|
|
317
|
+
let server = this.findServerByGuid(srv.uuid) as REMInterfaceServer;
|
|
318
|
+
if (typeof server === 'undefined') {
|
|
319
|
+
srv.errors.push(`Could not find server ${srv.name} : ${srv.uuid}`);
|
|
320
|
+
srv.success = false;
|
|
321
|
+
}
|
|
322
|
+
else if (!server.isConnected) {
|
|
323
|
+
srv.success = false;
|
|
324
|
+
srv.errors.push(`Server ${srv.name} : ${srv.uuid} not connected cannot back up`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// Try to get the data from the server.
|
|
328
|
+
zip.folder(server.name);
|
|
329
|
+
zip.file(`${server.name}/serverConfig.json`, JSON.stringify(server.cfg));
|
|
330
|
+
zip.folder(`${server.name}/data`);
|
|
331
|
+
try {
|
|
332
|
+
let resp = await server.getControllerConfig();
|
|
333
|
+
if (typeof resp !== 'undefined') {
|
|
334
|
+
if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
|
|
335
|
+
let ccfg = JSON.parse(resp.data);
|
|
336
|
+
zip.file(`${server.name}/data/controllerConfig.json`, JSON.stringify(ccfg));
|
|
337
|
+
srv.success = true;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
srv.errors.push(`Error getting controller configuration: ${resp.error.message}`);
|
|
341
|
+
srv.success = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
srv.success = false;
|
|
346
|
+
srv.errors.push(`No response from server`);
|
|
347
|
+
}
|
|
348
|
+
} catch (err) { srv.success = false; srv.errors.push(`Could not obtain server configuration`); }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
ret.options.backupDate = Timestamp.toISOLocal(ts);
|
|
353
|
+
zip.file('options.json', JSON.stringify(ret.options));
|
|
354
|
+
await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
|
|
355
|
+
fs.writeFileSync(ret.filePath, content);
|
|
356
|
+
this.lastBackup = ts.valueOf();
|
|
357
|
+
});
|
|
358
|
+
return ret;
|
|
359
|
+
}
|
|
360
|
+
public async checkAutoBackup() {
|
|
361
|
+
if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
|
|
362
|
+
this._autoBackupTimer = undefined;
|
|
363
|
+
let bu = config.getSection('controller.backups');
|
|
364
|
+
if (bu.automatic === true) {
|
|
365
|
+
if (typeof this.lastBackup === 'undefined' ||
|
|
366
|
+
(this.lastBackup < new Date().valueOf() - (bu.interval.days * 86400000) - (bu.interval.hours * 3600000))) {
|
|
367
|
+
bu.name = 'Automatic Backup';
|
|
368
|
+
await this.backupServer(bu);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else this.autoBackup = false;
|
|
372
|
+
if (this.autoBackup) {
|
|
373
|
+
await this.pruneAutoBackups(bu.keepCount);
|
|
374
|
+
let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
|
|
375
|
+
setTimeout(async () => {
|
|
376
|
+
try {
|
|
377
|
+
await this.checkAutoBackup();
|
|
378
|
+
} catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
|
|
379
|
+
}, Math.max(Math.min(nextBackup - new Date().valueOf(), 2147483647), 60000));
|
|
380
|
+
logger.info(`Last auto-backup ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next auto - backup ${Timestamp.toISOLocal(new Date(nextBackup))}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
public async validateRestore(opts): Promise<any> {
|
|
384
|
+
try {
|
|
385
|
+
let stats = { njsPC: {}, servers: [] };
|
|
386
|
+
// Step 1: Extract all the files from the zip file.
|
|
387
|
+
let rest = await RestoreFile.fromFile(opts.filePath);
|
|
388
|
+
// Step 2: Validate the njsPC data against the board. The return
|
|
389
|
+
// from here shoudld give a very detailed view of what it is about to do.
|
|
390
|
+
if (opts.options.njsPC === true) {
|
|
391
|
+
stats.njsPC = await sys.board.system.validateRestore(rest.njsPC);
|
|
392
|
+
}
|
|
393
|
+
// Step 3: For each REM server we need to validate the restore
|
|
394
|
+
// file.
|
|
395
|
+
if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
|
|
396
|
+
for (let i = 0; i < opts.options.servers.length; i++) {
|
|
397
|
+
let s = opts.options.servers[i];
|
|
398
|
+
if (s.restore) {
|
|
399
|
+
let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
|
|
400
|
+
// Check to see if the server is on-line.
|
|
401
|
+
// First, try by UUID.
|
|
402
|
+
let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
|
|
403
|
+
let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
|
|
404
|
+
// Second, try by host
|
|
405
|
+
if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
|
|
406
|
+
let srvs = this.findServersByType('rem') as REMInterfaceServer[];
|
|
407
|
+
cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
|
|
408
|
+
for (let j = 0; j < srvs.length; j++){
|
|
409
|
+
if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
|
|
410
|
+
srv = srvs[j];
|
|
411
|
+
ctx.server.warnings.push(`REM Server from backup file (${srv.uuid}/${srv.cfg.options.host}) matched to current REM Server (${cfg.uuid}/${cfg.serverConfig.options.host}) by host name or IP and not UUID. UUID in current config.json for REM will be updated.`)
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
stats.servers.push(ctx);
|
|
417
|
+
if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
|
|
418
|
+
else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
|
|
419
|
+
else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected or cannot be found by UUID and cannot restore. If this is a version 1.0 file, update your current REM UUID to match the backup REM UUID.`);
|
|
420
|
+
else {
|
|
421
|
+
let resp = await srv.validateRestore(cfg.controllerConfig);
|
|
422
|
+
if (typeof resp !== 'undefined') {
|
|
423
|
+
if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
|
|
424
|
+
let cctx = JSON.parse(resp.data);
|
|
425
|
+
ctx = extend(true, ctx, cctx);
|
|
426
|
+
}
|
|
427
|
+
else
|
|
428
|
+
ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
|
|
429
|
+
}
|
|
430
|
+
else
|
|
431
|
+
ctx.server.errors.push(`No response from server`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return stats;
|
|
439
|
+
} catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err);}
|
|
440
|
+
}
|
|
441
|
+
public async restoreServers(opts): Promise<any> {
|
|
442
|
+
let stats: { backupOptions?: any, njsPC?: RestoreResults, servers: any[] } = { servers: [] };
|
|
443
|
+
try {
|
|
444
|
+
// Step 1: Extract all the files from the zip file.
|
|
445
|
+
let rest = await RestoreFile.fromFile(opts.filePath);
|
|
446
|
+
stats.backupOptions = rest.options;
|
|
447
|
+
// Step 2: Validate the njsPC data against the board. The return
|
|
448
|
+
// from here shoudld give a very detailed view of what it is about to do.
|
|
449
|
+
if (opts.options.njsPC === true) {
|
|
450
|
+
logger.info(`Begin Restore njsPC`);
|
|
451
|
+
stats.njsPC = await sys.board.system.restore(rest.njsPC);
|
|
452
|
+
logger.info(`End Restore njsPC`);
|
|
453
|
+
}
|
|
454
|
+
// Step 3: For each REM server we need to validate the restore
|
|
455
|
+
// file.
|
|
456
|
+
if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
|
|
457
|
+
for (let i = 0; i < opts.options.servers.length; i++) {
|
|
458
|
+
let s = opts.options.servers[i];
|
|
459
|
+
if (s.restore) {
|
|
460
|
+
// Check to see if the server is on-line.
|
|
461
|
+
let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
|
|
462
|
+
let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
|
|
463
|
+
let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
|
|
464
|
+
if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
|
|
465
|
+
let srvs = this.findServersByType('rem') as REMInterfaceServer[];
|
|
466
|
+
cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
|
|
467
|
+
for (let j = 0; j < srvs.length; j++){
|
|
468
|
+
if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
|
|
469
|
+
srv = srvs[j];
|
|
470
|
+
let oldSrvCfg = srv.cfg;
|
|
471
|
+
oldSrvCfg.enabled = false;
|
|
472
|
+
await this.updateServerInterface(oldSrvCfg); // unload prev server interface
|
|
473
|
+
srv.uuid = srv.cfg.uuid = cfg.uuid;
|
|
474
|
+
config.setSection('web.interfaces.rem', cfg.serverConfig);
|
|
475
|
+
await this.updateServerInterface(cfg.serverConfig); // reset server interface
|
|
476
|
+
srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
|
|
477
|
+
logger.info(`Restore REM: Current UUID updated to UUID of backup.`);
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
stats.servers.push(ctx);
|
|
483
|
+
if (!srv.isConnected) await utils.sleep(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
|
|
484
|
+
if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
|
|
485
|
+
else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
|
|
486
|
+
else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
|
|
487
|
+
else {
|
|
488
|
+
let resp = await srv.validateRestore(cfg.controllerConfig);
|
|
489
|
+
if (typeof resp !== 'undefined') {
|
|
490
|
+
if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
|
|
491
|
+
let cctx = JSON.parse(resp.data);
|
|
492
|
+
ctx = extend(true, ctx, cctx);
|
|
493
|
+
// Ok so now here we are ready to restore the data.
|
|
494
|
+
let r = await srv.restoreConfig(cfg.controllerConfig);
|
|
495
|
+
|
|
496
|
+
}
|
|
497
|
+
else
|
|
498
|
+
ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
|
|
499
|
+
}
|
|
500
|
+
else
|
|
501
|
+
ctx.server.errors.push(`No response from server`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return stats;
|
|
509
|
+
} catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err); }
|
|
510
|
+
finally {
|
|
511
|
+
try {
|
|
512
|
+
let baseDir = process.cwd();
|
|
513
|
+
let ts = new Date();
|
|
514
|
+
function pad(n) { return (n < 10 ? '0' : '') + n; }
|
|
515
|
+
let filename = 'restoreLog(' + ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + ').log';
|
|
516
|
+
let filePath = path.join(baseDir, 'logs', filename);
|
|
517
|
+
fs.writeFileSync(filePath, JSON.stringify(stats, undefined, 3));
|
|
518
|
+
} catch (err) { logger.error(`Error writing restore log ${err.message}`); }
|
|
519
|
+
}
|
|
200
520
|
}
|
|
201
521
|
}
|
|
202
522
|
class ProtoServer {
|
|
@@ -298,7 +618,7 @@ export class HttpServer extends ProtoServer {
|
|
|
298
618
|
self._sockets = await self.sockServer.fetchSockets();
|
|
299
619
|
});
|
|
300
620
|
sock.on('echo', (msg) => { sock.emit('echo', msg); });
|
|
301
|
-
|
|
621
|
+
/* sock.on('receivePacketRaw', function (incomingPacket: any[]) {
|
|
302
622
|
//var str = 'Add packet(s) to incoming buffer: ';
|
|
303
623
|
logger.silly('User request (replay.html) to RECEIVE packet: %s', JSON.stringify(incomingPacket));
|
|
304
624
|
for (var i = 0; i < incomingPacket.length; i++) {
|
|
@@ -333,7 +653,7 @@ export class HttpServer extends ProtoServer {
|
|
|
333
653
|
conn.queueSendMessage(out);
|
|
334
654
|
} while (bytesToProcessArr.length > 0);
|
|
335
655
|
|
|
336
|
-
});
|
|
656
|
+
}); */
|
|
337
657
|
sock.on('sendOutboundMessage', (mdata) => {
|
|
338
658
|
let msg: Outbound = Outbound.create({});
|
|
339
659
|
Object.assign(msg, mdata);
|
|
@@ -341,6 +661,22 @@ export class HttpServer extends ProtoServer {
|
|
|
341
661
|
logger.silly(`sendOutboundMessage ${msg.toLog()}`);
|
|
342
662
|
conn.queueSendMessage(msg);
|
|
343
663
|
});
|
|
664
|
+
sock.on('sendInboundMessage', (mdata) => {
|
|
665
|
+
try {
|
|
666
|
+
|
|
667
|
+
let msg: Inbound = new Inbound();
|
|
668
|
+
msg.direction = mdata.direction;
|
|
669
|
+
msg.header = mdata.header;
|
|
670
|
+
msg.payload = mdata.payload;
|
|
671
|
+
msg.preamble = mdata.preamble;
|
|
672
|
+
msg.protocol = mdata.protocol;
|
|
673
|
+
msg.term = mdata.term;
|
|
674
|
+
if (msg.isValid) msg.process();
|
|
675
|
+
}
|
|
676
|
+
catch (err){
|
|
677
|
+
logger.error(`Error replaying packet: ${err.message}`);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
344
680
|
sock.on('sendLogMessages', function (sendMessages: boolean) {
|
|
345
681
|
console.log(`sendLogMessages set to ${sendMessages}`);
|
|
346
682
|
if (!sendMessages) sock.leave('msgLogger');
|
|
@@ -451,7 +787,7 @@ export class HttpServer extends ProtoServer {
|
|
|
451
787
|
}
|
|
452
788
|
}
|
|
453
789
|
export class HttpsServer extends HttpServer {
|
|
454
|
-
|
|
790
|
+
declare server: https.Server;
|
|
455
791
|
|
|
456
792
|
public async init(cfg) {
|
|
457
793
|
// const auth = require('http-auth');
|
|
@@ -927,6 +1263,23 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
927
1263
|
}, 5000);
|
|
928
1264
|
}
|
|
929
1265
|
}
|
|
1266
|
+
public async getControllerConfig() : Promise<InterfaceServerResponse> {
|
|
1267
|
+
try {
|
|
1268
|
+
let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
|
|
1269
|
+
return response;
|
|
1270
|
+
} catch (err) { logger.error(err); }
|
|
1271
|
+
}
|
|
1272
|
+
public async validateRestore(cfg): Promise<InterfaceServerResponse> {
|
|
1273
|
+
try {
|
|
1274
|
+
let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
|
|
1275
|
+
return response;
|
|
1276
|
+
} catch (err) { logger.error(err); }
|
|
1277
|
+
}
|
|
1278
|
+
public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
|
|
1279
|
+
try {
|
|
1280
|
+
return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
|
|
1281
|
+
} catch (err) { logger.error(err); }
|
|
1282
|
+
}
|
|
930
1283
|
private async initConnection() {
|
|
931
1284
|
try {
|
|
932
1285
|
// find HTTP server
|
|
@@ -935,16 +1288,21 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
935
1288
|
// First, send the connection info for njsPC and see if a connection exists.
|
|
936
1289
|
let url = '/config/checkconnection/';
|
|
937
1290
|
// can & should extend for https/username-password/ssl
|
|
938
|
-
let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "" }
|
|
1291
|
+
let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "", hostnames: [] }
|
|
1292
|
+
logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
|
|
1293
|
+
try {
|
|
1294
|
+
data.hostnames = await dns.promises.reverse(data.ipAddress);
|
|
1295
|
+
} catch (err) { logger.error(`Error getting hostnames for njsPC REM connection`); }
|
|
939
1296
|
let result = await this.putApiService(url, data, 5000);
|
|
940
1297
|
// If the result code is > 200 we have an issue. (-1 is for timeout)
|
|
941
1298
|
if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`));
|
|
942
|
-
else {
|
|
1299
|
+
else {
|
|
1300
|
+
this.remoteConnectionId = result.obj.id;
|
|
1301
|
+
};
|
|
943
1302
|
|
|
944
1303
|
// The passed connection has been setup/verified; now test for emit
|
|
945
1304
|
// if this fails, it could be because the remote connection is disabled. We will not
|
|
946
1305
|
// automatically re-enable it
|
|
947
|
-
|
|
948
1306
|
url = '/config/checkemit'
|
|
949
1307
|
data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
|
|
950
1308
|
// wait for REM server to finish resetting
|
|
@@ -957,14 +1315,14 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
957
1315
|
// console.log(data);
|
|
958
1316
|
clearTimeout(_tmr);
|
|
959
1317
|
logger.info(`REM bi-directional communications established.`)
|
|
960
|
-
|
|
1318
|
+
resolve();
|
|
961
1319
|
});
|
|
962
1320
|
result = await self.putApiService(url, data);
|
|
963
1321
|
// If the result code is > 200 or -1 we have an issue.
|
|
964
1322
|
if (result.status.code > 200 || result.status.code === -1) return reject(new Error(`initConnection: ${result.error.message}`));
|
|
965
1323
|
else {
|
|
966
1324
|
clearTimeout(_tmr);
|
|
967
|
-
|
|
1325
|
+
resolve();
|
|
968
1326
|
}
|
|
969
1327
|
}
|
|
970
1328
|
catch (err) { reject(new Error(`initConnection setTimeout: ${result.error.message}`)); }
|
|
@@ -1024,7 +1382,10 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1024
1382
|
req = http.request(opts, (response: http.IncomingMessage) => {
|
|
1025
1383
|
ret.status.code = response.statusCode;
|
|
1026
1384
|
ret.status.message = response.statusMessage;
|
|
1027
|
-
response.on('error', (err) => {
|
|
1385
|
+
response.on('error', (err) => {
|
|
1386
|
+
logger.error(`An error occurred with request: ${err}`);
|
|
1387
|
+
ret.error = err; resolve();
|
|
1388
|
+
});
|
|
1028
1389
|
response.on('data', (data) => { ret.data += data; });
|
|
1029
1390
|
response.on('end', () => { resolve(); });
|
|
1030
1391
|
});
|
|
@@ -1120,4 +1481,134 @@ export class REMInterfaceServer extends ProtoServer {
|
|
|
1120
1481
|
catch (err) { logger.error(err); }
|
|
1121
1482
|
}
|
|
1122
1483
|
}
|
|
1484
|
+
export class BackupFile {
|
|
1485
|
+
public static async fromBuffer(filename: string, buff: Buffer) {
|
|
1486
|
+
try {
|
|
1487
|
+
let bf = new BackupFile();
|
|
1488
|
+
bf.filename = filename;
|
|
1489
|
+
bf.filePath = path.join(process.cwd(), 'backups', bf.filename);
|
|
1490
|
+
await bf.extractBackupOptions(buff);
|
|
1491
|
+
return typeof bf.options !== 'undefined' ? bf : undefined;
|
|
1492
|
+
} catch (err) { logger.error(`Error creating buffered backup file: ${filename}`); }
|
|
1493
|
+
}
|
|
1494
|
+
public static async fromFile(filePath: string) {
|
|
1495
|
+
try {
|
|
1496
|
+
let bf = new BackupFile();
|
|
1497
|
+
bf.filePath = filePath;
|
|
1498
|
+
bf.filename = path.parse(filePath).base;
|
|
1499
|
+
await bf.extractBackupOptions(filePath);
|
|
1500
|
+
return typeof bf.options !== 'undefined' ? bf : undefined;
|
|
1501
|
+
} catch (err) { logger.error(`Error creating backup file from file ${filePath}`); }
|
|
1502
|
+
}
|
|
1503
|
+
public options: any;
|
|
1504
|
+
public filename: string;
|
|
1505
|
+
public filePath: string;
|
|
1506
|
+
public errors = [];
|
|
1507
|
+
protected async extractBackupOptions(file: string | Buffer) {
|
|
1508
|
+
try {
|
|
1509
|
+
let jszip = require("jszip");
|
|
1510
|
+
let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
|
|
1511
|
+
let zip = await jszip.loadAsync(buff);
|
|
1512
|
+
await zip.file('options.json').async('string').then((data) => {
|
|
1513
|
+
this.options = JSON.parse(data);
|
|
1514
|
+
if (typeof this.options.backupDate === 'undefined' && typeof file === 'string') {
|
|
1515
|
+
let name = path.parse(file).name;
|
|
1516
|
+
name = name.indexOf('(') !== -1 ? name.substring(0, name.indexOf('(')) : name;
|
|
1517
|
+
if (name.length === 19) {
|
|
1518
|
+
let date = name.substring(0, 10).replace(/-/g, '/');
|
|
1519
|
+
let time = name.substring(11).replace(/-/g, ':');
|
|
1520
|
+
let dt = Date.parse(`${date} ${time}`);
|
|
1521
|
+
if (!isNaN(dt)) this.options.backupDate = Timestamp.toISOLocal(new Date(dt));
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
} catch (err) { this.errors.push(err); logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
export class RestoreFile {
|
|
1529
|
+
public static async fromFile(filePath: string) {
|
|
1530
|
+
try {
|
|
1531
|
+
let rf = new RestoreFile();
|
|
1532
|
+
rf.filePath = filePath;
|
|
1533
|
+
rf.filename = path.parse(filePath).base;
|
|
1534
|
+
await rf.extractRestoreOptions(filePath);
|
|
1535
|
+
return rf;
|
|
1536
|
+
} catch (err) { logger.error(`Error created restore file options`); }
|
|
1537
|
+
}
|
|
1538
|
+
public filename: string;
|
|
1539
|
+
public filePath: string;
|
|
1540
|
+
public njsPC: { config:any, poolConfig: any, poolState: any };
|
|
1541
|
+
public servers: { name: string, uuid: string, serverConfig: any, controllerConfig: any }[] = [];
|
|
1542
|
+
public options: any;
|
|
1543
|
+
public errors = [];
|
|
1544
|
+
protected async extractFile(zip, path): Promise<any> {
|
|
1545
|
+
try {
|
|
1546
|
+
let obj;
|
|
1547
|
+
await zip.file(path).async('string').then((data) => { obj = JSON.parse(data); });
|
|
1548
|
+
return obj;
|
|
1549
|
+
} catch (err) { logger.error(`Error extracting restore data from ${this.filename}[${path}]: ${err.message}`); }
|
|
1550
|
+
}
|
|
1551
|
+
protected async extractRestoreOptions(file: string | Buffer) {
|
|
1552
|
+
try {
|
|
1553
|
+
let jszip = require("jszip");
|
|
1554
|
+
let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
|
|
1555
|
+
let zip = await jszip.loadAsync(buff);
|
|
1556
|
+
this.options = await this.extractFile(zip, 'options.json');
|
|
1557
|
+
// Now we need to extract all the servers from the file.
|
|
1558
|
+
if (this.options.njsPC) {
|
|
1559
|
+
this.njsPC = { config: {}, poolConfig: {}, poolState: {} };
|
|
1560
|
+
this.njsPC.config = await this.extractFile(zip, 'njsPC/config.json');
|
|
1561
|
+
this.njsPC.poolConfig = await this.extractFile(zip, 'njsPC/data/poolConfig.json');
|
|
1562
|
+
this.njsPC.poolState = await this.extractFile(zip, 'njsPC/data/poolState.json');
|
|
1563
|
+
}
|
|
1564
|
+
for (let i = 0; i < this.options.servers.length; i++) {
|
|
1565
|
+
// Extract each server from the file.
|
|
1566
|
+
let srv = this.options.servers[i];
|
|
1567
|
+
if (srv.backup && srv.success) {
|
|
1568
|
+
this.servers.push({
|
|
1569
|
+
name: srv.name,
|
|
1570
|
+
uuid: srv.uuid,
|
|
1571
|
+
serverConfig: await this.extractFile(zip, `${srv.name}/serverConfig.json`),
|
|
1572
|
+
controllerConfig: await this.extractFile(zip, `${srv.name}/data/controllerConfig.json`)
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
} catch(err) { this.errors.push(err); logger.error(`Error extracting restore options from ${file}: ${err.message}`); }
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
export class RestoreResults {
|
|
1580
|
+
public errors = [];
|
|
1581
|
+
public warnings = [];
|
|
1582
|
+
public success = [];
|
|
1583
|
+
public modules: { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number }[] = [];
|
|
1584
|
+
protected getModule(name: string): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
|
|
1585
|
+
let mod = this.modules.find(elem => name === elem.name);
|
|
1586
|
+
if (typeof mod === 'undefined') {
|
|
1587
|
+
mod = { name: name, errors: [], warnings: [], success: [], restored: 0, ignored: 0 };
|
|
1588
|
+
this.modules.push(mod);
|
|
1589
|
+
}
|
|
1590
|
+
return mod;
|
|
1591
|
+
}
|
|
1592
|
+
public addModuleError(name: string, err: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
|
|
1593
|
+
let mod = this.getModule(name);
|
|
1594
|
+
mod.errors.push(err);
|
|
1595
|
+
mod.ignored++;
|
|
1596
|
+
logger.error(`Restore ${name} -> ${err}`);
|
|
1597
|
+
return mod;
|
|
1598
|
+
}
|
|
1599
|
+
public addModuleWarning(name: string, warn: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
|
|
1600
|
+
let mod = this.getModule(name);
|
|
1601
|
+
mod.warnings.push(warn);
|
|
1602
|
+
mod.restored++;
|
|
1603
|
+
logger.warn(`Restore ${name} -> ${warn}`);
|
|
1604
|
+
return mod;
|
|
1605
|
+
}
|
|
1606
|
+
public addModuleSuccess(name: string, success: any): { name: string, errors: any[], warnings: any[], success: any[], restored: number, ignored: number } {
|
|
1607
|
+
let mod = this.getModule(name);
|
|
1608
|
+
mod.success.push(success);
|
|
1609
|
+
mod.restored++;
|
|
1610
|
+
logger.info(`Restore ${name} -> ${success}`);
|
|
1611
|
+
return mod;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1123
1614
|
export const webApp = new WebServer();
|