UNPKG

80.1 kBJavaScriptView Raw
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'use strict';
18
19const fs = require('fs');
20const q = require('q');
21const CloudProvider = require('../lib/cloudProvider');
22const util = require('../lib/util');
23const cryptoUtil = require('../lib/cryptoUtil');
24const childProcess = require('child_process');
25
26const BigIp = require('../lib/bigIp');
27const AutoscaleInstance = require('../lib/autoscaleInstance');
28const Logger = require('../lib/logger');
29const cloudProviderFactory = require('../lib/cloudProviderFactory');
30const dnsProviderFactory = require('../lib/dnsProviderFactory');
31const ipc = require('../lib/ipc');
32const commonOptions = require('./commonOptions');
33const BACKUP = require('../lib/sharedConstants').BACKUP;
34
35(function run() {
36 const DEFAULT_MAX_DISCONNECTED_MS = 3 * 60000; // 3 minute
37 const MIN_MS_BETWEEN_JOIN_REQUESTS = 5 * 60000; // 5 minutes
38 const PRIMARY_FILE_PATH = '/config/cloud/master';
39
40 const PASSPHRASE_LENGTH = 18;
41
42 const AUTOSCALE_PRIVATE_KEY = 'cloudLibsAutoscalePrivate.key';
43 const AUTOSCALE_PRIVATE_KEY_FOLDER = 'CloudLibsAutoscale';
44
45 const UCS_BACKUP_PREFIX = 'ucsAutosave_';
46 const UCS_BACKUP_DEFAULT_MAX_FILES = 7;
47 const UCS_BACKUP_DIRECTORY = '/var/local/ucs';
48 const DEFAULT_AUTOSCALE_TIMEOUT_IN_MINUTES = 10;
49
50 let logger;
51
52 const runner = {
53
54 /**
55 * Runs the autoscale script
56 *
57 * Provider is passed in only for testing. In production, provider will be instantiated
58 * based on the --cloud option
59 *
60 * @param {String[]} argv - The process arguments.
61 * @param {Ojbect} [testOpts] - Options used during testing
62 * @param {Object} [testOpts.autoscleProvider] - Mock for provider.
63 * @param {Object} [testOpts.bigIp] - Mock for BigIp.
64 * @param {Function} [cb] - Optional cb to call when done
65 */
66 run(argv, testOpts, cb) {
67 const DEFAULT_LOG_FILE = '/tmp/autoscale.log';
68 const ARGS_FILE_ID = `autoscale_${Date.now()}`;
69 const KEYS_TO_MASK = ['-p', '--password', '--big-iq-password'];
70
71 const OPTIONS_TO_UNDEFINE = [
72 'bigIqPasswordUri',
73 'bigIqPassword',
74 'password',
75 'passwordUrl'
76 ];
77
78 const loggerOptions = {};
79 const providerOptions = {};
80 const dnsProviderOptions = {};
81 const optionsForTest = {};
82
83 let externalTag = {};
84 let bigIp;
85 let loggableArgs;
86 let logFileName;
87 let primaryInstance;
88 let primaryIid;
89 let primaryBad;
90 let primaryBadReason;
91 let newPrimary;
92 let cloudProvider;
93 let dnsProvider;
94
95 Object.assign(optionsForTest, testOpts);
96
97 try {
98 /* eslint-disable max-len */
99 const options = commonOptions.getCommonOptions(DEFAULT_LOG_FILE)
100 .option(
101 '--cloud <cloud_provider>',
102 'Cloud provider (aws | azure | etc.)'
103 )
104 .option(
105 '--provider-options <cloud_options>',
106 'Options specific to cloud_provider. Ex: param1:value1,param2:value2',
107 util.map,
108 providerOptions
109 )
110 .option(
111 '-c, --cluster-action <type>',
112 'join (join a cluster) | update (update cluster to match existing instances | unblock-sync (allow other devices to sync to us) | backup-ucs (save a ucs to cloud storage)'
113 )
114 .option(
115 '--device-group <device_group>',
116 'Device group name.'
117 )
118 .option(
119 ' --full-load-on-sync',
120 ' Enable full load on sync. Default false.'
121 )
122 .option(
123 ' --asm-sync',
124 ' Enable ASM sync. Default sets ASM sync if ASM is provisioned.'
125 )
126 .option(
127 ' --network-failover',
128 ' Enable network failover. Default false.'
129 )
130 .option(
131 ' --no-auto-sync',
132 ' Enable auto sync. Default false (auto sync).'
133 )
134 .option(
135 ' --no-save-on-auto-sync',
136 ' Enable save on sync if auto sync is enabled. Default false (save on auto sync).'
137 )
138 .option(
139 '--block-sync',
140 'If this device is primary, do not allow other devices to sync to us. This prevents other devices from syncing to it until we are called again with --cluster-action unblock-sync.'
141 )
142 .option(
143 '--static',
144 'Indicates that this instance is not autoscaled. Default false (instance is autoscaled)'
145 )
146 .option(
147 '--external-tag <tag>',
148 'If there are instances in the autoscale cluster that are not autoscaled, the cloud tag applied to those instances. Format \'key:<tag_key>,value:<tag_value>\'', util.map, externalTag
149 )
150 .option(
151 '--license-pool',
152 'BIG-IP was licensed from a BIG-IQ license pool. This is so licenses can be revoked when BIG-IPs are scaled in. Supply the following:'
153 )
154 .option(
155 ' --big-iq-host <ip_address or FQDN>',
156 ' IP address or FQDN of BIG-IQ'
157 )
158 .option(
159 ' --big-iq-user <user>',
160 ' BIG-IQ admin user name'
161 )
162 .option(
163 ' --big-iq-password [password]',
164 ' BIG-IQ admin user password.'
165 )
166 .option(
167 ' --big-iq-password-uri [password_uri]',
168 ' URI (file, http(s), arn) to location that contains BIG-IQ admin user password. Use this or --big-iq-password.'
169 )
170 .option(
171 ' --big-iq-password-encrypted',
172 ' Indicates that the BIG-IQ password is encrypted.'
173 )
174 .option(
175 ' --license-pool-name <pool_name>',
176 ' Name of BIG-IQ license pool.'
177 )
178 .option(
179 ' --big-ip-mgmt-address <big_ip_address>',
180 ' IP address or FQDN of BIG-IP management port. Use this if BIG-IP reports an address not reachable from BIG-IQ.'
181 )
182 .option(
183 ' --big-ip-mgmt-port <big_ip_port>',
184 ' Port for the management address. Use this if the BIG-IP is not reachable from BIG-IQ via the port used in --port'
185 )
186 .option(
187 ' --no-unreachable',
188 ' Do not use the unreachable API even if it is supported by BIG-IQ.'
189 )
190 .option(
191 '--dns <dns_provider>',
192 ' Update the specified DNS provider when autoscaling occurs (gtm is the only current provider)'
193 )
194 .option(
195 ' --dns-ip-type <address_type>',
196 ' Type of ip address to use (public | private).'
197 )
198 .option(
199 ' --dns-app-port <port>',
200 ' Port on which application is listening on for health check'
201 )
202 .option(
203 ' --dns-provider-options <dns_provider_options>',
204 ' Options specific to dns_provider. Ex: param1:value1,param2:value2',
205 util.map,
206 dnsProviderOptions
207 )
208 .option(
209 '--max-ucs-files <max_ucs_files_to_save>',
210 'When running cluster action backup-ucs, maximum number of backup files to keep.',
211 UCS_BACKUP_DEFAULT_MAX_FILES
212 )
213 .option(
214 '--autoscale-timeout <autoscale_timeout>',
215 'Number of minutes after which autoscale process should be killed',
216 DEFAULT_AUTOSCALE_TIMEOUT_IN_MINUTES
217 )
218 .option(
219 '--primary-disconnected-time <primary_disconnected_time>',
220 'Time (in milliseconds) after which primary host is considered to be expired',
221 DEFAULT_MAX_DISCONNECTED_MS
222 )
223 .parse(argv);
224 /* eslint-enable max-len */
225
226 loggerOptions.console = options.console;
227 loggerOptions.logLevel = options.logLevel;
228 loggerOptions.module = module;
229
230 if (options.output) {
231 loggerOptions.fileName = options.output;
232 }
233
234 logger = Logger.getLogger(loggerOptions);
235 util.setLoggerOptions(loggerOptions);
236 cryptoUtil.setLoggerOptions(loggerOptions);
237
238 // Remove specific options with no provided value
239 OPTIONS_TO_UNDEFINE.forEach((opt) => {
240 if (typeof options[opt] === 'boolean') {
241 logger.debug(`No value set for option ${opt}. Removing option.`);
242 options[opt] = undefined;
243 }
244 });
245
246 // Expose options for test code
247 this.options = options;
248
249 if (!options.password && !options.passwordUrl) {
250 util.logAndExit('One of --password or --password-url is required.', 'error', 1);
251 }
252
253 // When running in cloud init, we need to exit so that cloud init can complete and
254 // allow the BIG-IP services to start
255 if (options.background) {
256 logFileName = options.output || DEFAULT_LOG_FILE;
257 logger.info('Spawning child process to do the work. Output will be in', logFileName);
258 util.runInBackgroundAndExit(process, logFileName);
259 }
260
261 // Log the input, but don't log passwords
262 loggableArgs = argv.slice();
263 for (let i = 0; i < loggableArgs.length; i++) {
264 if (KEYS_TO_MASK.indexOf(loggableArgs[i]) !== -1) {
265 loggableArgs[i + 1] = '*******';
266 }
267 }
268 logger.info(`${loggableArgs[1]} called with`, loggableArgs.join(' '));
269
270 // Get the concrete autoscale provider instance
271 cloudProvider = optionsForTest.cloudProvider;
272 if (!cloudProvider) {
273 cloudProvider = cloudProviderFactory.getCloudProvider(
274 options.cloud,
275 {
276 loggerOptions,
277 clOptions: options
278 }
279 );
280 }
281
282 // If updating DNS, get the concrete DNS provider instance
283 if (options.dns) {
284 dnsProvider = dnsProviderFactory.getDnsProvider(
285 options.dns,
286 {
287 loggerOptions,
288 clOptions: options
289 }
290 );
291 }
292
293 // Save args in restart script in case we need to reboot to recover from an error
294 util.saveArgs(argv, ARGS_FILE_ID)
295 .then(() => {
296 if (options.waitFor) {
297 logger.info('Waiting for', options.waitFor);
298 return ipc.once(options.waitFor);
299 }
300 return q();
301 })
302 .then(() => {
303 // Whatever we're waiting for is done, so don't wait for
304 // that again in case of a reboot
305 return util.saveArgs(argv, ARGS_FILE_ID, ['--wait-for']);
306 })
307 .then(() => {
308 if (options.clusterAction === 'join' || options.clusterAction === 'update') {
309 return getAutoscaleProcessInfo();
310 }
311 return q();
312 })
313 .then((results) => {
314 // Stop processing if there is an other running Autoscale process
315 // with cluster action of join or update
316 if (results && results.processCount && results.processCount > 1) {
317 logger.silly(`Running process count: ${results.processCount}`);
318 logger.silly(`Execution time in mins: ${parseInt(results.executionTime, 10)}`);
319 if (results.executionTime
320 && parseInt(results.executionTime, 10) < options.autoscaleTimeout) {
321 util.logAndExit('Another autoscale process already running. ' +
322 'Exiting.', 'warn', 1);
323 return q.reject('Another autoscale process already running.');
324 }
325 logger.info('Terminating the autoscale script execution.');
326 util.terminateProcessById(results.pid);
327 util.logAndExit('Long running autoscale processed terminated. Exiting...');
328 return q.reject('Long running autoscale processed terminated. Exiting...');
329 }
330 return q();
331 })
332 .then(() => {
333 logger.info('Initializing autoscale provider');
334 return cloudProvider.init(providerOptions, { autoscale: true });
335 })
336 .then(() => {
337 if (options.dns) {
338 logger.info('Initializing DNS provider');
339 return dnsProvider.init(dnsProviderOptions);
340 }
341 return q();
342 })
343 .then(() => {
344 logger.info('Getting this instance ID.');
345 return cloudProvider.getInstanceId();
346 })
347 .then((response) => {
348 logger.debug('This instance ID:', response);
349 this.instanceId = response;
350
351 logger.info('Getting info on all instances.');
352 if (Object.keys(externalTag).length === 0) {
353 externalTag = undefined;
354 }
355 return cloudProvider.getInstances({
356 externalTag,
357 instanceId: response
358 });
359 })
360 .then((response) => {
361 this.instances = response || {};
362 logger.debug('instances:', this.instances);
363
364 if (Object.keys(this.instances).length === 0) {
365 util.logAndExit('Instance list is empty. Exiting.', 'error', 1);
366 return q.reject('Instance list is empty. Exiting.');
367 }
368
369 this.instance = this.instances[this.instanceId];
370 if (!this.instance) {
371 util.logAndExit('Our instance ID is not in instance list. Exiting', 'error', 1);
372 return q.reject('Our instance ID is not in instance list. Exiting');
373 }
374
375 this.instance.status = this.instance.status || AutoscaleInstance.INSTANCE_STATUS_OK;
376 logger.silly('Instance status:', this.instance.status);
377
378 if (this.instance.status === AutoscaleInstance.INSTANCE_STATUS_BECOMING_PRIMARY
379 && !isPrimaryExpired(this.instance, options)) {
380 util.logAndExit('Currently becoming primary. Exiting.', 'info');
381 return q.reject('Currently becoming primary. Exiting.');
382 }
383
384 if (optionsForTest.bigIp) {
385 bigIp = optionsForTest.bigIp;
386 return q();
387 }
388 bigIp = new BigIp({ loggerOptions });
389
390 logger.info('Initializing BIG-IP.');
391 return bigIp.init(
392 options.host,
393 options.user,
394 options.password || options.passwordUrl,
395 {
396 port: options.port,
397 passwordIsUrl: typeof options.passwordUrl !== 'undefined',
398 passwordEncrypted: options.passwordEncrypted
399 }
400 );
401 })
402 .then(() => {
403 return bigIp.list('/tm/sys/global-settings');
404 })
405 .then((globalSettings) => {
406 this.instance.hostname = globalSettings.hostname;
407 return cloudProvider.putInstance(this.instanceId, this.instance);
408 })
409 .then(() => {
410 return cloudProvider.bigIpReady();
411 })
412 .then(() => {
413 return bigIp.deviceInfo();
414 })
415 .then((response) => {
416 this.instance.machineId = response.machineId; // we need this for revoke on BIG-IQ 5.3
417 this.instance.macAddress = response.hostMac; // we need this for revoke on BIG-IQ 5.4
418 this.instance.version = response.version;
419 markVersions(this.instances);
420 return cloudProvider.putInstance(this.instanceId, this.instance);
421 })
422 .then(() => {
423 let status = CloudProvider.STATUS_UNKNOWN;
424
425 logger.info('Determining primary instance id.');
426 primaryInstance = getPrimaryInstance(this.instances);
427
428 if (primaryInstance) {
429 if (!primaryInstance.instance.versionOk) {
430 primaryBadReason = 'version not most recent in group';
431 logger.silly(primaryBadReason);
432 status = CloudProvider.STATUS_VERSION_NOT_UP_TO_DATE;
433 primaryBad = true;
434 } else if (!isPrimaryExternalValueOk(primaryInstance.id, this.instances)) {
435 // if there are external instances in the mix, make sure the primary
436 // is one of them
437 primaryBadReason = 'primary is not external, ' +
438 'but there are external instances';
439 logger.silly(primaryBadReason);
440 status = CloudProvider.STATUS_NOT_EXTERNAL;
441 primaryBad = true;
442 } else if (!primaryInstance.instance.providerVisible) {
443 // The cloud provider does not currently see this instance
444 status = CloudProvider.STATUS_NOT_IN_CLOUD_LIST;
445 } else {
446 primaryIid = primaryInstance.id;
447
448 if (this.instanceId === primaryIid) {
449 this.instance.isPrimary = true;
450 }
451
452 status = CloudProvider.STATUS_OK;
453 }
454 }
455
456 return updatePrimaryStatus.call(this, cloudProvider, status);
457 })
458 .then(() => {
459 // If the primary is not visible, check to see if it's been gone
460 // for a while or if this is a random error
461 if (primaryInstance && !primaryBad && isPrimaryExpired(this.instance, options)) {
462 primaryBad = true;
463 primaryBadReason = 'primary is expired';
464 }
465
466 if (primaryIid) {
467 logger.info('Possible primary ID:', primaryIid);
468 return cloudProvider.isValidPrimary(primaryIid, this.instances);
469 } else if (primaryBad) {
470 logger.info('Old primary no longer valid:', primaryBadReason);
471 return q();
472 }
473
474 logger.info('No primary ID found.');
475 return q();
476 })
477 .then((validPrimary) => {
478 logger.silly(
479 'validPrimary:',
480 validPrimary,
481 ', primaryInstance: ',
482 primaryInstance,
483 ', primaryBad:',
484 primaryBad
485 );
486
487 if (validPrimary) {
488 // true validPrimary means we have a valid primaryIid, just pass it on
489 logger.info('Valid primary ID:', primaryIid);
490 return primaryIid;
491 }
492
493 // false or undefined validPrimary means no primaryIid or invalid primaryIid
494 if (validPrimary === false) {
495 logger.info('Invalid primary ID:', primaryIid);
496 cloudProvider.primaryInvalidated(primaryIid);
497 }
498
499 // if no primary, primary is visible or expired, elect, otherwise, wait
500 if (!primaryInstance ||
501 primaryInstance.instance.providerVisible ||
502 primaryBad) {
503 logger.info('Electing primary.');
504 return cloudProvider.electPrimary(this.instances);
505 }
506 return q();
507 })
508 .then((response) => {
509 const now = new Date();
510
511 if (response) {
512 // we just elected a primary
513 primaryIid = response;
514 this.instance.isPrimary = (this.instanceId === primaryIid);
515 logger.info('Using primary ID:', primaryIid);
516 logger.info(
517 'This instance',
518 (this.instance.isPrimary ? 'is' : 'is not'),
519 'primary'
520 );
521
522 if (this.instance.primaryStatus.instanceId !== primaryIid) {
523 logger.info('New primary elected');
524 newPrimary = true;
525
526 this.instance.primaryStatus = {
527 instanceId: primaryIid,
528 status: CloudProvider.STATUS_OK,
529 lastUpdate: now,
530 lastStatusChange: now
531 };
532
533 return cloudProvider.putInstance(this.instanceId, this.instance);
534 }
535 }
536 return q();
537 })
538 .then(() => {
539 if (this.instance.isPrimary && newPrimary) {
540 this.instance.status = AutoscaleInstance.INSTANCE_STATUS_BECOMING_PRIMARY;
541 return cloudProvider.putInstance(this.instanceId, this.instance);
542 }
543 return q();
544 })
545 .then(() => {
546 if (this.instance.isPrimary && newPrimary) {
547 return becomePrimary.call(this, cloudProvider, bigIp, options);
548 }
549 return q();
550 })
551 .then((response) => {
552 if (
553 this.instance.status === AutoscaleInstance.INSTANCE_STATUS_BECOMING_PRIMARY &&
554 response === true
555 ) {
556 this.instance.status = AutoscaleInstance.INSTANCE_STATUS_OK;
557 logger.silly('Became primary');
558 return cloudProvider.putInstance(this.instanceId, this.instance);
559 } else if (response === false) {
560 logger.warn('Error writing primary file');
561 }
562 return q();
563 })
564 .then(() => {
565 if (primaryIid && this.instance.status === AutoscaleInstance.INSTANCE_STATUS_OK) {
566 return cloudProvider.primaryElected(primaryIid);
567 }
568 return q();
569 })
570 .then(() => {
571 if (primaryIid && this.instance.status === AutoscaleInstance.INSTANCE_STATUS_OK) {
572 return cloudProvider.tagPrimaryInstance(primaryIid, this.instances);
573 }
574 return q();
575 })
576 .then(() => {
577 let message;
578 if (this.instance.status === AutoscaleInstance.INSTANCE_STATUS_OK) {
579 switch (options.clusterAction) {
580 case 'join':
581 logger.info('Cluster action join');
582 return handleJoin.call(
583 this,
584 cloudProvider,
585 bigIp,
586 primaryIid,
587 options
588 );
589 case 'update':
590 logger.info('Cluster action update');
591 return bigIp.deviceState(this.instance.hostname)
592 .then((response) => {
593 if (response && response.configsyncIp !== this.instance.privateIp) {
594 return bigIp.cluster.configSyncIp(this.instance.privateIp);
595 }
596 return q();
597 })
598 .then(() => {
599 return handleUpdate.call(
600 this,
601 cloudProvider,
602 bigIp,
603 primaryIid,
604 primaryBad || newPrimary,
605 options
606 );
607 });
608 case 'unblock-sync':
609 logger.info('Cluster action unblock-sync');
610 return bigIp.cluster.configSyncIp(this.instance.privateIp);
611 case 'backup-ucs': {
612 logger.info('Cluster action backup-ucs');
613 const clusterMetadata = {
614 instanceId: this.instanceId,
615 instance: this.instance,
616 instances: this.instances
617 };
618 return handleBackupUcs.call(
619 this,
620 cloudProvider,
621 clusterMetadata,
622 bigIp,
623 options
624 );
625 }
626 default:
627 message = `Unknown cluster action ${options.clusterAction}`;
628 logger.warn(message);
629 return q.reject(message);
630 }
631 } else {
632 logger.debug('Instance status not OK. Waiting.', this.instance.status);
633 return q();
634 }
635 })
636 .then(() => {
637 if (this.instance.status === AutoscaleInstance.INSTANCE_STATUS_OK) {
638 if (cloudProvider.hasFeature(CloudProvider.FEATURE_MESSAGING)
639 && (options.clusterAction === 'join' || options.clusterAction === 'update')) {
640 logger.info('Checking for messages');
641 return handleMessages.call(this, cloudProvider, bigIp, options);
642 }
643 } else {
644 logger.debug('Instance status not OK. Waiting.', this.instance.status);
645 }
646 return q();
647 })
648 .then(() => {
649 if (options.dns
650 && (options.clusterAction === 'join' || options.clusterAction === 'update')) {
651 logger.info('Updating DNS');
652
653 const instancesForDns = [];
654
655 Object.keys(this.instances).forEach((instanceId) => {
656 const instance = this.instances[instanceId];
657 const ip =
658 (options.dnsIpType === 'public' ? instance.publicIp : instance.privateIp);
659
660 if (instance.hostname) {
661 instancesForDns.push(
662 {
663 ip,
664 name: instance.hostname,
665 port: options.dnsAppPort
666 }
667 );
668 }
669 });
670 return dnsProvider.update(instancesForDns);
671 }
672 return q();
673 })
674 .catch((err) => {
675 if (err && err.code && err.message) {
676 logger.error('autoscaling error code:', err.code, 'message:', err.message);
677 } else {
678 logger.error('autoscaling error:', err && err.message ? err.message : err);
679 }
680 return err;
681 })
682 .done((err) => {
683 util.deleteArgs(ARGS_FILE_ID);
684
685 if (cb) {
686 cb(err);
687 }
688
689 // Exit so that any listeners don't keep us alive
690 util.logAndExit('Autoscale finished.');
691 });
692
693 // If we reboot, exit - otherwise cloud providers won't know we're done
694 ipc.once('REBOOT')
695 .then(() => {
696 util.logAndExit('REBOOT signaled. Exiting.');
697 });
698 } catch (err) {
699 if (logger) {
700 logger.error('autoscale error:', err);
701 }
702
703 if (cb) {
704 cb();
705 }
706 }
707 }
708 };
709
710 /**
711 * Handles --cluster-action join
712 *
713 * Called with this bound to the caller
714 */
715 function handleJoin(provider, bigIp, primaryIid, options) {
716 const deferred = q.defer();
717
718 logger.info('Cluster action JOIN');
719
720 logger.info('Initializing encryption');
721 initEncryption.call(this, provider, bigIp)
722 .then(() => {
723 let promise;
724
725 // If we are primary and are replacing an expired primary, other instances
726 // will join to us. Just set our config sync ip.
727 if (this.instance.isPrimary) {
728 if (!provider.hasFeature(CloudProvider.FEATURE_MESSAGING)) {
729 logger.info('Storing primary credentials.');
730 promise = provider.putPrimaryCredentials();
731 } else {
732 promise = q();
733 }
734
735 promise
736 .then((response) => {
737 logger.debug(response);
738
739 // Configure cm configsync-ip on this BIG-IP node
740 if (!options.blockSync) {
741 logger.info('Setting config sync IP.');
742 return bigIp.cluster.configSyncIp(this.instance.privateIp);
743 }
744 logger.info('Not seting config sync IP because block-sync is specified.');
745 return q();
746 })
747 .then(() => {
748 deferred.resolve();
749 })
750 .catch((err) => {
751 // rethrow here, otherwise error is hidden
752 throw err;
753 });
754 } else {
755 // We're not the primary
756
757 // Make sure the primary file is not on our disk
758 if (fs.existsSync(PRIMARY_FILE_PATH)) {
759 fs.unlinkSync(PRIMARY_FILE_PATH);
760 }
761
762 // Configure cm configsync-ip on this BIG-IP node and join the cluster
763 logger.info('Setting config sync IP.');
764 bigIp.cluster.configSyncIp(this.instance.privateIp)
765 .then(() => {
766 // If there is a primary, join it. Otherwise wait for an update event
767 // when we have a primary.
768 if (primaryIid) {
769 return joinCluster.call(this, provider, bigIp, primaryIid, options);
770 }
771 return q();
772 })
773 .then(() => {
774 deferred.resolve();
775 })
776 .catch((err) => {
777 // rethrow here, otherwise error is hidden
778 throw err;
779 });
780 }
781 });
782
783 return deferred.promise;
784 }
785
786 /**
787 * Handles --cluster-action update
788 *
789 * Called with this bound to the caller
790 */
791 function handleUpdate(provider, bigIp, primaryIid, primaryBadOrNew, options) {
792 logger.info('Cluster action UPDATE');
793
794 if (this.instance.isPrimary && !primaryBadOrNew) {
795 return checkClusteredDevices.call(this, provider, bigIp);
796 } else if (!this.instance.isPrimary) {
797 // We're not the primary, make sure the primary file is not on our disk
798 if (fs.existsSync(PRIMARY_FILE_PATH)) {
799 fs.unlinkSync(PRIMARY_FILE_PATH);
800 }
801
802 // If there is a new primary, join the cluster
803 if (primaryBadOrNew && primaryIid) {
804 return joinCluster.call(this, provider, bigIp, primaryIid, options);
805 } else if (primaryIid) {
806 // Double check that we are clustered
807 return bigIp.list('/tm/cm/trust-domain/Root')
808 .then((response) => {
809 if (!response || response.status === 'standalone') {
810 logger.info('This instance is not in cluster. Requesting join.');
811 return joinCluster.call(this, provider, bigIp, primaryIid, options);
812 }
813
814 return q();
815 })
816 .catch((err) => {
817 throw err;
818 });
819 }
820 }
821 return q();
822 }
823
824 /**
825 * Called with this bound to the caller
826 */
827 function handleMessages(provider, bigIp, options) {
828 const deferred = q.defer();
829 const instanceIdsBeingAdded = [];
830 const actions = [];
831 const actionPromises = [];
832
833 let messageMetadata = [];
834
835 if (this.instance.isPrimary && !options.blockSync) {
836 actions.push(CloudProvider.MESSAGE_ADD_TO_CLUSTER);
837 }
838
839 if (!this.instance.isPrimary) {
840 actions.push(CloudProvider.MESSAGE_SYNC_COMPLETE);
841 }
842
843 provider.getMessages(actions, { toInstanceId: this.instanceId })
844 .then((messages) => {
845 const readPromises = [];
846 const messagesArray = messages ? messages.slice() : [];
847
848 logger.debug('Handling', messages.length, 'message(s)');
849
850 messagesArray.forEach((message) => {
851 messageMetadata.push(
852 {
853 action: message.action,
854 toInstanceId: message.toInstanceId,
855 fromInstanceId: message.fromInstanceId
856 }
857 );
858 readPromises.push(readMessageData.call(this, provider, bigIp, message.data));
859 });
860
861 logger.silly('number of messages to read:', readPromises.length);
862
863 return q.all(readPromises);
864 })
865 .then((readMessages) => {
866 let metadata;
867 let messageData;
868
869 const alreadyAdding = function (instanceId) {
870 return instanceIdsBeingAdded.find((element) => {
871 return instanceId === element.toInstanceId;
872 });
873 };
874
875 const readMessagesArray = readMessages ? readMessages.slice() : [];
876
877 logger.silly('number of read messages:', readMessagesArray.length);
878
879 for (let i = 0; i < readMessagesArray.length; ++i) {
880 metadata = messageMetadata[i];
881 logger.silly('metadata:', metadata);
882
883 try {
884 messageData = JSON.parse(readMessagesArray[i]);
885 } catch (err) {
886 logger.warn('JSON.parse error:', err);
887 messageData = undefined;
888 deferred.reject(new Error('Unable to JSON parse message'));
889 }
890
891 if (messageData) {
892 let discard = false;
893 switch (metadata.action) {
894 // Add an instance to our cluster
895 case CloudProvider.MESSAGE_ADD_TO_CLUSTER:
896 logger.silly('message MESSAGE_ADD_TO_CLUSTER');
897
898 if (alreadyAdding(metadata.fromInstanceId)) {
899 logger.debug('Already adding', metadata.fromInstanceId, ', discarding');
900 discard = true;
901 }
902
903 if (!discard) {
904 instanceIdsBeingAdded.push({
905 toInstanceId: metadata.fromInstanceId,
906 fromUser: bigIp.user,
907 fromPassword: bigIp.password
908 });
909
910 actionPromises.push(
911 bigIp.cluster.joinCluster(
912 messageData.deviceGroup,
913 messageData.host,
914 messageData.username,
915 messageData.password,
916 true,
917 {
918 remotePort: messageData.port,
919 remoteHostname: messageData.hostname,
920 passwordEncrypted: false
921 }
922 )
923 );
924 }
925
926 break;
927
928 // sync is complete
929 case CloudProvider.MESSAGE_SYNC_COMPLETE:
930 logger.silly('message MESSAGE_SYNC_COMPLETE');
931 actionPromises.push(
932 provider.syncComplete(messageData.fromUser, messageData.fromPassword)
933 );
934
935 break;
936 default:
937 logger.warn('Unknown message action', metadata.action);
938 }
939 }
940 }
941
942 return q.all(actionPromises);
943 })
944 .then((responses) => {
945 const messagePromises = [];
946 let messageData;
947
948 const responsesArray = responses ? responses.slice() : [];
949
950 if (instanceIdsBeingAdded.length > 0) {
951 messageMetadata = [];
952
953 logger.silly('responses from join cluster', responsesArray);
954 for (let i = 0; i < responsesArray.length; i++) {
955 // responsesArray[i] === true iff that instance was successfully synced
956 if (responsesArray[i] === true) {
957 logger.silly(
958 'sync is complete for instance',
959 instanceIdsBeingAdded[i].toInstanceId
960 );
961
962 messageMetadata.push(
963 {
964 action: CloudProvider.MESSAGE_SYNC_COMPLETE,
965 toInstanceId: instanceIdsBeingAdded[i].toInstanceId,
966 fromInstanceId: this.instanceId
967 }
968 );
969 messageData = {
970 fromUser: instanceIdsBeingAdded[i].fromUser,
971 fromPassword: instanceIdsBeingAdded[i].fromPassword
972 };
973
974 messagePromises.push(
975 prepareMessageData.call(
976 this,
977 provider,
978 instanceIdsBeingAdded[i].toInstanceId,
979 JSON.stringify(messageData)
980 )
981 );
982 }
983 }
984 }
985
986 return q.all(messagePromises);
987 })
988 .then((preppedMessageData) => {
989 const syncCompletePromises = [];
990 let metadata;
991 let messageData;
992
993 for (let i = 0; i < preppedMessageData.length; i++) {
994 metadata = messageMetadata[i];
995 messageData = preppedMessageData[i];
996
997 syncCompletePromises.push(
998 provider.sendMessage(
999 CloudProvider.MESSAGE_SYNC_COMPLETE,
1000 {
1001 toInstanceId: metadata.toInstanceId,
1002 fromInstanceId: metadata.fromInstanceId,
1003 data: messageData
1004 }
1005 )
1006 );
1007 }
1008
1009 return q.all(syncCompletePromises);
1010 })
1011 .then(() => {
1012 deferred.resolve();
1013 })
1014 .catch((err) => {
1015 logger.warn('Error handling messages', err);
1016 deferred.reject(err);
1017 });
1018
1019 return deferred.promise;
1020 }
1021
1022 function validateUploadedUcs(provider, ucsFileName) {
1023 const ucsFilePath = `${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/${ucsFileName}`;
1024 return provider.getStoredUcs()
1025 .then((ucsData) => {
1026 logger.silly(`ucsFilePath: ${ucsFilePath}`);
1027 return util.writeUcsFile(ucsFilePath, ucsData);
1028 })
1029 .then(() => {
1030 return util.runShellCommand(`gzip -t -v ${ucsFilePath}`);
1031 })
1032 .then((response) => {
1033 if (response.indexOf('NOT OK') !== -1) {
1034 return q.resolve({
1035 status: 'CORRUPTED',
1036 filePath: ucsFilePath
1037 });
1038 }
1039 logger.silly('Validated integrity of recenetly generated UCS file.');
1040 return q.resolve({
1041 status: 'OK',
1042 filePath: ucsFilePath
1043 });
1044 })
1045 .catch((err) => {
1046 logger.warn('Error while validating ucs', err);
1047 return q.reject(err);
1048 });
1049 }
1050
1051 function handleBackupUcs(provider, clusterMetadata, bigIp, options) {
1052 if (!this.instance.isPrimary
1053 || this.instance.status !== AutoscaleInstance.INSTANCE_STATUS_OK) {
1054 logger.debug('not primary or not ready, skipping ucs backup');
1055 return q();
1056 }
1057
1058 const now = new Date().getTime();
1059 const ucsName = `${UCS_BACKUP_PREFIX}${now}`;
1060
1061 logger.info('Backing up UCS');
1062 // ajv, which is installed as a dependency of the Azure node SDK has a couple files that start
1063 // with '$'. prior to 13.1, Meanwhile, mcpd has a bug which fails to save a ucs if it runs
1064 // into a file that starts with a '$'. So, let's just move the file. It's a bit ugly, but
1065 // there's not really a better place to do this since it's a one-off bug. All of
1066 // f5-cloud-libs is removed before ucs is loaded, so we don't need to do anything on that end.
1067 this.instance.lastBackup = now;
1068 let isUcsFileValid = false;
1069 return cleanupAjv(bigIp)
1070 .then(() => {
1071 return bigIp.saveUcs(ucsName);
1072 })
1073 .then(() => {
1074 return provider.storeUcs(
1075 `${UCS_BACKUP_DIRECTORY}/${ucsName}.ucs`,
1076 options.maxUcsFiles,
1077 UCS_BACKUP_PREFIX
1078 );
1079 })
1080 .then(() => {
1081 logger.silly(`lastest ucs file: ${ucsName}.ucs`);
1082 return removeOldUcsFiles(`${ucsName}.ucs`);
1083 })
1084 .then(() => {
1085 return validateUploadedUcs(provider, `${ucsName}.ucs`);
1086 })
1087 .then((results) => {
1088 fs.unlinkSync(results.filePath);
1089 logger.silly('Removed local UCS file used in validation.');
1090 if (results.status !== 'OK') {
1091 provider.deleteStoredUcs(`${ucsName}.ucs`);
1092 return q.resolve();
1093 }
1094 isUcsFileValid = true;
1095 return q.resolve();
1096 })
1097 .then(() => {
1098 if (!isUcsFileValid) {
1099 return q.reject(new Error('Validation of ' +
1100 'generated UCS file failed; ' +
1101 'recently generated UCS file appears to be corrupted.'));
1102 }
1103 logger.debug(`Update instance metadata; lastBackUp to ${this.instance.lastBackup}`);
1104 return provider.putInstance(clusterMetadata.instanceId, this.instance);
1105 })
1106 .then(() => {
1107 return q.resolve();
1108 })
1109 .catch((err) => {
1110 logger.info('Error backing up ucs', err);
1111 if (fs.existsSync(`${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/${ucsName}.ucs`)) {
1112 fs.unlinkSync(`${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/${ucsName}.ucs`);
1113 }
1114 provider.deleteStoredUcs(`${ucsName}.ucs`);
1115 return q.reject(err);
1116 });
1117 }
1118
1119 /**
1120 * Handles becoming primary.
1121 *
1122 * @returns {Promise} promise which is resolved tieh true if successful
1123 */
1124 function becomePrimary(provider, bigIp, options) {
1125 let hasUcs = false;
1126 const promises = [];
1127 logger.info('Becoming primary.');
1128 logger.info('Checking if need to restore UCS.');
1129 /*
1130 - By default, lastBackup time is set to the begining of epoch (i.e. 2678400000)
1131 - When backup created, lastBackup value will be set to time of backup
1132 - lastBackup will be updated on each in-sync host to match primary lastBackup time
1133 - lastBackup will be used to confirm if a host was in-sync with previous primary to
1134 check if ucs restore is needed to copy over custom configs
1135 */
1136 if (this.instance.lastBackup === new Date(1970, 1, 1).getTime()) {
1137 logger.silly('will attempt to restore ucs; ' +
1138 'this instance never was in synced with previous primary');
1139 promises.push(provider.getStoredUcs());
1140 } else {
1141 logger.silly('no need to restore ucs; this instance was in sync with previous primary');
1142 }
1143 return Promise.all(promises)
1144 .then((response) => {
1145 if (response && response.length === 1 && response[0]) {
1146 hasUcs = true;
1147 return loadUcs(provider, bigIp, response[0], options.cloud);
1148 }
1149 return q();
1150 })
1151 .then(() => {
1152 logger.silly('setting lastBackup to current time since this instnace is primary now.');
1153 // this is done to prefer running config
1154 this.instance.lastBackup = new Date().getTime();
1155 // If we loaded UCS, re-initialize encryption so our keys
1156 // match each other and update lastBackup
1157 if (hasUcs) {
1158 return initEncryption.call(this, provider, bigIp);
1159 }
1160 return q();
1161 })
1162 .then(() => {
1163 return bigIp.list('/tm/sys/global-settings');
1164 })
1165 .then((globalSettings) => {
1166 const hostname = globalSettings ? globalSettings.hostname : undefined;
1167
1168 if (hostname) {
1169 this.instance.hostname = hostname;
1170 } else {
1171 logger.debug('hostname not found in this.instance or globalSettings');
1172 }
1173
1174 return bigIp.list('/tm/sys/provision');
1175 })
1176 .then((response) => {
1177 const modulesProvisioned = {};
1178
1179 if (response && response.length > 0) {
1180 response.forEach((module) => {
1181 modulesProvisioned[module.name] = !(module.level === 'none');
1182 });
1183 }
1184
1185 // Make sure device group exists
1186 logger.info('Creating device group.');
1187
1188 const deviceGroupOptions = {
1189 autoSync: options.autoSync,
1190 saveOnAutoSync: options.saveOnAutoSync,
1191 fullLoadOnSync: options.fullLoadOnSync,
1192 asmSync: modulesProvisioned.asm || options.asmSync,
1193 networkFailover: options.networkFailover
1194 };
1195
1196 return bigIp.cluster.createDeviceGroup(
1197 options.deviceGroup,
1198 'sync-failover',
1199 [this.instance.hostname],
1200 deviceGroupOptions
1201 );
1202 })
1203 .then(() => {
1204 logger.info('Writing primary file.');
1205 return writePrimaryFile(hasUcs);
1206 });
1207 }
1208
1209 /**
1210 * Called with this bound to the caller
1211 */
1212 function joinCluster(provider, bigIp, primaryIid, options) {
1213 const TEMP_USER_NAME_LENGHTH = 10; // these are hex bytes - user name will be 20 chars
1214 const TEMP_USER_PASSWORD_LENGTH = 24; // use a multiple of 6 to prevent '=' at the end
1215
1216 const now = new Date();
1217
1218 let managementIp;
1219 let tempPassword;
1220 let tempUser;
1221
1222 if (!primaryIid) {
1223 return q.reject(new Error('Must have a primary ID to join'));
1224 }
1225
1226 // don't send request too often - primary might be in process of syncing
1227 this.instance.lastJoinRequest = this.instance.lastJoinRequest || new Date(1970, 0);
1228 const elapsedMsFromLastJoin = now - new Date(this.instance.lastJoinRequest);
1229 if (elapsedMsFromLastJoin < MIN_MS_BETWEEN_JOIN_REQUESTS) {
1230 logger.silly('Join request is too soon after last join request.', elapsedMsFromLastJoin, 'ms');
1231 return q();
1232 }
1233
1234 logger.info('Joining cluster.');
1235
1236 if (provider.hasFeature(CloudProvider.FEATURE_MESSAGING)) {
1237 this.instance.lastJoinRequest = now;
1238 return provider.putInstance(this.instanceId, this.instance)
1239 .then((response) => {
1240 logger.debug(response);
1241 logger.debug('Resetting current device trust');
1242 return bigIp.cluster.resetTrust();
1243 })
1244 .then((response) => {
1245 logger.debug(response);
1246
1247 // Make sure we don't have a current copy of the device group
1248 return bigIp.cluster.deleteDeviceGroup(options.deviceGroup);
1249 })
1250 .then((response) => {
1251 logger.debug(response);
1252 return bigIp.deviceInfo();
1253 })
1254 .then((response) => {
1255 managementIp = response.managementAddress;
1256
1257 // Get a random user name to use
1258 return cryptoUtil.generateRandomBytes(TEMP_USER_NAME_LENGHTH, 'hex');
1259 })
1260 .then((respomse) => {
1261 tempUser = respomse;
1262
1263 // Get a random password for the user
1264 return cryptoUtil.generateRandomBytes(TEMP_USER_PASSWORD_LENGTH, 'base64');
1265 })
1266 .then((response) => {
1267 tempPassword = response;
1268
1269 // Create the temp user account
1270 return bigIp.onboard.updateUser(tempUser, tempPassword, 'admin');
1271 })
1272 .then(() => {
1273 logger.debug('Sending message to join cluster.');
1274
1275 const messageData = {
1276 host: managementIp,
1277 port: bigIp.port,
1278 username: tempUser,
1279 password: tempPassword,
1280 hostname: this.instance.hostname,
1281 deviceGroup: options.deviceGroup
1282 };
1283
1284 return prepareMessageData.call(this, provider, primaryIid, JSON.stringify(messageData));
1285 })
1286 .then((preppedData) => {
1287 if (preppedData) {
1288 return provider.sendMessage(
1289 CloudProvider.MESSAGE_ADD_TO_CLUSTER,
1290 {
1291 toInstanceId: primaryIid,
1292 fromInstanceId: this.instanceId,
1293 data: preppedData
1294 }
1295 );
1296 }
1297 logger.debug('No encrypted data received');
1298 return q();
1299 })
1300 .catch((err) => {
1301 // need to bubble up nested errors
1302 return q.reject(err);
1303 });
1304 }
1305
1306 // not using messaging, just send the request via iControl REST
1307 const primaryInstance = this.instances[primaryIid];
1308
1309 this.instance.lastJoinRequest = now;
1310 return provider.putInstance(this.instanceId, this.instance)
1311 .then((response) => {
1312 logger.debug(response);
1313 logger.debug('Resetting current device trust');
1314 return bigIp.cluster.resetTrust();
1315 })
1316 .then((response) => {
1317 logger.debug(response);
1318
1319 // Make sure we don't have a current copy of the device group
1320 return bigIp.cluster.deleteDeviceGroup(options.deviceGroup);
1321 })
1322 .then((response) => {
1323 logger.debug(response);
1324 return provider.getPrimaryCredentials(primaryInstance.mgmtIp, options.port);
1325 })
1326 .then((credentials) => {
1327 logger.debug('Sending request to join cluster.');
1328 return bigIp.cluster.joinCluster(
1329 options.deviceGroup,
1330 primaryInstance.mgmtIp,
1331 credentials.username,
1332 credentials.password,
1333 false,
1334 {
1335 remotePort: options.port
1336 }
1337 );
1338 });
1339 }
1340
1341 /**
1342 * Get the count of running Autoscale process,
1343 * its pid and current execution time with actions of join or update.
1344 */
1345 function getAutoscaleProcessInfo() {
1346 const actions = 'cluster-action update|' +
1347 '-c update|cluster-action join|' +
1348 '-c join|cluster-action backup-ucs';
1349 const grepCommand = `grep autoscale.js | grep -E '${actions}' | grep -v 'grep autoscale.js'`;
1350 const results = {};
1351
1352
1353 return util.getProcessCount(grepCommand)
1354 .then((response) => {
1355 if (response) {
1356 results.processCount = response;
1357 }
1358 return util.getProcessExecutionTimeWithPid(grepCommand);
1359 })
1360 .then((response) => {
1361 if (response) {
1362 results.pid = response.split('-')[0];
1363 results.executionTime = response.split('-')[1].split(':')[0];
1364 logger.silly(`Longer running autoscale process id: ${results.pid}`);
1365 }
1366 return q(results);
1367 })
1368 .catch((err) => {
1369 logger.error('Could not determine if another autoscale script is running');
1370 return q.reject(err);
1371 });
1372 }
1373
1374 function checkClusteredDevices(provider, bigIp) {
1375 return bigIp.cluster.getCmSyncStatus()
1376 .then((response) => {
1377 // response is an object of two lists (connected/disconnected) from getCmSyncStatus()
1378 logger.silly('cmSyncStatus:', response);
1379 const promises = [];
1380 const disconnected = response ? response.disconnected : [];
1381 const connected = response ? response.connected : [];
1382 const hostnames = [];
1383 const hostnamesToRemove = [];
1384
1385 if (disconnected.length > 0) {
1386 logger.info('Possibly disconnected devices:', disconnected);
1387
1388 // get a list of hostnames still in the instances list
1389 Object.keys(this.instances).forEach((instanceId) => {
1390 if (this.instances[instanceId].hostname) {
1391 hostnames.push(this.instances[instanceId].hostname);
1392 }
1393 });
1394
1395 // make sure this is not still in the instances list
1396 disconnected.forEach((hostname) => {
1397 if (hostnames.indexOf(hostname) === -1) {
1398 logger.info('Disconnected device:', hostname);
1399 hostnamesToRemove.push(hostname);
1400 }
1401 });
1402
1403 if (hostnamesToRemove.length > 0) {
1404 logger.info('Removing devices from cluster:', hostnamesToRemove);
1405 promises.push(bigIp.cluster.removeFromCluster(hostnamesToRemove));
1406 }
1407 }
1408
1409 if (connected.length > 0) {
1410 connected.forEach((hostaname) => {
1411 Object.keys(this.instances).forEach((instanceId) => {
1412 if (this.instances[instanceId].hostname === hostaname &&
1413 this.instances[instanceId].lastBackup < this.instance.lastBackup) {
1414 this.instances[instanceId].lastBackup = this.instance.lastBackup;
1415 logger.silly(`Update lastBackUp on connected instance: ${instanceId}`);
1416 promises.push(provider.putInstance(
1417 instanceId,
1418 this.instances[instanceId]
1419 ));
1420 }
1421 });
1422 });
1423 }
1424 return Promise.all(promises);
1425 })
1426 .catch((err) => {
1427 logger.warn('Could not get sync status');
1428 return q.reject(err);
1429 });
1430 }
1431
1432 /**
1433 * If the provider supports encryption, initializes and stores keys.
1434 *
1435 * Called with this bound to the caller.
1436 */
1437 function initEncryption(provider, bigIp) {
1438 const PRIVATE_KEY_OUT_FILE = '/tmp/tempPrivateKey.pem';
1439
1440 let passphrase;
1441
1442 if (provider.hasFeature(CloudProvider.FEATURE_ENCRYPTION)) {
1443 logger.debug('Generating public/private keys for autoscaling.');
1444 return cryptoUtil.generateRandomBytes(PASSPHRASE_LENGTH, 'base64')
1445 .then((response) => {
1446 passphrase = response;
1447 return cryptoUtil.generateKeyPair(
1448 PRIVATE_KEY_OUT_FILE,
1449 { passphrase, keyLength: '3072' }
1450 );
1451 })
1452 .then((publicKey) => {
1453 return provider.putPublicKey(this.instanceId, publicKey);
1454 })
1455 .then(() => {
1456 return bigIp.installPrivateKey(
1457 PRIVATE_KEY_OUT_FILE,
1458 AUTOSCALE_PRIVATE_KEY_FOLDER,
1459 AUTOSCALE_PRIVATE_KEY,
1460 { passphrase }
1461 );
1462 })
1463 .then(() => {
1464 return bigIp.save();
1465 })
1466 .catch((err) => {
1467 logger.info('initEncryption error', err && err.message ? err.message : err);
1468 return q.reject(err);
1469 });
1470 }
1471 return q();
1472 }
1473
1474 /**
1475 * Gets the instance marked as primary
1476 *
1477 * @param {Object} instances - Instances map
1478 *
1479 * @returns {Object} primary instance if one is found
1480 *
1481 * {
1482 * id: instance_id,
1483 * instance: instance_data
1484 * }
1485 */
1486 function getPrimaryInstance(instances) {
1487 let instanceId;
1488
1489 const instanceIds = Object.keys(instances);
1490 for (let i = 0; i < instanceIds.length; i++) {
1491 instanceId = instanceIds[i];
1492 if (instances[instanceId].isPrimary) {
1493 return {
1494 id: instanceId,
1495 instance: instances[instanceId]
1496 };
1497 }
1498 }
1499 return null;
1500 }
1501
1502 /**
1503 * Checks that primary instance has the most recent
1504 * version of all the BIG-IP instances
1505 *
1506 * @param {Object} instances - Instances map
1507 */
1508 function markVersions(instances) {
1509 let highestVersion = '0.0.0';
1510 let instance;
1511
1512 Object.keys(instances).forEach((instanceId) => {
1513 instance = instances[instanceId];
1514 if (instance.version && util.versionCompare(instance.version, highestVersion) > 0) {
1515 highestVersion = instance.version;
1516 }
1517 });
1518
1519 Object.keys(instances).forEach((instanceId) => {
1520 instance = instances[instanceId];
1521 if (!instance.version || util.versionCompare(instance.version, highestVersion) === 0) {
1522 instance.versionOk = true;
1523 } else {
1524 instance.versionOk = false;
1525 }
1526 });
1527 }
1528
1529 /**
1530 * Checks that if there are external instances, the primary is
1531 * one of them
1532 *
1533 * @param {String} primaryId - Instance ID of primary
1534 * @param {Object} instances - Instances map
1535 *
1536 * @returns {Boolean} True if there are no external instances or
1537 * if there are external instances and the primary is
1538 * one of them
1539 */
1540 function isPrimaryExternalValueOk(primaryId, instances) {
1541 const instanceIds = Object.keys(instances);
1542 let instance;
1543 let hasExternal;
1544
1545 for (let i = 0; i < instanceIds.length; i++) {
1546 instance = instances[instanceIds[i]];
1547 if (instance.external) {
1548 hasExternal = true;
1549 break;
1550 }
1551 }
1552
1553 if (hasExternal) {
1554 return !!instances[primaryId].external;
1555 }
1556
1557 return true;
1558 }
1559
1560 /*
1561 * Determines if the primary status has been bad for more than a certain
1562 * amount of time.
1563 *
1564 * @param {Object} instance - instance as returned by getInstances
1565 * @param {Object} options
1566 *
1567 * @returns {Boolean} Whether or not the primary status has been bad for too long
1568 */
1569 function isPrimaryExpired(instance, options) {
1570 const primaryStatus = instance.primaryStatus || {};
1571 let isExpired = false;
1572 let disconnectedMs;
1573
1574 if (primaryStatus.status !== CloudProvider.STATUS_OK) {
1575 disconnectedMs = new Date() - new Date(primaryStatus.lastStatusChange);
1576 logger.silly('primary has been disconnected for', disconnectedMs.toString(), 'ms');
1577 if (disconnectedMs > options.primaryDisconnectedTime) {
1578 logger.info('primary has been disconnected for too long (',
1579 disconnectedMs.toString(), 'ms )');
1580 isExpired = true;
1581 }
1582 }
1583 return isExpired;
1584 }
1585
1586 function updatePrimaryStatus(provider, status) {
1587 const now = new Date();
1588 this.instance.primaryStatus = this.instance.primaryStatus || {};
1589 this.instance.primaryStatus.lastUpdate = now;
1590 if (this.instance.primaryStatus.status !== status) {
1591 this.instance.primaryStatus.status = status;
1592 this.instance.primaryStatus.lastStatusChange = now;
1593 }
1594 return provider.putInstance(this.instanceId, this.instance);
1595 }
1596
1597 /**
1598 * Loads UCS
1599 *
1600 * @param {Object} bigIp - bigIp instances
1601 * @param {Buffer|Stream} ucsData - Either a Buffer or a ReadableStream containing UCS data
1602 * @param {String} cloudProvider - Cloud provider (aws, azure, etc)
1603 *
1604 * @returns {Promise} Promise that will be resolved when the UCS is loaded or rejected
1605 * if an error occurs.
1606 */
1607 function loadUcs(provider, bigIp, ucsData, cloudProvider) {
1608 const timeStamp = Date.now();
1609 const originalPath = `${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/ucsOriginal_${timeStamp}.ucs`;
1610 const updatedPath = `${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/ucsUpdated_${timeStamp}.ucs`;
1611 const updateScript = `${__dirname}/update_autoscale_ucs.py`;
1612
1613 const deferred = q.defer();
1614
1615 const preLoad = function () {
1616 // eslint-disable-next-line max-len
1617 const sedCommand = "sed -i '/sys dynad key {/ { N ; /\\n[[:space:]]\\+key[[:space:]]*\\$M\\$[^\\n]*/ { N; /\\n[[:space:]]*}/ { d } } }' /config/bigip_base.conf";
1618 const loadSysConfigCommand = 'load /sys config';
1619
1620 logger.silly('removing dynad key from base config');
1621 return util.runShellCommand(sedCommand)
1622 .then(() => {
1623 logger.silly('loading sys config');
1624 return util.runTmshCommand(loadSysConfigCommand);
1625 })
1626 .then(() => {
1627 logger.silly('waiting for BIG-IP to be ready');
1628 return bigIp.ready();
1629 })
1630 .catch((err) => {
1631 logger.warn('preload of ucs failed:', err);
1632 throw err;
1633 });
1634 };
1635
1636 const doLoad = function () {
1637 const args = [
1638 '--original-ucs',
1639 originalPath,
1640 '--updated-ucs',
1641 updatedPath,
1642 '--cloud-provider',
1643 cloudProvider,
1644 '--extract-directory',
1645 `${BACKUP.UCS_LOCAL_TMP_DIRECTORY}/ucsRestore`
1646 ];
1647 const loadUcsOptions = {
1648 initLocalKeys: true
1649 };
1650
1651 preLoad()
1652 .then(() => {
1653 childProcess.execFile(updateScript, args, (childProcessErr) => {
1654 if (childProcessErr) {
1655 const message = `${updateScript} failed: ${childProcessErr}`;
1656 logger.warn(message);
1657 deferred.reject(new Error(message));
1658 return;
1659 }
1660
1661 if (!fs.existsSync(updatedPath)) {
1662 logger.warn(`${updatedPath} does not exist after running ${updateScript}`);
1663 deferred.reject(new Error('updated ucs not found'));
1664 return;
1665 }
1666
1667 // If we're not sharing the password, put our current user back after
1668 // load
1669 if (!provider.hasFeature(CloudProvider.FEATURE_SHARED_PASSWORD)) {
1670 loadUcsOptions.restoreUser = true;
1671 }
1672
1673 bigIp.loadUcs(
1674 updatedPath,
1675 { 'no-license': true, 'reset-trust': true, 'no-platform-check': true },
1676 loadUcsOptions
1677 )
1678 .then(() => {
1679 // reset-trust on load does not always seem to work
1680 // use a belt-and-suspenders approach and reset now as well
1681 return bigIp.cluster.resetTrust();
1682 })
1683 .then(() => {
1684 // Attempt to delete the file, but ignore errors
1685 try {
1686 logger.info(`Ignoring errors: deleting originalPath: ${originalPath}
1687 deleting updatePath: ${updatedPath}`);
1688 fs.unlinkSync(originalPath);
1689 fs.unlinkSync(updatedPath);
1690 } finally {
1691 deferred.resolve();
1692 }
1693 })
1694 .catch((err) => {
1695 logger.info('error loading ucs', err);
1696 deferred.reject(err);
1697 });
1698 });
1699 })
1700 .catch((err) => {
1701 throw err;
1702 });
1703 };
1704
1705 util.writeUcsFile(originalPath, ucsData)
1706 .then(() => {
1707 doLoad();
1708 })
1709 .catch((err) => {
1710 logger.warn('Error reading ucs data', err);
1711 deferred.reject(err);
1712 });
1713 return deferred.promise;
1714 }
1715
1716 function writePrimaryFile(ucsLoaded) {
1717 const deferred = q.defer();
1718 const primaryInfo = { ucsLoaded };
1719
1720 // Mark ourself as primary on disk so other scripts have access to this info
1721 fs.writeFile(PRIMARY_FILE_PATH, JSON.stringify(primaryInfo), (err) => {
1722 if (err) {
1723 logger.warn('Error saving primary file', err);
1724 deferred.reject(err);
1725 return;
1726 }
1727
1728 logger.silly('Wrote primary file', PRIMARY_FILE_PATH, primaryInfo);
1729 deferred.resolve(true);
1730 });
1731
1732 return deferred.promise;
1733 }
1734
1735 function prepareMessageData(provider, instanceId, messageData) {
1736 if (!provider.hasFeature(CloudProvider.FEATURE_ENCRYPTION)) {
1737 return q(messageData);
1738 }
1739 return util.tryUntil(provider, util.MEDIUM_RETRY, provider.getPublicKey, [instanceId])
1740 .then((publicKey) => {
1741 return cryptoUtil.encrypt(publicKey, messageData);
1742 });
1743 }
1744
1745 function readMessageData(provider, bigIp, messageData) {
1746 let filePromise;
1747
1748 if (!provider.hasFeature(CloudProvider.FEATURE_ENCRYPTION)) {
1749 return q(messageData);
1750 }
1751
1752 if (!this.cloudPrivateKeyPath) {
1753 logger.silly('getting private key path');
1754 filePromise = bigIp.getPrivateKeyFilePath(AUTOSCALE_PRIVATE_KEY_FOLDER, AUTOSCALE_PRIVATE_KEY);
1755 } else {
1756 logger.silly('using cached key');
1757 filePromise = q(this.cloudPrivateKeyPath);
1758 }
1759
1760 return filePromise
1761 .then((cloudPrivateKeyPath) => {
1762 this.cloudPrivateKeyPath = cloudPrivateKeyPath;
1763 return bigIp.getPrivateKeyMetadata(AUTOSCALE_PRIVATE_KEY_FOLDER, AUTOSCALE_PRIVATE_KEY);
1764 })
1765 .then((privateKeyData) => {
1766 return cryptoUtil.decrypt(
1767 this.cloudPrivateKeyPath,
1768 messageData,
1769 {
1770 passphrase: privateKeyData.passphrase,
1771 passphraseEncrypted: true
1772 }
1773 );
1774 });
1775 }
1776
1777 function cleanupAjv(bigIp) {
1778 return bigIp.deviceInfo()
1779 .then((deviceInfo) => {
1780 if (util.versionCompare(deviceInfo.version, '13.1.0') < 0) {
1781 const filesToRemove = [
1782 `${__dirname}/../node_modules/ajv/lib/$data.js`,
1783 `${__dirname}/../node_modules/ajv/lib/refs/$data.json`
1784 ];
1785
1786 const deferred = q.defer();
1787 let filesHandled = 0;
1788
1789 filesToRemove.forEach((fileToRemove) => {
1790 fs.stat(fileToRemove, (statError) => {
1791 if (statError) {
1792 filesHandled += 1;
1793 if (filesHandled === filesToRemove.length) {
1794 deferred.resolve();
1795 }
1796 } else {
1797 fs.rename(fileToRemove, fileToRemove.replace('$', 'dollar_'), (renameErr) => {
1798 if (renameErr) {
1799 logger.info('cleanupAjv unable to remove', fileToRemove);
1800 }
1801
1802 filesHandled += 1;
1803 if (filesHandled === filesToRemove.length) {
1804 deferred.resolve();
1805 }
1806 });
1807 }
1808 });
1809 });
1810
1811 return deferred.promise;
1812 }
1813 return q();
1814 })
1815 .catch((err) => {
1816 logger.info('Unable to cleanup AJV', err);
1817 return q.reject(err);
1818 });
1819 }
1820
1821 function removeOldUcsFiles(latestFile) {
1822 const deferred = q.defer();
1823
1824 logger.silly('removing old ucs files');
1825 fs.readdir(UCS_BACKUP_DIRECTORY, (err, files) => {
1826 if (err) {
1827 logger.info(`Error reading ${UCS_BACKUP_DIRECTORY}`, err);
1828 deferred.reject(err);
1829 } else {
1830 files.forEach((file) => {
1831 if (file.startsWith(UCS_BACKUP_PREFIX) && file !== latestFile) {
1832 fs.unlinkSync(`${UCS_BACKUP_DIRECTORY}/${file}`);
1833 }
1834 });
1835 deferred.resolve();
1836 }
1837 });
1838
1839 return deferred.promise;
1840 }
1841
1842 module.exports = runner;
1843
1844 // If we're called from the command line, run
1845 // This allows for test code to call us as a module
1846 if (!module.parent) {
1847 runner.run(process.argv);
1848 }
1849}());