1 | /**
|
2 | * Copyright 2016-2018 F5 Networks, Inc.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 |
|
17 | ;
|
18 |
|
19 | const assert = require('assert');
|
20 | const q = require('q');
|
21 | const util = require('./util');
|
22 | const Logger = require('./logger');
|
23 |
|
24 | const DEVICE_GROUP_PATH = '/tm/cm/device-group/';
|
25 | const TRUST_DOMAIN_NAME = 'Root';
|
26 |
|
27 | let 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 | */
|
42 | function 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 | */
|
74 | BigIpCluster.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 | */
|
137 | BigIpCluster.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 | */
|
177 | BigIpCluster.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 | */
|
212 | BigIpCluster.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 | */
|
251 | BigIpCluster.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 | */
|
296 | BigIpCluster.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 | */
|
395 | BigIpCluster.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 | */
|
424 | BigIpCluster.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 | */
|
466 | BigIpCluster.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 | */
|
517 | BigIpCluster.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 | */
|
563 | BigIpCluster.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 | */
|
628 | BigIpCluster.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 | */
|
970 | BigIpCluster.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 | */
|
1011 | BigIpCluster.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 | */
|
1069 | BigIpCluster.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 | */
|
1102 | BigIpCluster.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 | */
|
1157 | BigIpCluster.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 | */
|
1193 | BigIpCluster.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 | */
|
1235 | BigIpCluster.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 |
|
1332 | module.exports = BigIpCluster;
|