UNPKG

54.9 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 assert = require('assert');
20const q = require('q');
21const util = require('./util');
22const Logger = require('./logger');
23
24const DEVICE_GROUP_PATH = '/tm/cm/device-group/';
25const TRUST_DOMAIN_NAME = 'Root';
26
27let loggerOptions;
28
29/**
30 * Cluster constructor
31 *
32 * @class
33 * @classdesc
34 * Provides clustering functionality to a base BigIp object
35 *
36 * @param {Object} bigIpCore - Base BigIp object.
37 * @param {Object} [options] - Optional parameters.
38 * @param {Object} [options.logger] - Logger to use. Or, pass loggerOptions to get your own logger.
39 * @param {Object} [options.loggerOptions] - Options for the logger.
40 * See {@link module:logger.getLogger} for details.
41 */
42function BigIpCluster(bigIpCore, options) {
43 const logger = options ? options.logger : undefined;
44 loggerOptions = options ? options.loggerOptions : undefined;
45
46 if (logger) {
47 this.logger = logger;
48 util.setLogger(logger);
49 } else {
50 loggerOptions = loggerOptions || { logLevel: 'none' };
51 loggerOptions.module = module;
52 this.logger = Logger.getLogger(loggerOptions);
53 util.setLoggerOptions(loggerOptions);
54 }
55
56 this.core = bigIpCore;
57}
58
59/**
60 * Adds a device to the trust group.
61 *
62 * @param {String} deviceName - Device name to add.
63 * @param {String} remoteHost - IP address of remote host to add
64 * @param {String} remoteUser - Admin user name on remote host
65 * @param {String} remotePassword - Admin user password on remote host
66 * @param {Object} [retryOptions] - Options for retrying the request.
67 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
68 * 0 to not retry. Default 60.
69 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
70 *
71 * @returns {Promise} A promise which is resolved when the request is complete
72 * or rejected if an error occurs.
73 */
74BigIpCluster.prototype.addToTrust = function addToTrust(
75 deviceName,
76 remoteHost,
77 remoteUser,
78 remotePassword,
79 retryOptions
80) {
81 const retry = retryOptions || util.DEFAULT_RETRY;
82 retry.continueOnErrorMessage = 'remoteSender';
83
84 const func = function () {
85 return this.core.ready()
86 .then(() => {
87 // Check to see if host is in the trust domain already
88 return this.isInTrustGroup(deviceName);
89 })
90 .then((isInGroup) => {
91 if (!isInGroup) {
92 // We have to pass the password to iControl Rest just like we would to tmsh
93 // so escape the quotes, then wrap it in quotes
94 let escapedPassword = remotePassword.replace(/\\/g, '\\\\');
95 escapedPassword = escapedPassword.replace(/"/g, '\\"');
96 escapedPassword = `"${escapedPassword}"`;
97 return this.core.create(
98 '/tm/cm/add-to-trust',
99 {
100 command: 'run',
101 name: TRUST_DOMAIN_NAME,
102 caDevice: true,
103 device: remoteHost,
104 username: remoteUser,
105 password: escapedPassword,
106 deviceName
107 },
108 undefined,
109 util.NO_RETRY
110 );
111 }
112
113 return q();
114 })
115 .catch((err) => {
116 this.logger.info(`Add to trust failed: ${err.message ? err.message : err}`);
117 return q.reject(err);
118 });
119 };
120
121 return util.tryUntil(this, retry, func);
122};
123
124/**
125 * Adds a device to a device group.
126 *
127 * @param {String} deviceName - Device name to add.
128 * @param {String} deviceGroup - Name of the device group to add device to.
129 * @param {Object} [retryOptions] - Options for retrying the request.
130 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
131 * 0 to not retry. Default 60.
132 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
133 *
134 * @returns {Promise} A promise which is resolved when the request is complete
135 * or rejected if an error occurs.
136 */
137BigIpCluster.prototype.addToDeviceGroup = function addToDeviceGroup(deviceName, deviceGroup, retryOptions) {
138 const retry = retryOptions || util.DEFAULT_RETRY;
139
140 const func = function () {
141 return this.core.ready()
142 .then(() => {
143 return this.isInDeviceGroup(deviceName, deviceGroup);
144 })
145 .then((isInGroup) => {
146 if (!isInGroup) {
147 return this.core.create(
148 `${DEVICE_GROUP_PATH}~Common~${deviceGroup}/devices`,
149 {
150 name: deviceName
151 },
152 undefined,
153 util.NO_RETRY
154 );
155 }
156
157 return q();
158 });
159 };
160
161 return util.tryUntil(this, retry, func);
162};
163
164/**
165 * Checks to see if a device is in a device group
166 *
167 * @param {String[]} deviceNames - Device names to check for.
168 * @param {String} deviceGroup - Device group to look in.
169 * @param {Object} [retryOptions] - Options for retrying the request.
170 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
171 * 0 to not retry. Default 60.
172 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
173 *
174 * @returns {Promise} A promise which is resolved with an array of names that are in the device group
175 * and in deviceNames, or rejected if an error occurs.
176 */
177BigIpCluster.prototype.areInDeviceGroup = function areInDeviceGroup(deviceNames, deviceGroup, retryOptions) {
178 const retry = retryOptions || util.DEFAULT_RETRY;
179
180 const func = function () {
181 return this.core.ready()
182 .then(() => {
183 return this.core.list(`${DEVICE_GROUP_PATH}${deviceGroup}/devices`, undefined, util.NO_RETRY);
184 })
185 .then((currentDevices) => {
186 const devicesInGroup = [];
187 currentDevices.forEach((currentDevice) => {
188 if (deviceNames.indexOf(currentDevice.name) !== -1) {
189 devicesInGroup.push(currentDevice.name);
190 }
191 });
192
193 return devicesInGroup;
194 });
195 };
196
197 return util.tryUntil(this, retry, func);
198};
199
200/**
201 * Checks to see if a device is in the trust group
202 *
203 * @param {String[]} deviceNames - Device names to check for.
204 * @param {Object} [retryOptions] - Options for retrying the request.
205 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
206 * 0 to not retry. Default 60.
207 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
208 *
209 * @returns {Promise} A promise which is resolved with an array of names that are in the trust group
210 * and in deviceNames, or rejected if an error occurs.
211 */
212BigIpCluster.prototype.areInTrustGroup = function areInTrustGroup(deviceNames, retryOptions) {
213 const retry = retryOptions || util.DEFAULT_RETRY;
214
215 const func = function () {
216 return this.core.ready()
217 .then(() => {
218 return this.core.list(`/tm/cm/trust-domain/${TRUST_DOMAIN_NAME}`, undefined, util.NO_RETRY);
219 })
220 .then((response) => {
221 let i = deviceNames.length - 1;
222
223 if (response && response.caDevices) {
224 while (i >= 0) {
225 if (response.caDevices.indexOf(`/Common/${deviceNames[i]}`) === -1) {
226 deviceNames.splice(i, 1);
227 }
228 i -= 1;
229 }
230 }
231
232 return deviceNames;
233 });
234 };
235
236 return util.tryUntil(this, retry, func);
237};
238
239/**
240 * Sets the config sync ip
241 *
242 * @param {String} syncIp - The IP address to use for config sync.
243 * @param {Object} [retryOptions] - Options for retrying the request.
244 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
245 * 0 to not retry. Default 60.
246 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
247 *
248 * @returns {Promise} A promise which is resolved when the request is complete
249 * or rejected if an error occurs.
250 */
251BigIpCluster.prototype.configSyncIp = function configSyncIp(syncIp, retryOptions) {
252 const retry = retryOptions || util.DEFAULT_RETRY;
253
254 const func = function () {
255 return this.core.ready()
256 .then(() => {
257 return this.core.deviceInfo(util.NO_RETRY);
258 })
259 .then((response) => {
260 return this.core.modify(
261 `/tm/cm/device/~Common~${response.hostname}`,
262 {
263 configsyncIp: syncIp
264 }
265 );
266 });
267 };
268
269 return util.tryUntil(this, retry, func);
270};
271
272/**
273 * Creates a device group
274 *
275 * @param {String} deviceGroup - Name for device group.
276 * @param {String} type - Type of device group. Must be
277 * 'sync-only' || 'sync-failover'.
278 * @param {String|String[]} [deviceNames] - Device name or array of names to
279 * add to the group.
280 * @param {Object} [options] - Object containg device group options.
281 * @param {Boolean} [options.autoSync] - Whether or not to autoSync. Default false.
282 * @param {Boolean} [options.saveOnAutoSync] - If autoSync is eanbled, whether or not to save on
283 autoSync. Default false.
284 * @param {Boolean} [options.networkFailover] - Whether or not to use network fail-over.
285 * Default false.
286 * @param {Boolean} [options.fullLoadOnSync] - Whether or not to do a full sync. Default false.
287 * @param {Boolean} [options.asmSync] - Whether or not do to ASM sync. Default false.
288 * @param {Object} [retryOptions] - Options for retrying the request.
289 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
290 * 0 to not retry. Default 60.
291 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
292 *
293 * @returns {Promise} A promise which is resolved when the request is complete
294 * or rejected if an error occurs.
295 */
296BigIpCluster.prototype.createDeviceGroup = function createDeviceGroup(
297 deviceGroup,
298 type,
299 deviceNames,
300 options,
301 retryOptions
302) {
303 let names;
304
305 if (!deviceGroup) {
306 return q.reject(new Error('deviceGroup is required'));
307 }
308
309 if (type !== 'sync-only' && type !== 'sync-failover') {
310 return q.reject(new Error('type must be sync-only or sync-failover'));
311 }
312
313 if (!Array.isArray(deviceNames)) {
314 names = [deviceNames];
315 } else {
316 names = deviceNames.slice();
317 }
318
319 const retry = retryOptions || util.DEFAULT_RETRY;
320
321 const groupOptions = {};
322 if (options) {
323 Object.keys(options).forEach((option) => {
324 groupOptions[option] = options[option];
325 });
326 }
327
328 const groupSettings = {};
329 groupSettings.autoSync = groupOptions.autoSync ? 'enabled' : 'disabled';
330 groupSettings.fullLoadOnSync = !!groupOptions.fullLoadOnSync;
331 groupSettings.asmSync = groupOptions.asmSync ? 'enabled' : 'disabled';
332
333 if (groupSettings.autoSync === 'enabled') {
334 groupSettings.saveOnAutoSync = !!groupOptions.saveOnAutoSync;
335 }
336
337 if (type === 'sync-failover') {
338 groupSettings.networkFailover = groupOptions.networkFailover ? 'enabled' : 'disabled';
339 }
340
341 const func = function () {
342 return this.core.ready()
343 .then(() => {
344 // Check to see if the device group already exists
345 return this.hasDeviceGroup(deviceGroup);
346 })
347 .then((response) => {
348 if (response === false) {
349 groupSettings.name = deviceGroup;
350 groupSettings.devices = names || [];
351 groupSettings.type = type;
352
353 return this.core.create(DEVICE_GROUP_PATH, groupSettings, undefined, util.NO_RETRY);
354 }
355
356 // If the device group exists, re-apply group settings in case they've been updated
357 return this.core.modify(
358 `${DEVICE_GROUP_PATH}${deviceGroup}`,
359 groupSettings,
360 undefined,
361 util.NO_RETRY
362 )
363 .then(() => {
364 // Check that the requested devices are in the device group
365 return this.areInDeviceGroup(names, deviceGroup, retryOptions);
366 })
367 .then((devicesInGroup) => {
368 const promises = [];
369
370 names.forEach((deviceName) => {
371 if (devicesInGroup.indexOf(deviceName) === -1) {
372 promises.push({
373 promise: this.addToDeviceGroup,
374 arguments: [deviceName, deviceGroup]
375 });
376 }
377 });
378
379 return util.callInSerial(this, promises);
380 });
381 });
382 };
383
384 return util.tryUntil(this, retry, func);
385};
386
387/**
388 * Deletes a device group
389 *
390 * @param {String} deviceGroup - Name of device group.
391 *
392 * @returns {Promise} A promise which is resolved when the request is complete
393 * or rejected if an error occurs.
394 */
395BigIpCluster.prototype.deleteDeviceGroup = function deleteDeviceGroup(deviceGroup) {
396 if (!deviceGroup) {
397 return q.reject(new Error('deviceGroup is required'));
398 }
399
400 return this.hasDeviceGroup(deviceGroup)
401 .then((response) => {
402 if (response === true) {
403 return this.removeAllFromDeviceGroup(deviceGroup)
404 .then(() => {
405 return this.core.delete(DEVICE_GROUP_PATH + deviceGroup);
406 });
407 }
408 return q();
409 });
410};
411
412/**
413 * Checks for existence of a device group
414 *
415 * @param {String} deviceGroup - Name for device group.
416 * @param {Object} [retryOptions] - Options for retrying the request.
417 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
418 * 0 to not retry. Default 60.
419 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
420 *
421 * @returns {Promise} A promise which is resolved with true/false based on device group existence
422 * or rejected if an error occurs.
423 */
424BigIpCluster.prototype.hasDeviceGroup = function hasDeviceGroup(deviceGroup, retryOptions) {
425 if (!deviceGroup) {
426 return q.reject(new Error('deviceGroup is required'));
427 }
428
429 const retry = retryOptions || util.SHORT_RETRY;
430
431 const func = function () {
432 return this.core.ready()
433 .then(() => {
434 // Check to see if the device group already exists
435 return this.core.list(DEVICE_GROUP_PATH);
436 })
437 .then((response) => {
438 const containsGroup = (deviceGroups) => {
439 for (let i = 0; i < deviceGroups.length; i++) {
440 if (deviceGroups[i].name === deviceGroup) {
441 return true;
442 }
443 }
444 return false;
445 };
446
447 let hasGroup = false;
448
449 if (response && containsGroup(response)) {
450 hasGroup = true;
451 }
452
453 return q(hasGroup);
454 });
455 };
456
457 return util.tryUntil(this, retry, func);
458};
459
460/**
461 * Gets cm sync status
462 *
463 * @returns {Promise} Promise which is resolved with a list of connected and
464 * disconnected host names
465 */
466BigIpCluster.prototype.getCmSyncStatus = function getCmSyncStatus() {
467 const cmSyncStatus = {
468 connected: [],
469 disconnected: []
470 };
471
472 let entries;
473 let description;
474 let descriptionTokens;
475
476 return this.core.list('/tm/cm/sync-status', undefined, { maxRetries: 120, retryIntervalMs: 10000 })
477 .then((response) => {
478 this.logger.debug(response);
479 entries = response
480 .entries['https://localhost/mgmt/tm/cm/sync-status/0']
481 .nestedStats.entries['https://localhost/mgmt/tm/cm/syncStatus/0/details'];
482
483 if (entries) {
484 Object.keys(entries.nestedStats.entries).forEach((detail) => {
485 description = entries.nestedStats.entries[detail].nestedStats.entries.details.description;
486 descriptionTokens = description.split(': ');
487 if (descriptionTokens[1] && descriptionTokens[1].toLowerCase() === 'connected') {
488 cmSyncStatus.connected.push(descriptionTokens[0]);
489 } else if (
490 descriptionTokens[1] && descriptionTokens[1].toLowerCase() === 'disconnected'
491 ) {
492 cmSyncStatus.disconnected.push(descriptionTokens[0]);
493 }
494 });
495 } else {
496 this.logger.silly('No entries in sync status');
497 }
498
499 this.logger.debug(cmSyncStatus);
500 return cmSyncStatus;
501 });
502};
503
504/**
505 * Checks to see if a device is device group
506 *
507 * @param {String} deviceName - Device name to check for.
508 * @param {String} deviceGroup - Device group to check in.
509 * @param {Object} [retryOptions] - Options for retrying the request.
510 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
511 * 0 to not retry. Default 60.
512 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
513 *
514 * @returns {Promise} A promise which is resolved with true or false
515 * or rejected if an error occurs.
516 */
517BigIpCluster.prototype.isInDeviceGroup = function isInDeviceGroup(deviceName, deviceGroup, retryOptions) {
518 const retry = retryOptions || util.DEFAULT_RETRY;
519
520 const func = function () {
521 return this.core.ready()
522 .then(() => {
523 return this.hasDeviceGroup(deviceGroup);
524 })
525 .then((response) => {
526 if (response === false) {
527 return false;
528 }
529 return this.core.list(`${DEVICE_GROUP_PATH}${deviceGroup}/devices`, undefined, util.NO_RETRY);
530 })
531 .then((response) => {
532 const containsHost = function (devices) {
533 for (let i = 0; i < devices.length; i++) {
534 if (devices[i].name.indexOf(deviceName) !== -1) {
535 return true;
536 }
537 }
538 return false;
539 };
540
541 if (response === false) {
542 return false;
543 }
544 return containsHost(response);
545 });
546 };
547
548 return util.tryUntil(this, retry, func);
549};
550
551/**
552 * Checks to see if a device is in the trust group
553 *
554 * @param {String} deviceName - Device name to check for.
555 * @param {Object} [retryOptions] - Options for retrying the request.
556 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
557 * 0 to not retry. Default 60.
558 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
559 *
560 * @returns {Promise} A promise which is resolved with true or false
561 * or rejected if an error occurs.
562 */
563BigIpCluster.prototype.isInTrustGroup = function isInTrustGroup(deviceName, retryOptions) {
564 const retry = retryOptions || util.DEFAULT_RETRY;
565
566 const func = function () {
567 return this.core.ready()
568 .then(() => {
569 return this.core.list(`/tm/cm/trust-domain/${TRUST_DOMAIN_NAME}`, undefined, util.NO_RETRY);
570 })
571 .then((response) => {
572 if (response && response.caDevices) {
573 return response.caDevices.indexOf(`/Common/${deviceName}`) !== -1;
574 }
575 return false;
576 });
577 };
578
579 return util.tryUntil(this, retry, func);
580};
581
582/**
583 * Joins a cluster and optionally syncs.
584 *
585 * This is a just a higher level function that calls other funcitons in this
586 * and other bigIp* files:
587 * - Add to trust on remote host
588 * - Add to remote device group
589 * - Sync remote device group
590 * - Check for datasync-global-dg and sync that as well if it is present
591 * (this is necessary so that we know when syncing is complete)
592 * The device group must already exist on the remote host.
593 *
594 * @param {String} deviceGroup - Name of device group to join.
595 * @param {String} remoteHost - Managemnt IP for the remote device.
596 * @param {String} remoteUser - Remote device admin user name.
597 * @param {String} remotePassword - Remote device admin user password.
598 * @param {Boolean} isLocal - Whether the device group is defined locally or if
599 * we are joining one on a remote host.
600 * @param {Object} [options] - Optional arguments.
601 * @param {Number} [options.remotePort] - Remote device port to connect to. Default the
602 * port of this device instance.
603 * @param {Boolean} [options.sync] - Whether or not to perform a sync. Default true.
604 * @param {Number} [options.syncDelay] - Delay in ms to wait after sending sync command
605 * before proceeding. Default 30000.
606 * @param {Number} [options.syncCompDelay] - Delay in ms to wait between checking sync complete.
607 * Default 10000.
608 * @param {String[]} [options.syncCompDevices] - List of device names that should be connected.
609 * If defined, sync complete failures are ignored
610 * when device trust can not sync due to disconnected
611 * devices that are not in this list. Default is to
612 * leave undefined and fail on all sync complete failures.
613 * @param {Boolean} [options.passwordIsUrl] - Indicates that password is a URL for the password.
614 * @param {Boolean} [options.passwordEncrypted] - Indicates that the password is encrypted (with the
615 * local cloud public key)
616 * @param {String} [options.remoteHostname] - If adding to a local group (isLocal === true) the
617 * cm hostname of the remote host.
618 * @param {Boolean} [options.noWait] - Don't wait for configSyncIp, just fail if it's not
619 * ready right away. This is used for providers that have
620 * messaging - they will try again periodically
621 * @param {String} [options.product] - The product we are running on (BIG-IP | BIG-IQ). Default
622 * is to determine the product programmatically.
623 *
624 * @returns {Promise} A promise which is resolved when the request is complete
625 * or rejected if an error occurs. If promise is resolved, it is
626 * is resolved with true if syncing occurred.
627 */
628BigIpCluster.prototype.joinCluster = function joinCluster(
629 deviceGroup,
630 remoteHost,
631 remoteUser,
632 remotePassword,
633 isLocal,
634 options
635) {
636 const normalizedOptions = {};
637
638 let clusteringBigIp;
639 let remoteBigIp;
640 let hostname;
641 let managementIp;
642 let version;
643
644 const checkClusterReadiness = function checkClusterReadiness(deviceGroupToCheck) {
645 const func = function () {
646 let promises;
647 let localHostname;
648 let remoteHostname;
649
650 return this.core.ready()
651 .then(() => {
652 return this.core.deviceInfo();
653 })
654 .then((response) => {
655 localHostname = response.hostname;
656 return remoteBigIp.deviceInfo();
657 })
658 .then((response) => {
659 remoteHostname = response.hostname;
660
661 this.logger.silly('localHostname', localHostname, 'remoteHostname', remoteHostname);
662
663 promises = [
664 this.core.deviceState(localHostname, util.NO_RETRY),
665 remoteBigIp.deviceState(remoteHostname, util.NO_RETRY)
666 ];
667
668 // if the group is not local, make sure it exists on the remote
669 if (!isLocal) {
670 promises.push(remoteBigIp.list(
671 DEVICE_GROUP_PATH + deviceGroupToCheck,
672 undefined,
673 util.NO_RETRY
674 ));
675 }
676
677 return q.all(promises);
678 })
679 .then((responses) => {
680 // if the last promise (checking for device group) fails,
681 // q.all will reject - no need to check its response
682 if (!responses[0].configsyncIp || responses[0].configsyncIp === 'none') {
683 return q.reject(new Error('No local config sync IP.'));
684 }
685
686 if (!responses[1].configsyncIp || responses[1].configsyncIp === 'none') {
687 return q.reject(new Error('No remote config sync IP.'));
688 }
689
690 return q();
691 });
692 };
693
694 const retry = normalizedOptions.noWait ? util.NO_RETRY : { maxRetries: 240, retryIntervalMs: 10000 };
695 return util.tryUntil(this, retry, func);
696 }.bind(this);
697
698 const processJoin = function processJoin() {
699 return remoteBigIp.init(
700 remoteHost,
701 remoteUser,
702 remotePassword,
703 {
704 port: normalizedOptions.remotePort,
705 passwordIsUrl: normalizedOptions.passwordIsUrl,
706 passwordEncrypted: normalizedOptions.passwordEncrypted,
707 product: normalizedOptions.product
708 }
709 )
710 .then(() => {
711 this.logger.info('Checking remote host for cluster readiness.');
712 return checkClusterReadiness(deviceGroup);
713 })
714 .then((response) => {
715 this.logger.debug(response);
716
717 if (!isLocal) {
718 this.logger.info('Getting local hostname for trust.');
719 return this.core.list('/tm/cm/device');
720 }
721
722 return q();
723 })
724 .then((response) => {
725 this.logger.debug(response);
726
727 if (!isLocal) {
728 // We may get back just one device or an array of devices
729 const normalizedResponse = Array.isArray(response) ? response : [response];
730 const device = normalizedResponse.find((dev) => {
731 return dev.selfDevice === 'true';
732 });
733
734 if (typeof device === 'undefined') {
735 return q.reject(new Error('Self device could not be found to set local hostname'));
736 }
737
738 hostname = device.hostname;
739
740 this.logger.info('Getting local management address.');
741 return this.core.deviceInfo();
742 }
743
744 hostname = normalizedOptions.remoteHostname;
745 return q();
746 })
747 .then((response) => {
748 this.logger.debug(response);
749
750 let user;
751 let password;
752
753 if (!isLocal) {
754 managementIp = response.managementAddress;
755 user = this.core.user;
756 password = this.core.password;
757 } else {
758 managementIp = remoteHost;
759 user = remoteUser;
760 password = remotePassword;
761 }
762
763 this.logger.info('Adding to', isLocal ? 'local' : 'remote', 'trust.');
764 return clusteringBigIp.addToTrust(hostname, managementIp, user, password);
765 })
766 .then((response) => {
767 this.logger.debug(response);
768
769 this.logger.info('Adding to', isLocal ? 'local' : 'remote', 'device group.');
770 return clusteringBigIp.addToDeviceGroup(hostname, deviceGroup);
771 })
772 .then((response) => {
773 this.logger.debug(response);
774
775 if (normalizedOptions.sync) {
776 // If the group datasync-global-dg is present (which it likely is if ASM is provisioned)
777 // we need to force a sync of it as well. Otherwise we will not be able to determine
778 // the overall sync status because there is no way to get the sync status
779 // of a single device group
780 this.logger.info('Checking for datasync-global-dg.');
781 return this.core.list(DEVICE_GROUP_PATH);
782 }
783
784 return q();
785 })
786 .then((response) => {
787 const dataSyncResponse = response;
788
789 // Sometimes sync just fails silently, so we retry all of the sync commands until both
790 // local and remote devices report that they are in sync
791 const syncAndCheck = function syncAndCheck(datasyncGlobalDgResponse) {
792 const deferred = q.defer();
793 const syncPromise = q.defer();
794
795 const SYNC_COMPLETE_RETRY = {
796 maxRetries: 3,
797 retryIntervalMs: normalizedOptions.syncCompDelay
798 };
799
800 this.logger.info('Telling', isLocal ? 'local' : 'remote', 'to sync.');
801
802 // We need to wait some time (30 sec?) between issuing sync commands or else sync
803 // never completes.
804 clusteringBigIp.sync('to-group', deviceGroup, false, util.NO_RETRY)
805 .then(() => {
806 setTimeout(() => {
807 syncPromise.resolve();
808 }, normalizedOptions.syncDelay);
809 })
810 .done();
811
812 syncPromise.promise
813 .then(() => {
814 for (let i = 0; i < datasyncGlobalDgResponse.length; i++) {
815 if (datasyncGlobalDgResponse[i].name === 'datasync-global-dg') {
816 // Prior to 12.1, set the sync leader
817 if (util.versionCompare(version, '12.1.0') < 0) {
818 this.logger.info('Setting sync leader.');
819 return this.core.modify(
820 `${DEVICE_GROUP_PATH}datasync-global-dg/devices/${hostname}`,
821 { 'set-sync-leader': true },
822 undefined,
823 util.NO_RETRY
824 );
825 }
826 // On 12.1 and later, do a full sync
827 this.logger.info(
828 'Telling',
829 isLocal ? 'local' : 'remote',
830 'to sync datasync-global-dg request.'
831 );
832 return clusteringBigIp.sync(
833 'to-group',
834 'datasync-global-dg',
835 true,
836 util.NO_RETRY
837 );
838 }
839 }
840 return q();
841 })
842 .then(() => {
843 this.logger.info('Waiting for sync to complete.');
844 return clusteringBigIp.syncComplete(SYNC_COMPLETE_RETRY);
845 })
846 .then(() => {
847 this.logger.info('Sync complete.');
848 deferred.resolve();
849 })
850 .catch((err) => {
851 this.logger.info('Sync not yet complete.');
852 this.logger.verbose('Sync Error', err);
853
854 if (err && err.recommendedAction) {
855 // In some cases, sync complete tells us to sync a different group
856 if (err.recommendedAction.sync) {
857 const recommendedGroup = err.recommendedAction.sync;
858 this.logger.info(`Recommended action to sync group ${recommendedGroup}`);
859 clusteringBigIp.sync('to-group', recommendedGroup, true, util.NO_RETRY)
860 .then(() => {
861 return clusteringBigIp.syncComplete(
862 SYNC_COMPLETE_RETRY,
863 {
864 connectedDevices: normalizedOptions.syncCompDevices
865 }
866 );
867 })
868 .then(() => {
869 deferred.resolve();
870 })
871 .catch(() => {
872 deferred.reject();
873 });
874 }
875 } else {
876 deferred.reject();
877 }
878 })
879 .done();
880
881 return deferred.promise;
882 }.bind(this);
883
884 this.logger.debug(response);
885
886 if (normalizedOptions.sync) {
887 return this.core.deviceInfo()
888 .then((deviceInfo) => {
889 // we need this later when we sync the datasync-global-dg group
890 version = deviceInfo.version;
891 return util.tryUntil(
892 this,
893 { maxRetries: 10, retryIntervalMs: normalizedOptions.syncDelay },
894 syncAndCheck,
895 [dataSyncResponse]
896 );
897 })
898 .then(() => {
899 return true;
900 });
901 }
902
903 return q();
904 })
905 .catch((err) => {
906 this.logger.info(`join cluster failed: ${err.message ? err.message : err}`);
907 return q.reject(err);
908 });
909 }.bind(this);
910
911 if (options) {
912 Object.keys(options).forEach((option) => {
913 normalizedOptions[option] = options[option];
914 });
915 }
916
917 normalizedOptions.remotePort = normalizedOptions.remotePort || this.core.port;
918 normalizedOptions.syncDelay = normalizedOptions.syncDelay || 30000;
919 normalizedOptions.syncCompDelay = normalizedOptions.syncCompDelay || 10000;
920 normalizedOptions.noWait =
921 typeof normalizedOptions.noWait === 'undefined' ? false : normalizedOptions.noWait;
922
923 if (typeof normalizedOptions.sync === 'undefined') {
924 normalizedOptions.sync = true;
925 }
926
927 assert(typeof deviceGroup === 'string', 'deviceGroup is required for joinCluster');
928 assert(typeof remoteHost === 'string', 'remoteHost is required for joinCluster');
929 assert(typeof remoteUser === 'string', 'remoteUser is required for joinCluster');
930 assert(typeof remotePassword === 'string', 'remotePassword is required for joinCluster');
931
932 const BigIp = require('./bigIp'); // eslint-disable-line global-require
933 const ctorOptions = {};
934 if (loggerOptions) {
935 ctorOptions.loggerOptions = loggerOptions;
936 } else {
937 ctorOptions.logger = this.logger;
938 }
939 remoteBigIp = new BigIp(ctorOptions);
940 clusteringBigIp = isLocal ? this : remoteBigIp.cluster;
941
942 // If we're adding to a local device group, make sure the device is not already in it
943 if (isLocal) {
944 return this.isInDeviceGroup(options.remoteHostname, deviceGroup)
945 .then((isInGroup) => {
946 if (isInGroup) {
947 this.logger.debug(options.remoteHostname, 'is already in the cluster.');
948 return q(false);
949 }
950 return processJoin();
951 });
952 }
953
954 return processJoin();
955};
956
957/**
958 * Removes a device from cluster
959 *
960 * This is a just a higher level function that calls other funcitons in this
961 * and other bigIp* files:
962 * - Remove from device group
963 * - Remove from trust
964 *
965 * @param {String|String[]} deviceNames - Name or array of names of devices to remove
966 *
967 * @returns {Promise} A promise which is resolved when the request is complete
968 * or rejected if an error occurs.
969 */
970BigIpCluster.prototype.removeFromCluster = function removeFromCluster(deviceNames) {
971 let names;
972 if (!Array.isArray(deviceNames)) {
973 names = [deviceNames];
974 } else {
975 names = deviceNames.slice();
976 }
977
978 return this.core.ready()
979 .then(() => {
980 this.logger.info('Getting device groups');
981 return this.core.list(DEVICE_GROUP_PATH);
982 })
983 .then((response) => {
984 const promises = [];
985 response.forEach((deviceGroup) => {
986 promises.push(this.removeFromDeviceGroup(names, deviceGroup.name));
987 });
988 return q.all(promises);
989 })
990 .then((response) => {
991 this.logger.debug(response);
992
993 this.logger.info('Removing from trust.');
994 return this.removeFromTrust(names);
995 });
996};
997
998/**
999 * Removes a device from a device group
1000 *
1001 * @param {String|String[]} deviceNames - Name or array of names of devices to remove.
1002 * @param {String} deviceGroup - Name of device group.
1003 * @param {Object} [retryOptions] - Options for retrying the request.
1004 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1005 * 0 to not retry. Default 60.
1006 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1007 *
1008 * @returns {Promise} A promise which is resolved when the request is complete
1009 * or rejected if an error occurs.
1010 */
1011BigIpCluster.prototype.removeFromDeviceGroup = function removeFromDeviceGroup(
1012 deviceNames,
1013 deviceGroup,
1014 retryOptions
1015) {
1016 const retry = retryOptions || util.DEFAULT_RETRY;
1017 let names;
1018 const asmDataSyncGroupRegPattern = new RegExp('^datasync-*.*-dg$');
1019
1020 if (deviceGroup === 'device_trust_group' || asmDataSyncGroupRegPattern.test(deviceGroup)) {
1021 this.logger.silly('Ignoring', deviceGroup, 'which is read only or does not require device removal');
1022 return q();
1023 }
1024 this.logger.silly('Processing Device Group', deviceGroup);
1025
1026 if (!Array.isArray(deviceNames)) {
1027 names = [deviceNames];
1028 } else {
1029 names = deviceNames.slice();
1030 }
1031
1032 const func = function () {
1033 return this.core.ready()
1034 .then(() => {
1035 return this.core.list(`${DEVICE_GROUP_PATH}${deviceGroup}/devices`, undefined, util.NO_RETRY);
1036 })
1037 .then((currentDevices) => {
1038 const devicesToKeep = [];
1039 currentDevices.forEach((currentDevice) => {
1040 if (names.indexOf(currentDevice.name) === -1) {
1041 devicesToKeep.push(currentDevice.name);
1042 }
1043 });
1044 if (devicesToKeep.length !== currentDevices.length) {
1045 return this.core.modify(
1046 DEVICE_GROUP_PATH + deviceGroup,
1047 { devices: devicesToKeep }
1048 );
1049 }
1050 return q();
1051 });
1052 };
1053
1054 return util.tryUntil(this, retry, func);
1055};
1056
1057/**
1058 * Removes all devices from a device group
1059 *
1060 * @param {String} deviceGroup - Name of device group.
1061 * @param {Object} [retryOptions] - Options for retrying the request.
1062 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1063 * 0 to not retry. Default 60.
1064 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1065 *
1066 * @returns {Promise} A promise which is resolved when the request is complete
1067 * or rejected if an error occurs.
1068 */
1069BigIpCluster.prototype.removeAllFromDeviceGroup = function removeAllFromDeviceGroup(
1070 deviceGroup,
1071 retryOptions
1072) {
1073 const retry = retryOptions || util.DEFAULT_RETRY;
1074
1075 if (deviceGroup === 'device_trust_group') {
1076 this.logger.silly('Ignoring', deviceGroup, 'which is read only');
1077 return q();
1078 }
1079
1080 const func = function () {
1081 return this.core.modify(
1082 DEVICE_GROUP_PATH + deviceGroup,
1083 { devices: [] }
1084 );
1085 };
1086
1087 return util.tryUntil(this, retry, func);
1088};
1089
1090/**
1091 * Removes a device from the device trust
1092 *
1093 * @param {String|String[]} deviceNames - Name or array of names of devices to remove
1094 * @param {Object} [retryOptions] - Options for retrying the request.
1095 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1096 * 0 to not retry. Default 60.
1097 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1098 *
1099 * @returns {Promise} A promise which is resolved when the request is complete
1100 * or rejected if an error occurs.
1101 */
1102BigIpCluster.prototype.removeFromTrust = function removeFromTrust(deviceNames, retryOptions) {
1103 const retry = retryOptions || util.DEFAULT_RETRY;
1104 let names;
1105
1106 if (!Array.isArray(deviceNames)) {
1107 names = [deviceNames];
1108 } else {
1109 names = deviceNames.slice();
1110 }
1111
1112 const func = function () {
1113 return this.core.ready()
1114 .then(() => {
1115 // Check to see if host is in the trust domain already
1116 return this.areInTrustGroup(names);
1117 })
1118 .then((devicesInGroup) => {
1119 const promises = [];
1120
1121 devicesInGroup.forEach((deviceName) => {
1122 promises.push(this.core.create(
1123 '/tm/cm/remove-from-trust',
1124 {
1125 command: 'run',
1126 name: 'Root',
1127 caDevice: true,
1128 deviceName
1129 },
1130 undefined,
1131 util.NO_RETRY
1132 ));
1133 });
1134
1135 if (promises.length !== 0) {
1136 return q.all(promises);
1137 }
1138
1139 return q();
1140 });
1141 };
1142
1143 return util.tryUntil(this, retry, func);
1144};
1145
1146/**
1147 * Resets the device trust
1148 *
1149 * @param {Object} [retryOptions] - Options for retrying the request.
1150 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1151 * 0 to not retry. Default 60.
1152 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1153 *
1154 * @returns {Promise} A promise which is resolved when the request is complete
1155 * or rejected if an error occurs.
1156 */
1157BigIpCluster.prototype.resetTrust = function resetTrust(retryOptions) {
1158 const retry = retryOptions || util.DEFAULT_RETRY;
1159
1160 return this.core.ready()
1161 .then(() => {
1162 // Get the software version
1163 return this.core.deviceInfo();
1164 })
1165 .then((response) => {
1166 const version = response.version;
1167 let resetPath = '/tm/cm/trust-domain';
1168 if (util.versionCompare(version, '13.0.0') < 0) {
1169 resetPath += '/Root';
1170 }
1171 return this.core.delete(resetPath, undefined, undefined, util.NO_RETRY);
1172 })
1173 .then(() => {
1174 return this.core.ready(retry);
1175 });
1176};
1177
1178/**
1179 * Syncs to/from device group
1180 *
1181 * @param {String} direction - 'to-group' || 'from-group'
1182 * @param {String} deviceGroup - Name of the device group to sync.
1183 * @param {Boolean} [forceFullLoadPush] - Whether or not to use the force-full-load-push option.
1184 * Default false.
1185 * @param {Object} [retryOptions] - Options for retrying the request.
1186 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1187 * 0 to not retry. Default 60.
1188 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1189 *
1190 * @returns {Promise} A promise which is resolved when the request is complete
1191 * or rejected if an error occurs.
1192 */
1193BigIpCluster.prototype.sync = function sync(direction, deviceGroup, forceFullLoadPush, retryOptions) {
1194 const retry = retryOptions || util.DEFAULT_RETRY;
1195
1196 const func = function () {
1197 return this.core.ready()
1198 .then(() => {
1199 return this.core.create(
1200 '/tm/cm',
1201 {
1202 command: 'run',
1203 utilCmdArgs: [
1204 'config-sync',
1205 forceFullLoadPush ? 'force-full-load-push' : '',
1206 direction,
1207 deviceGroup].join(' ')
1208 },
1209 undefined,
1210 util.NO_RETRY
1211 );
1212 });
1213 };
1214
1215 return util.tryUntil(this, retry, func);
1216};
1217
1218/**
1219 * Checks sync status to see if it is complete
1220 *
1221 * @param {Object} [retryOptions] - Options for retrying the request.
1222 * @param {Integer} [retryOptions.maxRetries] - Number of times to retry if first try fails.
1223 * 0 to not retry. Default 60.
1224 * @param {Integer} [retryOptions.retryIntervalMs] - Milliseconds between retries. Default 10000.
1225 * @param {Object} [options] - Optional arguments.
1226 * @param {String[]} [options.connectedDevices] - List of device names that should be connected.
1227 * If defined, sync complete failures are ignored
1228 * when device trust can not sync due to disconnected
1229 * devices that are not in this list. Default is to
1230 * leave undefined and fail on all sync complete failures.
1231 *
1232 * @returns {Promise} A promise which is resolved if sync is complete,
1233 * or rejected on error or recommended action.
1234 */
1235BigIpCluster.prototype.syncComplete = function syncComplete(retryOptions, options) {
1236 const retry = retryOptions || util.DEFAULT_RETRY;
1237 const opts = options || {};
1238
1239 /**
1240 * Returns a promise that resolves true if at least one device is disconnected
1241 * and all supplied devices in the list are connected, otherwise it resolves false.
1242 */
1243 const isSyncDisconnectFailure = function isSyncDisconnectFailure(deviceNames) {
1244 let localHostname = '';
1245
1246 if (!Array.isArray(deviceNames)) {
1247 return q.resolve(false);
1248 }
1249
1250 return this.core.deviceInfo()
1251 .then((deviceInfo) => {
1252 localHostname = deviceInfo.hostname;
1253 return this.getCmSyncStatus();
1254 })
1255 .then((cmSyncStatus) => {
1256 if (cmSyncStatus.disconnected.length === 0) {
1257 return false;
1258 }
1259
1260 cmSyncStatus.connected.push(localHostname);
1261 return deviceNames.every((device) => {
1262 return cmSyncStatus.connected.indexOf(device) >= 0;
1263 });
1264 });
1265 }.bind(this);
1266
1267 const func = function () {
1268 const deferred = q.defer();
1269 this.core.ready()
1270 .then(() => {
1271 return this.core.list('/tm/cm/sync-status', undefined, util.NO_RETRY);
1272 })
1273 .then((response) => {
1274 const mainStats =
1275 response.entries['https://localhost/mgmt/tm/cm/sync-status/0'].nestedStats.entries;
1276 const toGroupTag = 'to group ';
1277 let detailedStats;
1278 let detailKeys;
1279 let description;
1280 let rejectReason;
1281 let toGroupIndex;
1282
1283 if (mainStats.color.description === 'green') {
1284 deferred.resolve();
1285 } else {
1286 // Look for a recommended action
1287 detailedStats =
1288 mainStats['https://localhost/mgmt/tm/cm/syncStatus/0/details'].nestedStats.entries;
1289 detailKeys = Object.keys(detailedStats);
1290 for (let i = 0; i < detailKeys.length; i++) {
1291 description = detailedStats[detailKeys[i]].nestedStats.entries.details.description;
1292 if (description.indexOf('Recommended action') !== -1) {
1293 // If found, look for the group to sync.
1294 toGroupIndex = description.indexOf(toGroupTag);
1295 if (toGroupIndex !== -1) {
1296 rejectReason = {
1297 recommendedAction: {
1298 sync: description.substring(toGroupIndex + toGroupTag.length)
1299 }
1300 };
1301 }
1302 break;
1303 }
1304 }
1305
1306 deferred.reject(rejectReason);
1307 }
1308 })
1309 .catch((err) => {
1310 deferred.reject(err);
1311 })
1312 .done();
1313
1314 return deferred.promise;
1315 };
1316
1317 return util.tryUntil(this, retry, func)
1318 .catch((err) => {
1319 if (err && err.recommendedAction && err.recommendedAction.sync === 'device_trust_group') {
1320 return isSyncDisconnectFailure(opts.connectedDevices)
1321 .then((skipFail) => {
1322 if (skipFail) {
1323 return q.resolve();
1324 }
1325 return q.reject(err);
1326 });
1327 }
1328 return q.reject(err);
1329 });
1330};
1331
1332module.exports = BigIpCluster;