1 | /**
|
2 | * Copyright 2016-2018 F5 Networks, Inc.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 |
|
17 | ;
|
18 |
|
19 | const EOL = require('os').EOL;
|
20 | const URL = require('url');
|
21 | const fs = require('fs');
|
22 | const path = require('path');
|
23 | const childProcess = require('child_process');
|
24 | const http = require('http');
|
25 | const https = require('https');
|
26 | const q = require('q');
|
27 | const Logger = require('./logger');
|
28 | const ipc = require('./ipc');
|
29 | const signals = require('./signals');
|
30 | const cloudProviderFactory = require('./cloudProviderFactory');
|
31 |
|
32 | const REBOOT_SCRIPTS_DIR = '/tmp/rebootScripts/';
|
33 |
|
34 | const ARGS_TO_KEEP = [
|
35 | '--host',
|
36 | '--user',
|
37 | '--password',
|
38 | '--password-uri',
|
39 | '--password-url',
|
40 | '--password-encrypted',
|
41 | '--port',
|
42 | '--signal',
|
43 | '--log-level',
|
44 | '--output',
|
45 | '--error-file',
|
46 | '--no-console'];
|
47 |
|
48 | let logger = Logger.getLogger({
|
49 | logLevel: 'none',
|
50 | module
|
51 | });
|
52 |
|
53 | /**
|
54 | * Miscellaneous utility functions that don't have a better home
|
55 | *
|
56 | * @module
|
57 | */
|
58 | module.exports = {
|
59 |
|
60 | // 15 minutes
|
61 | DEFAULT_RETRY: {
|
62 | maxRetries: 90,
|
63 | retryIntervalMs: 10000
|
64 | },
|
65 |
|
66 | // 15 minutes - explicit continue on all error codes
|
67 | DEFAULT_RETRY_IGNORE_ERRORS: {
|
68 | maxRetries: 90,
|
69 | retryIntervalMs: 10000,
|
70 | continueOnError: true
|
71 | },
|
72 |
|
73 | // 1 secondish
|
74 | SHORT_RETRY: {
|
75 | maxRetries: 3,
|
76 | retryIntervalMs: 300
|
77 | },
|
78 |
|
79 | // 1 minute
|
80 | MEDIUM_RETRY: {
|
81 | maxRetries: 30,
|
82 | retryIntervalMs: 2000
|
83 | },
|
84 |
|
85 | // 5 minutes, 5 second interval
|
86 | LONG_RETRY: {
|
87 | maxRetries: 60,
|
88 | retryIntervalMs: 5000
|
89 | },
|
90 |
|
91 | // 5 minutes, 1 second interval
|
92 | QUICK_BUT_LONG_RETRY: {
|
93 | maxRetries: 300,
|
94 | retryIntervalMs: 1000
|
95 | },
|
96 |
|
97 | NO_RETRY: {
|
98 | maxRetries: 0,
|
99 | retryIntervalMs: 0
|
100 | },
|
101 |
|
102 | getProduct() {
|
103 | return this.getProductString()
|
104 | .then((response) => {
|
105 | if (!response) {
|
106 | return this.runTmshCommand('save sys config')
|
107 | .then(() => {
|
108 | return this.getProductString();
|
109 | })
|
110 | .then((productString) => {
|
111 | return q(productString);
|
112 | })
|
113 | .catch((errString) => {
|
114 | logger.silly('Unable to get product string',
|
115 | errString && errString.message ? errString.message : errString);
|
116 | return q.reject(errString);
|
117 | });
|
118 | }
|
119 | return q(response);
|
120 | })
|
121 | .catch((err) => {
|
122 | return q.reject(err);
|
123 | });
|
124 | },
|
125 |
|
126 | ipToNumber(ip) {
|
127 | const d = ip.split('.');
|
128 | let n = d[0] * Math.pow(256, 3); // eslint-disable-line no-restricted-properties
|
129 | n += d[1] * Math.pow(256, 2); // eslint-disable-line no-restricted-properties
|
130 | n += d[2] * 256;
|
131 | n += d[3] * 1;
|
132 | return n;
|
133 | },
|
134 |
|
135 | setLogger(aLogger) {
|
136 | logger = aLogger;
|
137 | },
|
138 |
|
139 | setLoggerOptions(loggerOptions) {
|
140 | const loggerOpts = {};
|
141 | Object.keys(loggerOptions).forEach((option) => {
|
142 | loggerOpts[option] = loggerOptions[option];
|
143 | });
|
144 | loggerOpts.module = module;
|
145 | logger = Logger.getLogger(loggerOpts);
|
146 | },
|
147 |
|
148 | /**
|
149 | * Tries a method until it succeeds or reaches a maximum number of retries.
|
150 | *
|
151 | * @param {Object} thisArg - The 'this' argument to pass to the
|
152 | * called function
|
153 | * @param {Object} retryOptions - Options for retrying the request.
|
154 | * @param {Integer} retryOptions.maxRetries - Number of times to retry if first
|
155 | * try fails. 0 to not retry.
|
156 | * Default 60.
|
157 | * @param {Integer} retryOptions.retryIntervalMs - Milliseconds between retries.
|
158 | * Default 10000.
|
159 | * @param {Boolean} [retryOptions.continueOnError] - Continue even if we get an
|
160 | * HTTP BAD_REQUEST code. Default false.
|
161 | * @param {String | RegExp} [retryOptions.continueOnErrorMessage] - Continue on error if the 400 error
|
162 | * message matches this regex
|
163 | * @param {Function} funcToTry - Function to try. Function should
|
164 | * return a Promise which is later
|
165 | * resolved or rejected.
|
166 | * @param {Object[]} args - Array of arguments to pass to
|
167 | * funcToTry
|
168 | *
|
169 | * @returns {Promise} A promise which is resolved with the return from funcToTry
|
170 | * if funcToTry is resolved within maxRetries.
|
171 | */
|
172 | tryUntil(thisArg, retryOptions, funcToTry, args) {
|
173 | let deferred;
|
174 |
|
175 | const shouldReject = function (err) {
|
176 | if (err && err.code && err.code === 400) {
|
177 | if (retryOptions.continueOnError) {
|
178 | return false;
|
179 | }
|
180 | if (err.message && retryOptions.continueOnErrorMessage) {
|
181 | let regex = retryOptions.continueOnErrorMessage;
|
182 | if (!(regex instanceof RegExp)) {
|
183 | regex = new RegExp(retryOptions.continueOnErrorMessage);
|
184 | }
|
185 | return !regex.test(err.message);
|
186 | }
|
187 | return true;
|
188 | }
|
189 | return false;
|
190 | };
|
191 |
|
192 | const tryIt = function tryIt(maxRetries, interval, theFunc, deferredOrNull) {
|
193 | let numRemaining = maxRetries;
|
194 | let promise;
|
195 |
|
196 | const retryOrReject = function (err) {
|
197 | logger.silly(
|
198 | 'tryUntil: retryOrReject: numRemaining:', numRemaining,
|
199 | ', code:', err && err.code ? err.code : err,
|
200 | ', message:', err && err.message ? err.message : err
|
201 | );
|
202 | if (shouldReject(err)) {
|
203 | logger.verbose('Unrecoverable error from HTTP request. Not retrying.');
|
204 | deferred.reject(err);
|
205 | } else if (numRemaining > 0) {
|
206 | numRemaining -= 1;
|
207 | setTimeout(tryIt, interval, numRemaining, interval, theFunc, deferred);
|
208 | } else {
|
209 | logger.verbose('Max tries reached.');
|
210 | const originalMessage = err && err.message ? err.message : 'unknown';
|
211 | const updatedError = {};
|
212 | Object.assign(updatedError, err);
|
213 | updatedError.message = `tryUntil: max tries reached: ${originalMessage}`;
|
214 | updatedError.name = err && err.name ? err.name : '';
|
215 | deferred.reject(updatedError);
|
216 | }
|
217 | };
|
218 |
|
219 | if (!deferredOrNull) {
|
220 | deferred = q.defer();
|
221 | }
|
222 |
|
223 | try {
|
224 | promise = theFunc.apply(thisArg, args)
|
225 | .then((response) => {
|
226 | deferred.resolve(response);
|
227 | })
|
228 | .catch((err) => {
|
229 | logger.silly('tryUntil: got error', err);
|
230 | logger.silly('typeof err', typeof err);
|
231 | let message = '';
|
232 | if (err) {
|
233 | message = err.message ? err.message : err;
|
234 | }
|
235 | logger.verbose('tryUntil error:', message, 'tries left:', maxRetries.toString());
|
236 | retryOrReject(err);
|
237 | });
|
238 |
|
239 | // Allow this to work with native promises which do not have a done
|
240 | if (promise.done) {
|
241 | promise.done();
|
242 | }
|
243 | } catch (err) {
|
244 | retryOrReject(err);
|
245 | }
|
246 |
|
247 | return deferred.promise;
|
248 | };
|
249 |
|
250 | return tryIt(retryOptions.maxRetries, retryOptions.retryIntervalMs, funcToTry);
|
251 | },
|
252 |
|
253 | /**
|
254 | * Calls an array of promises in serial.
|
255 | *
|
256 | * @param {Object} thisArg - The 'this' argument to pass to the called function
|
257 | * @param {Object[]} promises - An array of promise definitions. Each
|
258 | * definition should be:
|
259 | * {
|
260 | * promise: A function that returns a promise
|
261 | * arguments: Array of arguments to pass to the function,
|
262 | * message: An optional message to display at the start of this promise
|
263 | * }
|
264 | * @param {Integer} [delay] - Delay in milliseconds to use between calls.
|
265 | */
|
266 | callInSerial(thisArg, promises, delay) {
|
267 | const interval = delay || 0;
|
268 |
|
269 | const deferred = q.defer();
|
270 | const results = [];
|
271 |
|
272 | const callThem = function (index) {
|
273 | if (index === promises.length) {
|
274 | deferred.resolve(results);
|
275 | return;
|
276 | }
|
277 |
|
278 | if (promises[index].message) {
|
279 | logger.info(promises[index].message);
|
280 | }
|
281 |
|
282 | promises[index].promise.apply(thisArg, promises[index].arguments)
|
283 | .then((response) => {
|
284 | results.push(response);
|
285 |
|
286 | if (index < promises.length - 1) {
|
287 | setTimeout(callThem, interval, index + 1);
|
288 | } else {
|
289 | deferred.resolve(results);
|
290 | }
|
291 | })
|
292 | .catch((err) => {
|
293 | deferred.reject(err);
|
294 | });
|
295 | };
|
296 |
|
297 | callThem(0);
|
298 |
|
299 | return deferred.promise;
|
300 | },
|
301 |
|
302 | deleteUser(user) {
|
303 | return this.runTmshCommand(`delete auth user ${user}`);
|
304 | },
|
305 |
|
306 | /**
|
307 | *
|
308 | * Create a buffer from a data string.
|
309 | * Buffer.from() is preferred, and should be used in versions of NodeJS that support it.
|
310 | * Buffer.from() was introduced in Node 4.5.0 and in 5.10.0
|
311 | *
|
312 | * @param {String} data - data to create a buffer from
|
313 | * @param {String} encoding - data encoding. Default is 'utf8'
|
314 | */
|
315 | createBufferFrom(data, encoding) {
|
316 | if (typeof data !== 'string') {
|
317 | throw new Error('data must be a string');
|
318 | }
|
319 | let useBufferFrom;
|
320 | const nodeVersion = process.version.split('v').pop();
|
321 | if (nodeVersion.split('.')[0] === '4' && (this.versionCompare(nodeVersion, '4.5.0') > -1)) {
|
322 | useBufferFrom = true;
|
323 | } else if (this.versionCompare(nodeVersion, '5.10.0') > -1) {
|
324 | useBufferFrom = true;
|
325 | } else {
|
326 | useBufferFrom = false;
|
327 | }
|
328 | if (useBufferFrom) {
|
329 | return Buffer.from(data, encoding || 'utf8');
|
330 | }
|
331 | // eslint-disable-next-line
|
332 | return new Buffer(data, encoding || 'utf8');
|
333 | },
|
334 |
|
335 | /**
|
336 | * Log a message and exit.
|
337 | *
|
338 | * Makes sure that message is logged before exiting.
|
339 | *
|
340 | * @param {String} message - Message to log
|
341 | * @param {String} [level] - Level at which to log the message. Default info.
|
342 | * @param {Number} [code] - Exit code. Default 0.
|
343 | */
|
344 | logAndExit(message, level, code) {
|
345 | const logLevel = level || 'info';
|
346 | let exitCode = code;
|
347 |
|
348 | if (typeof exitCode === 'undefined') {
|
349 | exitCode = 0;
|
350 | }
|
351 |
|
352 | logger.log(logLevel, message);
|
353 | // exit on flush event if file transport exists
|
354 | if (logger.transports.file) {
|
355 | logger.transports.file.on('flush', () => {
|
356 | process.exit(exitCode);
|
357 | });
|
358 | }
|
359 | // if no file transport or flush event does not trigger
|
360 | // simply exit after hard count
|
361 | setTimeout(() => {
|
362 | process.exit(exitCode);
|
363 | }, 1000);
|
364 | },
|
365 |
|
366 | /**
|
367 | * Log a message to the error log file.
|
368 | *
|
369 | * @param {String} message - Message to log
|
370 | * @param {Object} options - Logger options
|
371 | */
|
372 | logError(message, options) {
|
373 | const loggerOptions = {};
|
374 | Object.assign(loggerOptions, options);
|
375 |
|
376 | loggerOptions.json = true;
|
377 | loggerOptions.verboseLabel = true;
|
378 |
|
379 | if (loggerOptions.errorFile) {
|
380 | loggerOptions.fileName = loggerOptions.errorFile;
|
381 | } else if (loggerOptions.fileName) {
|
382 | loggerOptions.fileName = `${path.dirname(loggerOptions.fileName)}/cloudLibsError.log`;
|
383 | } else {
|
384 | loggerOptions.fileName = '/tmp/cloudLibsError.log';
|
385 | }
|
386 |
|
387 | const errorLogger = Logger.getLogger(loggerOptions);
|
388 | errorLogger.error(message);
|
389 | },
|
390 |
|
391 | /**
|
392 | * Returns the count of running processes, given the provided grep command
|
393 | * example:
|
394 | * getProcessCount('grep autoscale.js')
|
395 | *
|
396 | * @param {String} grepCommand - grep command to execute.
|
397 | *
|
398 | * @returns {Promise} A Promise that is resolved with the output of the
|
399 | * shell command or rejected if an error occurs.
|
400 | */
|
401 | getProcessCount(grepCommand) {
|
402 | if (!grepCommand) {
|
403 | const errorMessage = 'grep command is required';
|
404 | logger.error(errorMessage);
|
405 | return q.reject(new Error(errorMessage));
|
406 | }
|
407 | const shellCommand = `/bin/ps -eo pid,cmd | ${grepCommand} | wc -l`;
|
408 |
|
409 | return this.runShellCommand(shellCommand)
|
410 | .then((response) => {
|
411 | return q(
|
412 | (typeof response === 'string') ? response.trim() : response
|
413 | );
|
414 | })
|
415 | .catch((err) => {
|
416 | logger.warn('Unable to get process count', err && err.message ? err.message : err);
|
417 | return q.reject(err);
|
418 | });
|
419 | },
|
420 |
|
421 | /**
|
422 | * Returns execution time and pid of process, given the provided grep command
|
423 | * example:
|
424 | * getProcessExecutionTimeWithPid('grep autoscale.js')
|
425 | *
|
426 | * @param {String} grepCommand - grep command to execute.
|
427 | *
|
428 | * @returns {Promise} A Promise that is resolved with the output of the
|
429 | * shell command or rejected if an error occurs.
|
430 | */
|
431 | getProcessExecutionTimeWithPid(grepCommand) {
|
432 | if (!grepCommand) {
|
433 | const errorMessage = 'grep command is required';
|
434 | logger.error(errorMessage);
|
435 | return q.reject(new Error(errorMessage));
|
436 | }
|
437 | const cmd = `/bin/ps -eo pid,etime,cmd --sort=-time | ${grepCommand} | awk '{ print $1"-"$2 }'`;
|
438 |
|
439 | logger.silly(`shellCommand: ${cmd}`);
|
440 | return this.runShellCommand(cmd)
|
441 | .then((response) => {
|
442 | return q(
|
443 | (typeof response === 'string') ? response.trim() : response
|
444 | );
|
445 | })
|
446 | .catch((err) => {
|
447 | logger.warn('Unable to get process execution time with pid',
|
448 | err && err.message ? err.message : err);
|
449 | return q.reject(err);
|
450 | });
|
451 | },
|
452 |
|
453 | /**
|
454 | * Terminates process using PID value
|
455 | * example:
|
456 | * terminateProcessById('1212')
|
457 | *
|
458 | * @param {String} pid - process pid value.
|
459 | *
|
460 | * @returns {Promise} A Promise that is resolved with the output of the
|
461 | * shell command or rejected if an error occurs.
|
462 | */
|
463 | terminateProcessById(pid) {
|
464 | if (!pid) {
|
465 | const errorMessage = 'pid is required for process termination';
|
466 | logger.error(errorMessage);
|
467 | return q.reject(new Error(errorMessage));
|
468 | }
|
469 | logger.silly(`Autoscale Process ID to kill: ${pid}`);
|
470 | const shellCommand = `/bin/kill -9 ${pid}`;
|
471 | return this.runShellCommand(shellCommand)
|
472 | .then((response) => {
|
473 | return q(
|
474 | (typeof response === 'string') ? response.trim() : response
|
475 | );
|
476 | })
|
477 | .catch((err) => {
|
478 | logger.warn('Unable to terminate the process',
|
479 | err && err.message ? err.message : err);
|
480 | return q.reject(err);
|
481 | });
|
482 | },
|
483 |
|
484 | /**
|
485 | * Reboots the device
|
486 | *
|
487 | * First save arguments from running scripts so that they are started
|
488 | * again on startup
|
489 | *
|
490 | * @param {Object} bigIp - The BigIp instance to reboot.
|
491 | * @param {Object} [options] - Optional parameters.
|
492 | * @param {Boolean} [options.signalOnly] - Indicates that we should not actually reboot, just
|
493 | * prepare args and signal that reboot is required
|
494 | *
|
495 | * @returns {Promise} A Promise that is resolved when the reboot command
|
496 | * has been sent or rejected if an error occurs.
|
497 | */
|
498 | reboot(bigIp, options) {
|
499 | return prepareArgsForReboot(logger)
|
500 | .then(() => {
|
501 | if (options && options.signalOnly) {
|
502 | logger.info('Skipping reboot due to options. Signaling only.');
|
503 | ipc.send(signals.REBOOT_REQUIRED);
|
504 | return q();
|
505 | }
|
506 | logger.info('Rebooting.');
|
507 | ipc.send(signals.REBOOT);
|
508 | return bigIp.reboot();
|
509 | });
|
510 | },
|
511 |
|
512 | /**
|
513 | * Spawns a new process in the background and exits current process
|
514 | *
|
515 | * @param {Object} process - Node.js process
|
516 | * @param {String} logFileName - Name to pass for output log file
|
517 | */
|
518 | runInBackgroundAndExit(process, logFileName) {
|
519 | let args;
|
520 | let myChild;
|
521 |
|
522 | if (process.argv.length > 100) {
|
523 | logger.warn("Too many arguments - maybe we're stuck in a restart loop?");
|
524 | } else {
|
525 | args = process.argv.slice(1);
|
526 |
|
527 | // remove the background option(s)
|
528 | for (let i = args.length - 1; i >= 0; i -= 1) {
|
529 | if (args[i] === '--background') {
|
530 | args.splice(i, 1);
|
531 | }
|
532 | }
|
533 |
|
534 | // capture output in a log file
|
535 | if (args.indexOf('--output') === -1) {
|
536 | args.push('--output', logFileName);
|
537 | }
|
538 |
|
539 | logger.debug('Spawning child process.', args);
|
540 | myChild = childProcess.spawn(
|
541 | process.argv[0],
|
542 | args,
|
543 | {
|
544 | stdio: 'ignore',
|
545 | detached: true
|
546 | }
|
547 | );
|
548 | myChild.unref();
|
549 | }
|
550 |
|
551 | logger.debug('Original process exiting.');
|
552 | process.exit();
|
553 | },
|
554 |
|
555 | /**
|
556 | * Filters options arguments provided based on a list of arguments to keep
|
557 | *
|
558 | * @param {Object} options - Options object to pull arguments from
|
559 | * @param {Object} [argsToKeep] - Arguments to keep. Default ARGS_TO_KEEP.
|
560 | */
|
561 | getArgsToStripDuringForcedReboot(options, argsToKeep) {
|
562 | const argsTK = argsToKeep || ARGS_TO_KEEP;
|
563 | const argsToStrip = [];
|
564 | options.options.forEach((opt) => {
|
565 | // add to argsToStrip, unless it is one of the args we want to keep
|
566 | if (argsTK) {
|
567 | if (opt.long && argsTK.indexOf(opt.long) === -1) {
|
568 | argsToStrip.push(opt.long);
|
569 | // if short option exist for this arg, also strip
|
570 | if (opt.short) {
|
571 | argsToStrip.push(opt.short);
|
572 | }
|
573 | }
|
574 | }
|
575 | });
|
576 | return argsToStrip;
|
577 | },
|
578 |
|
579 | /**
|
580 | * Saves arguments that started a script so that they can be re-used
|
581 | * in the event we get stuck and need to reboot.
|
582 | *
|
583 | * @param {String[]} args - Array of arguments that can be used to re-run
|
584 | * the process (i.e. process.argv)
|
585 | * @param {String} id - Some unique id for the process. This will be used
|
586 | * as the file name in which
|
587 | * to store the files.
|
588 | * @param {Object} [argsToStrip] - Array of arguments to strip. Default none.
|
589 | */
|
590 | saveArgs(args, id, argsToStrip) {
|
591 | const deferred = q.defer();
|
592 | const fullPath = `${REBOOT_SCRIPTS_DIR}${id}.sh`;
|
593 | const updatedArgs = [];
|
594 |
|
595 | try {
|
596 | fs.stat(REBOOT_SCRIPTS_DIR, (fsStatErr) => {
|
597 | if (fsStatErr && fsStatErr.code === 'ENOENT') {
|
598 | try {
|
599 | fs.mkdirSync(REBOOT_SCRIPTS_DIR);
|
600 | } catch (err) {
|
601 | // Check for race condition on creating directory while others are doing the same
|
602 | if (err.code !== 'EEXIST') {
|
603 | logger.error(
|
604 | 'Error creating',
|
605 | REBOOT_SCRIPTS_DIR,
|
606 | 'Not saving args for a second try.',
|
607 | err
|
608 | );
|
609 | deferred.resolve();
|
610 | }
|
611 | }
|
612 | } else if (fsStatErr) {
|
613 | logger.warn('Unable to stat', REBOOT_SCRIPTS_DIR, 'Not saving args for a second try.');
|
614 | // Just resolve - may as well try to run anyway
|
615 | deferred.resolve();
|
616 | }
|
617 |
|
618 | try {
|
619 | fs.open(fullPath, 'w', (fsOpenErr, fd) => {
|
620 | if (fsOpenErr) {
|
621 | logger.warn('Unable to open', fullPath, 'Not saving args for a second try.');
|
622 | // Just resolve - may as well try to run anyway
|
623 | deferred.resolve();
|
624 | return;
|
625 | }
|
626 |
|
627 | // push the executable and script name
|
628 | updatedArgs.push(args[0], args[1]);
|
629 | for (let i = 2; i < args.length; i++) {
|
630 | if (args[i][0] === '-') {
|
631 | if (!argsToStrip || argsToStrip.indexOf(args[i]) === -1) {
|
632 | updatedArgs.push(args[i]);
|
633 |
|
634 | // If the first character of the next argument does not start with '-',
|
635 | // assume it is a parameter for this argument
|
636 | if (args.length > i + 1) {
|
637 | if (args[i + 1][0] !== '-') {
|
638 | updatedArgs.push(args[i + 1]);
|
639 | }
|
640 | }
|
641 | }
|
642 | }
|
643 | }
|
644 |
|
645 | try {
|
646 | fs.writeSync(fd, `#!/bin/bash${EOL}`);
|
647 | fs.writeSync(fd, updatedArgs.join(' ') + EOL);
|
648 | fs.fchmodSync(fd, 0o755);
|
649 | } catch (err) {
|
650 | logger.warn('Unable to save args', err);
|
651 | } finally {
|
652 | fs.closeSync(fd);
|
653 | deferred.resolve();
|
654 | }
|
655 | });
|
656 | } catch (err) {
|
657 | logger.warn('Unable to open args file', err);
|
658 | deferred.resolve();
|
659 | }
|
660 | });
|
661 | } catch (err) {
|
662 | logger.warn('Unable to stat', REBOOT_SCRIPTS_DIR);
|
663 | deferred.resolve();
|
664 | }
|
665 |
|
666 | return deferred.promise;
|
667 | },
|
668 |
|
669 | /**
|
670 | * Deletes the arguments previously saved with saveArgs
|
671 | *
|
672 | * This should be called when a script successfully completes.
|
673 | *
|
674 | * @param {String} id - Some unique id for the process. This will be used as the file name in which
|
675 | * to store the files.
|
676 | */
|
677 | deleteArgs(id) {
|
678 | const file = `${REBOOT_SCRIPTS_DIR}${id}.sh`;
|
679 | if (fs.existsSync(file)) {
|
680 | fs.unlinkSync(file);
|
681 | }
|
682 | },
|
683 |
|
684 | /**
|
685 | * Adds value to an array
|
686 | *
|
687 | * Typically used by the option parser for collecting
|
688 | * multiple values for a command line option
|
689 | *
|
690 | * @param {String} val - The comma separated value string
|
691 | * @param {String[]} collecction - The array into which to put the value
|
692 | *
|
693 | * @returns {String[]} The updated collection
|
694 | */
|
695 | collect(val, collection) {
|
696 | collection.push(val);
|
697 | return collection;
|
698 | },
|
699 |
|
700 | /**
|
701 | * Parses a comma-separated value
|
702 | *
|
703 | * Typically used by the option parser for collecting
|
704 | * multiple values which are comma separated values.
|
705 | *
|
706 | * Leading and trailing spaces are removed from keys and values
|
707 | *
|
708 | * @param {String} val - The comma separated value string
|
709 | * @param {String[][]} collecction - The array into which to put a new array conataining the values
|
710 | *
|
711 | * @returns {String[][]} The updated collection
|
712 | */
|
713 | csv(val, collection) {
|
714 | const values = val.split(',');
|
715 | const newEntry = [];
|
716 | values.forEach((value) => {
|
717 | newEntry.push(value.trim());
|
718 | });
|
719 | collection.push(newEntry);
|
720 | return collection;
|
721 | },
|
722 |
|
723 | /**
|
724 | * Parses a ':' deliminated key-value pair and stores them
|
725 | * in a container.
|
726 | *
|
727 | * Leading and trailing spaces are removed from keys and values
|
728 | *
|
729 | * Typically used by the option parser for collecting
|
730 | * multiple key-value pairs for a command line option
|
731 | *
|
732 | * @param {String} pair - String in the format of key:value
|
733 | * @param {Object} container - Object into which to put the key:value
|
734 | */
|
735 | pair(pair, container) {
|
736 | const nameVal = pair.split(/:(.+)/);
|
737 | container[nameVal[0].trim()] = nameVal[1].trim(); // eslint-disable-line no-param-reassign
|
738 | },
|
739 |
|
740 | /**
|
741 | * Parses a string of keys and values into a single map
|
742 | *
|
743 | * Keys are separated from values by a ':'
|
744 | * Key value pairs are separated from each other by a ','
|
745 | * Example:
|
746 | * user:JoeBob,password:iamcool
|
747 | *
|
748 | * @param {String} mapString - String form of map. See example above.
|
749 | * @param {Objcect} container - Container to hold the map
|
750 | *
|
751 | * @returns {Object} A map of all of key value pairs.
|
752 | */
|
753 | map(mapString, container) {
|
754 | let params;
|
755 | let key;
|
756 | let value;
|
757 |
|
758 | // prepend a ',' so we can use a regex to split
|
759 | const mungedString = `,${mapString}`;
|
760 |
|
761 | // split on ,<key>:<value>
|
762 | params = mungedString.split(/,([^,]+?):/);
|
763 |
|
764 | // strip off the first match, which is an empty string
|
765 | params = params.splice(1);
|
766 |
|
767 | for (let i = 0; i < params.length; i++) {
|
768 | key = params[i].trim();
|
769 | value = params[i + 1].trim();
|
770 |
|
771 | if (value.toLocaleLowerCase() === 'true' || value.toLocaleLowerCase() === 'false') {
|
772 | value = Boolean(value.toLocaleLowerCase() === 'true');
|
773 | }
|
774 |
|
775 | container[key] = value; // eslint-disable-line no-param-reassign
|
776 | i += 1;
|
777 | }
|
778 | },
|
779 |
|
780 | /**
|
781 | * Parses a string of keys and values. Each call is one element
|
782 | * in the array.
|
783 | *
|
784 | * Keys are separated from values by a ':'
|
785 | * Key value pairs are separated from each other by a ','
|
786 | * Example:
|
787 | * user:JoeBob,password:iamcool
|
788 | *
|
789 | * @param {String} mapString - String form of map. See example above.
|
790 | * @param {Objcect[]} container - Container into which to push the map object
|
791 | *
|
792 | * @returns {Object[]} An array containing one map per call with the same container
|
793 | */
|
794 | mapArray(mapString, container) {
|
795 | const mapObject = {};
|
796 | let params;
|
797 | let i;
|
798 |
|
799 | // prepend a ',' so we can use a regex to split
|
800 | const mungedString = `,${mapString}`;
|
801 |
|
802 | // split on ,<key>:<value>
|
803 | params = mungedString.split(/,([^,]+?):/);
|
804 |
|
805 | // strip off the first match, which is an empty string
|
806 | params = params.splice(1);
|
807 |
|
808 | for (i = 0; i < params.length; i++) {
|
809 | mapObject[params[i].trim()] = params[i + 1].trim();
|
810 | i += 1;
|
811 | }
|
812 |
|
813 | container.push(mapObject);
|
814 | },
|
815 |
|
816 | /**
|
817 | * Lower cases all the keys in an object, including nested keys.
|
818 | *
|
819 | * Typically used when working with JSON input.
|
820 | *
|
821 | * If a parameter that is not of type object is provided, the original parameter will be returned.
|
822 | *
|
823 | * @param {Object} obj - Object in which to lowercase keys
|
824 | *
|
825 | * @returns {Object} - An object resembling the provided obj, but with lowercased keys.
|
826 | */
|
827 | lowerCaseKeys(obj) {
|
828 | if (typeof obj === 'object') {
|
829 | const newObj = {};
|
830 | Object.keys(obj).forEach((key) => {
|
831 | if (typeof obj[key] === 'object') {
|
832 | const nestedObj = this.lowerCaseKeys(obj[key]);
|
833 | newObj[key.toLocaleLowerCase()] = nestedObj;
|
834 | } else {
|
835 | newObj[key.toLocaleLowerCase()] = obj[key];
|
836 | }
|
837 | });
|
838 | return newObj;
|
839 | }
|
840 | return obj;
|
841 | },
|
842 |
|
843 | /**
|
844 | * Writes data to a file
|
845 | *
|
846 | * @param {String} data - The data to write
|
847 | * @param {String} file - The file to write to
|
848 | *
|
849 | * @returns A promise which will be resolved when the file is written
|
850 | * or rejected if an error occurs
|
851 | */
|
852 | writeDataToFile(data, file) {
|
853 | const deferred = q.defer();
|
854 |
|
855 | if (fs.existsSync(file)) {
|
856 | fs.unlinkSync(file);
|
857 | }
|
858 |
|
859 | fs.writeFile(
|
860 | file,
|
861 | data,
|
862 | { mode: 0o400 },
|
863 | (err) => {
|
864 | if (err) {
|
865 | deferred.reject(err);
|
866 | } else {
|
867 | deferred.resolve();
|
868 | }
|
869 | }
|
870 | );
|
871 |
|
872 | return deferred.promise;
|
873 | },
|
874 |
|
875 | /**
|
876 | * Reads data from a file
|
877 | *
|
878 | * @param {String} file - The file to read from
|
879 | *
|
880 | * @returns A promise which will be resolved with the contents of the file
|
881 | * or rejected if an error occurs
|
882 | */
|
883 | readDataFromFile(file) {
|
884 | const deferred = q.defer();
|
885 |
|
886 | fs.readFile(file, (err, data) => {
|
887 | if (err) {
|
888 | deferred.reject(err);
|
889 | } else {
|
890 | deferred.resolve(data);
|
891 | }
|
892 | });
|
893 |
|
894 | return deferred.promise;
|
895 | },
|
896 |
|
897 | /**
|
898 | * Writes data to a URL.
|
899 | *
|
900 | * Only file URLs are supported for now.
|
901 | *
|
902 | * @param {String} data - The data to write
|
903 | * @param {String} url - URL to which to write. Only file URLs are supported
|
904 | */
|
905 | writeDataToUrl(data, url) {
|
906 | const deferred = q.defer();
|
907 | let parsedUrl;
|
908 |
|
909 | try {
|
910 | parsedUrl = URL.parse(url);
|
911 | if (parsedUrl.protocol === 'file:') {
|
912 | this.writeDataToFile(data, parsedUrl.pathname)
|
913 | .then(() => {
|
914 | deferred.resolve();
|
915 | })
|
916 | .catch((err) => {
|
917 | deferred.reject(err);
|
918 | });
|
919 | } else {
|
920 | deferred.reject(new Error('Only file URLs are currently supported.'));
|
921 | }
|
922 | } catch (err) {
|
923 | deferred.reject(err);
|
924 | }
|
925 |
|
926 | return deferred.promise;
|
927 | },
|
928 |
|
929 | /**
|
930 | * Disambiguates data that is either raw data or in a URI.
|
931 | *
|
932 | * @param {String} dataOrUri - Data URI (file, http, https, AWS arn) to
|
933 | * location containing data.
|
934 | * @param {Boolean} dataIsUri - Indicates that password is a URI for the password
|
935 | * @param {Object} [options] - Optional parameters.
|
936 | * @param {Object} [options.clOptions] - Command line options if called from a script.
|
937 | * Required for Azure Storage URIs
|
938 | * @param {Object} [options.logger] - Logger to use. Or, pass loggerOptions to
|
939 | * get your own logger.
|
940 | * @param {Object} [options.loggerOptions] - Options for the logger.
|
941 | * See {@link module:logger.getLogger} for details.
|
942 | */
|
943 | readData(dataOrUri, dataIsUri, options) {
|
944 | const deferred = q.defer();
|
945 | if (dataIsUri) {
|
946 | const matchOptions = {
|
947 | storageUri: dataOrUri
|
948 | };
|
949 | let provider;
|
950 |
|
951 | // If no cloud provider match, proceed since URI may be plain URL or plain data
|
952 | try {
|
953 | provider = cloudProviderFactory.getCloudProvider(null, options, matchOptions);
|
954 | } catch (err) {
|
955 | if (err.message !== 'Unavailable cloud provider') {
|
956 | throw err;
|
957 | }
|
958 | }
|
959 |
|
960 | if (provider) {
|
961 | const clOptions = (options && options.clOptions) ? options.clOptions : null;
|
962 | provider.init(clOptions)
|
963 | .then(() => {
|
964 | return this.tryUntil(
|
965 | provider,
|
966 | this.MEDIUM_RETRY,
|
967 | provider.getDataFromUri,
|
968 | [dataOrUri]
|
969 | );
|
970 | })
|
971 | .then((data) => {
|
972 | deferred.resolve(data);
|
973 | })
|
974 | .catch((err) => {
|
975 | logger.info('Could not find BIG-IQ credentials file in cloud provider storage');
|
976 | deferred.reject(err);
|
977 | });
|
978 | } else {
|
979 | // Plain old url
|
980 | this.getDataFromUrl(dataOrUri)
|
981 | .then((data) => {
|
982 | deferred.resolve(data);
|
983 | })
|
984 | .catch((err) => {
|
985 | deferred.reject(err);
|
986 | });
|
987 | }
|
988 | } else {
|
989 | // Plain old data
|
990 | deferred.resolve(dataOrUri);
|
991 | }
|
992 |
|
993 | return deferred.promise;
|
994 | },
|
995 |
|
996 | /**
|
997 | * Gets data from a URL.
|
998 | *
|
999 | * Only file, http, https URLs are supported for now.
|
1000 | *
|
1001 | * @param {String} url - URL from which to get the data. Only
|
1002 | * file, http, https URLs are supported for now.
|
1003 | * @param {Object} [options] - http/https request options
|
1004 | *
|
1005 | * @returns {String} A promise which will be resolved with the data
|
1006 | * or rejected if an error occurs.
|
1007 | */
|
1008 | getDataFromUrl(url, options) {
|
1009 | const parsedUrl = URL.parse(url);
|
1010 | const deferred = q.defer();
|
1011 | const requestOptions = Object.assign({}, options);
|
1012 | let executor;
|
1013 |
|
1014 | try {
|
1015 | if (parsedUrl.protocol === 'file:') {
|
1016 | fs.readFile(parsedUrl.pathname, { encoding: 'ascii' }, (err, data) => {
|
1017 | if (err) {
|
1018 | deferred.reject(err);
|
1019 | } else {
|
1020 | deferred.resolve(data.trim());
|
1021 | }
|
1022 | });
|
1023 | } else if (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') {
|
1024 | executor = parsedUrl.protocol === 'http:' ? http : https;
|
1025 | requestOptions.protocol = parsedUrl.protocol;
|
1026 | requestOptions.hostname = parsedUrl.hostname;
|
1027 | requestOptions.port = parsedUrl.port;
|
1028 | requestOptions.path = parsedUrl.pathname + (parsedUrl.search ? parsedUrl.search : '');
|
1029 |
|
1030 | executor.get(requestOptions, (response) => {
|
1031 | const statusCode = response.statusCode;
|
1032 | const contentType = response.headers['content-type'];
|
1033 | let rawData = '';
|
1034 | let data;
|
1035 |
|
1036 | if (statusCode >= 300) {
|
1037 | const message = `${url.toString()} returned with status code ${statusCode}`;
|
1038 | deferred.reject(new Error(message));
|
1039 | response.resume();
|
1040 | }
|
1041 |
|
1042 | response.setEncoding('utf8');
|
1043 | response.on('data', (chunk) => {
|
1044 | rawData += chunk;
|
1045 | });
|
1046 | response.on('end', () => {
|
1047 | if (contentType && contentType.indexOf('application/json') !== -1) {
|
1048 | try {
|
1049 | data = JSON.parse(rawData);
|
1050 | } catch (err) {
|
1051 | deferred.reject(err);
|
1052 | }
|
1053 | } else {
|
1054 | data = rawData.trim();
|
1055 | }
|
1056 | deferred.resolve(data);
|
1057 | });
|
1058 | })
|
1059 | .on('error', (err) => {
|
1060 | deferred.reject(err);
|
1061 | });
|
1062 | } else {
|
1063 | deferred.reject(new Error('Only file, http, and https URLs are currently supported.'));
|
1064 | }
|
1065 | } catch (err) {
|
1066 | deferred.reject(err);
|
1067 | }
|
1068 |
|
1069 | return deferred.promise;
|
1070 | },
|
1071 |
|
1072 | /**
|
1073 | * Downloads a file from a URL
|
1074 | *
|
1075 | * @param {String} url - URL to download from
|
1076 | *
|
1077 | * @returns {Promise} A promise which is resolved with the file name the file was downloaded to
|
1078 | * or rejected if an error occurs.
|
1079 | */
|
1080 | download(url) {
|
1081 | const deferred = q.defer();
|
1082 | const parsedUrl = URL.parse(url);
|
1083 | let executor;
|
1084 |
|
1085 | if (parsedUrl.protocol === 'http:') {
|
1086 | executor = http;
|
1087 | } else if (parsedUrl.protocol === 'https:') {
|
1088 | executor = https;
|
1089 | } else {
|
1090 | deferred.reject(new Error(`Unhandled protocol: ${parsedUrl.protocol}`));
|
1091 | return deferred.promise;
|
1092 | }
|
1093 |
|
1094 | const fileName = `/tmp/f5-cloud-libs_'${Date.now()}`;
|
1095 | const file = fs.createWriteStream(fileName);
|
1096 |
|
1097 | executor.get(url, (response) => {
|
1098 | response.pipe(file);
|
1099 | file.on('finish', () => {
|
1100 | file.close(() => {
|
1101 | deferred.resolve(fileName);
|
1102 | });
|
1103 | });
|
1104 | })
|
1105 | .on('error', (err) => {
|
1106 | if (fs.existsSync(fileName)) {
|
1107 | fs.unlink(fileName);
|
1108 | }
|
1109 | deferred.reject(err);
|
1110 | });
|
1111 |
|
1112 | return deferred.promise;
|
1113 | },
|
1114 |
|
1115 | /**
|
1116 | * Synchronously removes a directory and all files in the directory
|
1117 | *
|
1118 | * @param {String} dir - Directory to remove
|
1119 | */
|
1120 | removeDirectorySync(dir) {
|
1121 | if (fs.existsSync(dir)) {
|
1122 | fs.readdirSync(dir).forEach((file) => {
|
1123 | const curPath = `${dir}/${file}`;
|
1124 | if (fs.statSync(curPath).isDirectory()) {
|
1125 | this.removeDirectorySync(curPath);
|
1126 | } else {
|
1127 | fs.unlinkSync(curPath);
|
1128 | }
|
1129 | });
|
1130 | fs.rmdirSync(dir);
|
1131 | }
|
1132 | },
|
1133 |
|
1134 | /**
|
1135 | * Performs a local ready check
|
1136 | *
|
1137 | * @returns {Promise} A promise which is resolved upon completion of
|
1138 | * the script or rejected if an error occurs.
|
1139 | */
|
1140 | localReady() {
|
1141 | const deferred = q.defer();
|
1142 |
|
1143 | logger.silly('Performing local ready check');
|
1144 | childProcess.exec(`/bin/sh ${__dirname}/../scripts/waitForMcp.sh`, (error) => {
|
1145 | if (error) {
|
1146 | deferred.reject(error);
|
1147 | } else {
|
1148 | deferred.resolve();
|
1149 | }
|
1150 | });
|
1151 |
|
1152 | return deferred.promise;
|
1153 | },
|
1154 |
|
1155 | /**
|
1156 | * Runs a shell command and returns the output
|
1157 | *
|
1158 | * @param {String} commands - Command to run
|
1159 | *
|
1160 | * @returns {Promise} A promise which is resolved with the results of the
|
1161 | * command or rejected if an error occurs.
|
1162 | */
|
1163 | runShellCommand(command) {
|
1164 | const deferred = q.defer();
|
1165 | childProcess.exec(command, (error, stdout, stderr) => {
|
1166 | if (error) {
|
1167 | deferred.reject(new Error(`${error}:${stderr}`));
|
1168 | } else {
|
1169 | deferred.resolve(stdout);
|
1170 | }
|
1171 | });
|
1172 | return deferred.promise;
|
1173 | },
|
1174 |
|
1175 | /**
|
1176 | * Runs a tmsh command and returns the output
|
1177 | *
|
1178 | * @param {String} commands - Command to run ('list ltm pool', for example)
|
1179 | *
|
1180 | * @returns {Promise} A promise which is resolved with the results of the
|
1181 | * command or rejected if an error occurs.
|
1182 | */
|
1183 | runTmshCommand(command) {
|
1184 | const deferred = q.defer();
|
1185 |
|
1186 | this.localReady()
|
1187 | .then(() => {
|
1188 | const tmshCommand = `/usr/bin/tmsh -a ${command}`;
|
1189 | return this.tryUntil(this, this.MEDIUM_RETRY, this.runShellCommand, [tmshCommand]);
|
1190 | })
|
1191 | .then((response) => {
|
1192 | deferred.resolve(response);
|
1193 | })
|
1194 | .catch((err) => {
|
1195 | logger.silly('tmsh command failed', err && err.message ? err.message : err);
|
1196 | deferred.reject(err);
|
1197 | });
|
1198 | return deferred.promise;
|
1199 | },
|
1200 |
|
1201 | /**
|
1202 | * Parse a tmsh response into an object
|
1203 | *
|
1204 | * @param {String} response - tmsh response data to parse
|
1205 | *
|
1206 | * @returns {Promise} A promise containing the parsed response data, as an object.
|
1207 | */
|
1208 | parseTmshResponse(response) {
|
1209 | const keyVals = response.split(/\s+/);
|
1210 | const result = {};
|
1211 |
|
1212 | // find the parts inside the {}
|
1213 | const openingBraceIndex = keyVals.indexOf('{');
|
1214 | const closingBraceIndex = keyVals.lastIndexOf('}');
|
1215 |
|
1216 | for (let i = openingBraceIndex + 1; i < closingBraceIndex - 1; i += 2) {
|
1217 | result[keyVals[i]] = keyVals[i + 1];
|
1218 | }
|
1219 |
|
1220 | return result;
|
1221 | },
|
1222 |
|
1223 | /**
|
1224 | * Returns the product type (BigIP/BigIQ) from the bigip_base.conf file
|
1225 | */
|
1226 | getProductString() {
|
1227 | const deferred = q.defer();
|
1228 | fs.stat('/usr/bin/tmsh', (fsStatErr) => {
|
1229 | if (fsStatErr && fsStatErr.code === 'ENOENT') {
|
1230 | deferred.resolve('CONTAINER');
|
1231 | } else if (fsStatErr) {
|
1232 | logger.silly('Unable to determine product', fsStatErr.message);
|
1233 | deferred.reject(fsStatErr);
|
1234 | } else {
|
1235 | this.runShellCommand('grep -m 1 product /config/bigip_base.conf | awk \'{print $2}\'')
|
1236 | .then((response) => {
|
1237 | deferred.resolve(response.trim());
|
1238 | })
|
1239 | .catch((err) => {
|
1240 | logger.silly('Unable to determine product', err && err.message ? err.message : err);
|
1241 | deferred.reject(err);
|
1242 | });
|
1243 | }
|
1244 | });
|
1245 |
|
1246 | return deferred.promise;
|
1247 | },
|
1248 | /**
|
1249 | * Compares two software version numbers (e.g. "1.7.1" or "1.2b").
|
1250 | *
|
1251 | * This function is based on https://gist.github.com/TheDistantSea/8021359
|
1252 | *
|
1253 | * @param {string} v1 The first version to be compared.
|
1254 | * @param {string} v2 The second version to be compared.
|
1255 | * @param {object} [options] Optional flags that affect comparison behavior:
|
1256 | * <ul>
|
1257 | * <li>
|
1258 | * <tt>zeroExtend: true</tt> changes the result if one version
|
1259 | * string has less parts than the other. In
|
1260 | * this case the shorter string will be padded with "zero"
|
1261 | * parts instead of being considered smaller.
|
1262 | * </li>
|
1263 | * </ul>
|
1264 | * @returns {number}
|
1265 | * <ul>
|
1266 | * <li>0 if the versions are equal</li>
|
1267 | * <li>a negative integer iff v1 < v2</li>
|
1268 | * <li>a positive integer iff v1 > v2</li>
|
1269 | * </ul>
|
1270 | *
|
1271 | * @copyright by Jon Papaioannou (["john", "papaioannou"].join(".") + "@gmail.com"),
|
1272 | * Eugene Molotov (["eugene", "m92"].join(".") + "@gmail.com")
|
1273 | * @license This function is in the public domain. Do what you want with it, no strings attached.
|
1274 | */
|
1275 | versionCompare(v1, v2, options) {
|
1276 | function compareParts(v1parts, v2parts, zeroExtend) {
|
1277 | if (zeroExtend) {
|
1278 | while (v1parts.length < v2parts.length) v1parts.push('0');
|
1279 | while (v2parts.length < v1parts.length) v2parts.push('0');
|
1280 | }
|
1281 |
|
1282 | for (let i = 0; i < v1parts.length; i++) {
|
1283 | if (v2parts.length === i) {
|
1284 | return 1;
|
1285 | }
|
1286 |
|
1287 | let v1part = parseInt(v1parts[i], 10);
|
1288 | let v2part = parseInt(v2parts[i], 10);
|
1289 | const v1partIsString = (Number.isNaN(v1part));
|
1290 | const v2partIsString = (Number.isNaN(v2part));
|
1291 | v1part = v1partIsString ? v1parts[i] : v1part;
|
1292 | v2part = v2partIsString ? v2parts[i] : v2part;
|
1293 |
|
1294 | if (v1partIsString === v2partIsString) {
|
1295 | if (v1partIsString === false) {
|
1296 | // integer compare
|
1297 | if (v1part > v2part) {
|
1298 | return 1;
|
1299 | } else if (v1part < v2part) {
|
1300 | return -1;
|
1301 | }
|
1302 | } else {
|
1303 | // letters and numbers in string
|
1304 | // split letters and numbers
|
1305 | const v1subparts = v1part.match(/[a-zA-Z]+|[0-9]+/g);
|
1306 | const v2subparts = v2part.match(/[a-zA-Z]+|[0-9]+/g);
|
1307 | if ((v1subparts.length === 1) && (v2subparts.length === 1)) {
|
1308 | // only letters in string
|
1309 | v1part = v1subparts[0];
|
1310 | v2part = v2subparts[0];
|
1311 | if (v1part > v2part) {
|
1312 | return 1;
|
1313 | } else if (v1part < v2part) {
|
1314 | return -1;
|
1315 | }
|
1316 | }
|
1317 |
|
1318 | if (v1part !== v2part) {
|
1319 | const result = compareParts(v1subparts, v2subparts);
|
1320 | if (result !== 0) {
|
1321 | return result;
|
1322 | }
|
1323 | }
|
1324 | }
|
1325 | } else {
|
1326 | return v2partIsString ? 1 : -1;
|
1327 | }
|
1328 | }
|
1329 |
|
1330 | if (v1parts.length !== v2parts.length) {
|
1331 | return -1;
|
1332 | }
|
1333 |
|
1334 | return 0;
|
1335 | }
|
1336 |
|
1337 | const v1split = v1.split(/[.-]/);
|
1338 | const v2split = v2.split(/[.-]/);
|
1339 | const zeroExtend = options && options.zeroExtend;
|
1340 | return compareParts(v1split, v2split, zeroExtend);
|
1341 | },
|
1342 | /**
|
1343 | * Writes UCS file to disk
|
1344 | *
|
1345 | * @returns {Promise} A promise which is resolved upon completion of
|
1346 | * the script or rejected if an error occurs.
|
1347 | */
|
1348 | writeUcsFile(ucsFilePath, ucsData) {
|
1349 | const deferred = q.defer();
|
1350 | let ucsFile;
|
1351 | // If ucsData has a pipe method (is a stream), use it
|
1352 | if (ucsData.pipe) {
|
1353 | logger.silly('ucsData is a Stream');
|
1354 | if (!fs.existsSync(ucsFilePath.substring(0, ucsFilePath.lastIndexOf('/')))) {
|
1355 | fs.mkdirSync(ucsFilePath.substring(0, ucsFilePath.lastIndexOf('/')), { recursive: true });
|
1356 | }
|
1357 | ucsFile = fs.createWriteStream(ucsFilePath);
|
1358 |
|
1359 | ucsData.pipe(ucsFile);
|
1360 |
|
1361 | ucsFile.on('finish', () => {
|
1362 | logger.silly('finished piping ucsData');
|
1363 | ucsFile.close(() => {
|
1364 | deferred.resolve(true);
|
1365 | });
|
1366 | });
|
1367 | ucsFile.on('error', (err) => {
|
1368 | logger.silly('Error piping ucsData', err);
|
1369 | deferred.reject(err);
|
1370 | });
|
1371 | ucsData.on('error', (err) => {
|
1372 | logger.info('Error reading ucs data', err);
|
1373 | deferred.reject(err);
|
1374 | });
|
1375 | } else {
|
1376 | // Otherwise, assume it's a Buffer
|
1377 | logger.silly('ucsData is a Buffer');
|
1378 | logger.silly(`ucsFilePath: ${ucsFilePath}`);
|
1379 | try {
|
1380 | fs.mkdirSync(ucsFilePath.substr(0, ucsFilePath.lastIndexOf('/')));
|
1381 | } catch (err) {
|
1382 | if (err.code !== 'EEXIST') {
|
1383 | deferred.reject(err);
|
1384 | }
|
1385 | }
|
1386 | fs.writeFile(ucsFilePath, ucsData, (err) => {
|
1387 | logger.silly('finished writing ucsData');
|
1388 | if (err) {
|
1389 | logger.silly('Error writing ucsData', err);
|
1390 | deferred.reject(err);
|
1391 | return;
|
1392 | }
|
1393 | deferred.resolve(true);
|
1394 | });
|
1395 | }
|
1396 |
|
1397 | return deferred.promise;
|
1398 | },
|
1399 |
|
1400 | };
|
1401 |
|
1402 | /**
|
1403 | * Copies all the saved arguments (from saveArgs) to the startup file
|
1404 | * so that when the box reboots, the arguments are executed.
|
1405 | */
|
1406 | function prepareArgsForReboot() {
|
1407 | const deferred = q.defer();
|
1408 | let startupScripts;
|
1409 | let startupCommands;
|
1410 | let startupCommandsChanged;
|
1411 |
|
1412 | const STARTUP_DIR = '/config/';
|
1413 | const STARTUP_FILE = `${STARTUP_DIR}startup`;
|
1414 | const REBOOT_SIGNAL = ipc.signalBasePath + signals.REBOOT;
|
1415 |
|
1416 | if (!fs.existsSync(STARTUP_DIR)) {
|
1417 | logger.debug('No /config directory. Skipping.');
|
1418 | deferred.resolve();
|
1419 | return deferred.promise;
|
1420 | }
|
1421 |
|
1422 | try {
|
1423 | startupCommands = fs.readFileSync(STARTUP_FILE, 'utf8');
|
1424 | } catch (err) {
|
1425 | logger.warn('Error reading starup file.');
|
1426 | deferred.reject(err);
|
1427 | return deferred.promise;
|
1428 | }
|
1429 |
|
1430 | try {
|
1431 | startupScripts = fs.readdirSync(REBOOT_SCRIPTS_DIR);
|
1432 | } catch (err) {
|
1433 | logger.warn('Error reading directory with reboot args.');
|
1434 | deferred.reject(err);
|
1435 | return deferred.promise;
|
1436 | }
|
1437 |
|
1438 | // Make sure there's a new line at the end in case we add anything
|
1439 | startupCommands += EOL;
|
1440 |
|
1441 | // If we just rebooted, make sure the REBOOT signal file is deleted
|
1442 | // so scripts don't think we need to reboot again
|
1443 | if (startupCommands.indexOf(REBOOT_SIGNAL) === -1) {
|
1444 | startupCommandsChanged = true;
|
1445 | startupCommands += `rm -f ${REBOOT_SIGNAL}${EOL}`;
|
1446 | }
|
1447 |
|
1448 | startupScripts.forEach((script) => {
|
1449 | const fullPath = REBOOT_SCRIPTS_DIR + script;
|
1450 | if (startupCommands.indexOf(fullPath) === -1) {
|
1451 | startupCommandsChanged = true;
|
1452 | startupCommands += `if [ -f ${fullPath} ]; then${EOL}`;
|
1453 | startupCommands += ` ${fullPath} &${EOL}`;
|
1454 | startupCommands += `fi${EOL}`;
|
1455 | startupCommands += EOL;
|
1456 | }
|
1457 | });
|
1458 |
|
1459 | if (startupCommandsChanged) {
|
1460 | try {
|
1461 | fs.writeFileSync(STARTUP_FILE, startupCommands);
|
1462 | } catch (err) {
|
1463 | logger.warn('Failed writing startup file', STARTUP_FILE, err);
|
1464 | }
|
1465 | }
|
1466 |
|
1467 | deferred.resolve();
|
1468 |
|
1469 | return deferred.promise;
|
1470 | }
|