UNPKG

25.7 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 q = require('q');
20const BigIp = require('../lib/bigIp');
21const Logger = require('../lib/logger');
22const ActiveError = require('../lib/activeError');
23const cloudProviderFactory = require('../lib/cloudProviderFactory');
24const ipc = require('../lib/ipc');
25const signals = require('../lib/signals');
26const util = require('../lib/util');
27const commonOptions = require('./commonOptions');
28const localCryptoUtil = require('../lib/localCryptoUtil');
29
30(function run() {
31 const runner = {
32 /**
33 * Runs the clustering script
34 *
35 * @param {String[]} argv - The process arguments
36 * @param {Object} testOpts - Options used during testing
37 * @param {Object} testOpts.bigIp - BigIp object to use for testing
38 * @param {Function} cb - Optional cb to call when done
39 */
40 run(argv, testOpts, cb) {
41 const DEFAULT_LOG_FILE = '/tmp/cluster.log';
42 const ARGS_FILE_ID = `cluster_${Date.now()}`;
43 const KEYS_TO_MASK = ['-p', '--password', '--remote-password'];
44 const REQUIRED_OPTIONS = ['host', 'user'];
45
46 const OPTIONS_TO_UNDEFINE = [
47 'remotePassword',
48 'remotePasswordUrl',
49 'password',
50 'passwordUrl'
51 ];
52
53 const providerOptions = {};
54 const loggerOptions = {};
55 const optionsForTest = {};
56
57 let provider;
58 let loggableArgs;
59 let logger;
60 let logFileName;
61 let bigIp;
62 let rebooting;
63 let exiting;
64
65 let bigIqPasswordData = {};
66
67 Object.assign(optionsForTest, testOpts);
68
69 try {
70 /* eslint-disable max-len */
71 const options = commonOptions.getCommonOptions(DEFAULT_LOG_FILE)
72 .option(
73 '--config-sync-ip <config_sync_ip>',
74 'IP address for config sync.'
75 )
76 .option(
77 '--big-iq-failover-peer-ip <peer_ip>',
78 'If configuring a BIG-IQ failover primary, this is the management IP address for the secondary'
79 )
80 .option(
81 '--cloud <provider>',
82 'Cloud provider (aws | azure | etc.). Optionally use this if passwords are stored in cloud storage. This replaces the need for --remote-user/--remote-password(-url). An implemetation of cloudProvider must exist at the correct location.'
83 )
84 .option(
85 ' --big-iq-password-data-uri <key_uri>',
86 ' URI (arn, url, etc.) to a JSON file containing the BIG-IQ passwords (required keys: admin, root)'
87 )
88 .option(
89 ' --big-iq-password-data-encrypted',
90 ' Indicates that the BIG-IQ password data is encrypted (either with encryptDataToFile or generatePassword)'
91 )
92 .option(
93 ' --primary',
94 'If using a cloud provider, indicates that this is the primary. If running on a BIG-IP credentials should be stored. If running on a BIG-IQ, --create-group and --join-group options are not needed.'
95 )
96 .option(
97 ' --provider-options <cloud_options>',
98 'Any options (JSON stringified) that are required for the specific cloud provider.',
99 util.map,
100 providerOptions
101 )
102 .option(
103 '--create-group',
104 'Create a device group with the options:'
105 )
106 .option(
107 ' --device-group <device_group>',
108 ' Name of the device group.'
109 )
110 .option(
111 ' --sync-type <sync_type>',
112 ' Type of sync this cluster is for ("sync-only" | "sync-failover").'
113 )
114 .option(
115 ' --device <device_name>',
116 ' A device name to add to the group. For multiple devices, use multiple --device entries.',
117 util.collect,
118 []
119 )
120 .option(
121 ' --auto-sync',
122 ' Enable auto sync.'
123 )
124 .option(
125 ' --save-on-auto-sync',
126 ' Enable save on sync if auto sync is enabled.'
127 )
128 .option(
129 ' --full-load-on-sync',
130 ' Enable full load on sync.'
131 )
132 .option(
133 ' --asm-sync',
134 ' Enable ASM sync.'
135 )
136 .option(
137 ' --network-failover',
138 ' Enable network failover.'
139 )
140 .option(
141 '--join-group',
142 'Join a remote device group with the options:'
143 )
144 .option(
145 ' --remote-host <remote_ip_address>',
146 ' Managemnt IP for the BIG-IP on which the group exists.'
147 )
148 .option(
149 ' --remote-user <remote_user>',
150 ' Remote BIG-IP admin user name.'
151 )
152 .option(
153 ' --remote-password [remote_password]',
154 ' Remote BIG-IP admin user password. Use this or --remote-password-url'
155 )
156 .option(
157 ' --remote-password-url [remote_password_url]',
158 ' URL (file, http(s)) that contains. Use this or --remote-password'
159 )
160 .option(
161 ' --remote-port <remote_port>',
162 ' Remote BIG-IP port to connect to. Default is port of this BIG-IP.',
163 parseInt
164 )
165 .option(
166 ' --device-group <remote_device_group_name>',
167 ' Name of existing device group on remote BIG-IP to join.'
168 )
169 .option(
170 ' --sync',
171 ' Tell the remote to sync to us after joining the group.'
172 )
173 .option(
174 '--remove-from-cluster',
175 'Remove a device from the cluster'
176 )
177 .option(
178 ' --device-group <device_group>',
179 ' Name of the device group.'
180 )
181 .option(
182 ' --device <device_name>',
183 ' Device name to remove.'
184 )
185 .parse(argv);
186 /* eslint-enable max-len */
187
188 options.port = options.port || 443;
189
190 loggerOptions.console = options.console;
191 loggerOptions.logLevel = options.logLevel;
192 loggerOptions.module = module;
193
194 if (options.output) {
195 loggerOptions.fileName = options.output;
196 }
197
198 if (options.errorFile) {
199 loggerOptions.errorFile = options.errorFile;
200 }
201
202 logger = Logger.getLogger(loggerOptions);
203 ipc.setLoggerOptions(loggerOptions);
204 util.setLoggerOptions(loggerOptions);
205
206 // Remove specific options with no provided value
207 OPTIONS_TO_UNDEFINE.forEach((opt) => {
208 if (typeof options[opt] === 'boolean') {
209 logger.debug(`No value set for option ${opt}. Removing option.`);
210 options[opt] = undefined;
211 }
212 });
213
214 // Expose options for test code
215 this.options = options;
216
217 // Log the input, but don't log passwords
218 loggableArgs = argv.slice();
219 for (let i = 0; i < loggableArgs.length; i++) {
220 if (KEYS_TO_MASK.indexOf(loggableArgs[i]) !== -1) {
221 loggableArgs[i + 1] = '*******';
222 }
223 }
224 logger.info(`${loggableArgs[1]} called with`, loggableArgs.join(' '));
225
226
227 for (let i = 0; i < REQUIRED_OPTIONS.length; i++) {
228 if (!options[REQUIRED_OPTIONS[i]]) {
229 const error = `${REQUIRED_OPTIONS[i]} is a required command line option.`;
230
231 ipc.send(signals.CLOUD_LIBS_ERROR);
232
233 util.logError(error, loggerOptions);
234 util.logAndExit(error, 'error', 1);
235 }
236 }
237
238 if (!options.password && !options.passwordUrl && !options.bigIqPasswordDataUri) {
239 const error =
240 'One of --password, --password-url or --big-iq-password-data-uri is required.';
241
242 ipc.send(signals.CLOUD_LIBS_ERROR);
243
244 util.logError(error, loggerOptions);
245 util.logAndExit(error, 'error', 1);
246 }
247
248 if (options.bigIqFailoverPeerIp && !options.bigIqPasswordDataUri) {
249 const error = '--big-iq-password-data-uri is required for BIG-IQ failover';
250
251 ipc.send(signals.CLOUD_LIBS_ERROR);
252
253 util.logError(error, loggerOptions);
254 util.logAndExit(error, 'error', 1);
255 }
256
257 // When running in cloud init, we need to exit so that cloud init can complete and
258 // allow the BIG-IP services to start
259 if (options.background) {
260 logFileName = options.output || DEFAULT_LOG_FILE;
261 logger.info('Spawning child process to do the work. Output will be in', logFileName);
262 util.runInBackgroundAndExit(process, logFileName);
263 }
264
265 if (options.cloud) {
266 // Create provider client, allowing provider to be overwritten in test code
267 provider = optionsForTest.cloudProvider;
268 if (!provider) {
269 provider = cloudProviderFactory.getCloudProvider(
270 options.cloud,
271 {
272 loggerOptions,
273 clOptions: options
274 }
275 );
276 }
277 }
278
279 // Save args in restart script in case we need to reboot to recover from an error
280 util.saveArgs(argv, ARGS_FILE_ID)
281 .then(() => {
282 if (options.waitFor) {
283 logger.info('Waiting for', options.waitFor);
284 return ipc.once(options.waitFor);
285 }
286 return q();
287 })
288 .then(() => {
289 // Whatever we're waiting for is done, so don't wait for
290 // that again in case of a reboot
291 return util.saveArgs(argv, ARGS_FILE_ID, ['--wait-for']);
292 })
293 .then(() => {
294 logger.info('Cluster starting.');
295 ipc.send(signals.CLUSTER_RUNNING);
296
297 // Retrieve, and save, stored password data
298 if (options.bigIqPasswordDataUri) {
299 return util.readData(options.bigIqPasswordDataUri,
300 true,
301 {
302 clOptions: providerOptions,
303 logger,
304 loggerOptions
305 })
306 .then((uriData) => {
307 if (options.bigIqPasswordDataEncrypted) {
308 return localCryptoUtil.decryptPassword(uriData);
309 }
310 return q(uriData);
311 })
312 .then((uriData) => {
313 bigIqPasswordData = util.lowerCaseKeys(
314 JSON.parse(uriData.trim())
315 );
316 })
317 .then(() => {
318 if (!bigIqPasswordData.admin || !bigIqPasswordData.root) {
319 const msg =
320 'Required passwords missing from --biq-iq-password-data-uri';
321 logger.info(msg);
322 return q.reject(msg);
323 }
324 return q();
325 })
326 .catch((err) => {
327 logger.info('Unable to retrieve JSON from --big-iq-password-data-uri');
328 return q.reject(err);
329 });
330 }
331 return q();
332 })
333 .then(() => {
334 // Create the bigIp client object
335 bigIp = optionsForTest.bigIp || new BigIp({ loggerOptions });
336
337 logger.info('Initializing BIG-IP.');
338 return bigIp.init(
339 options.host,
340 options.user || 'admin',
341 options.password || options.passwordUrl || bigIqPasswordData.admin,
342 {
343 port: options.port,
344 passwordIsUrl: typeof options.passwordUrl !== 'undefined',
345 passwordEncrypted: options.passwordEncrypted,
346 clOptions: providerOptions
347 }
348 );
349 })
350 .then(() => {
351 logger.info('Waiting for BIG-IP to be ready.');
352 return bigIp.ready();
353 })
354 .then(() => {
355 logger.info('BIG-IP is ready.');
356
357 if (options.cloud) {
358 logger.info('Initializing cloud provider.');
359 return provider.init(providerOptions);
360 }
361 return q();
362 })
363 .then(() => {
364 if (options.cloud) {
365 return provider.bigIpReady();
366 }
367 return q();
368 })
369 .then(() => {
370 // Primary BIG-IQ initiates peering with secondary BIG-IQ
371 if (options.primary
372 && options.bigIqFailoverPeerIp
373 && bigIp.isBigIq()
374 ) {
375 logger.info(`Adding ${options.bigIqFailoverPeerIp} as high availability peer.`);
376 return bigIp.cluster.addSecondary(
377 options.bigIqFailoverPeerIp,
378 options.user || 'admin',
379 bigIp.password,
380 bigIqPasswordData.root
381 );
382 }
383 return q();
384 })
385 .then(() => {
386 if (options.configSyncIp) {
387 logger.info('Setting config sync ip.');
388 return bigIp.cluster.configSyncIp(options.configSyncIp);
389 }
390 return q();
391 })
392 .then(() => {
393 if (options.createGroup && bigIp.isBigIp()) {
394 if (!options.deviceGroup || !options.syncType) {
395 throw new Error('Create device group: device-group and sync-type required.');
396 }
397
398 logger.info('Creating group', options.deviceGroup);
399 const deviceGroupOptions = {
400 autoSync: options.autoSync,
401 saveOnAutoSync: options.saveOnAutoSync,
402 fullLoadOnSync: options.fullLoadOnSync,
403 asmSync: options.asmSync,
404 networkFailover: options.networkFailover
405 };
406
407 return bigIp.cluster.createDeviceGroup(
408 options.deviceGroup,
409 options.syncType,
410 options.device,
411 deviceGroupOptions
412 );
413 }
414 return q();
415 })
416 .then((response) => {
417 logger.debug(response);
418
419 // If we are using cloud storage and are the primary, store our credentials
420 if (options.cloud && options.primary && bigIp.isBigIp()) {
421 logger.info('Storing credentials.');
422 return util.tryUntil(
423 provider,
424 util.DEFAULT_RETRY,
425 provider.putPrimaryCredentials
426 );
427 }
428 return q();
429 })
430 .then((response) => {
431 logger.debug(response);
432
433 // options.cloud set indicates that the provider must use some storage
434 // for its primary credentials
435 if (options.cloud && options.joinGroup && bigIp.isBigIp()) {
436 logger.info('Getting primary credentials.');
437 return util.tryUntil(
438 provider,
439 util.DEFAULT_RETRY,
440 provider.getPrimaryCredentials,
441 [options.remoteHost, options.remotePort]
442 );
443 }
444 return q();
445 })
446 .then((response) => {
447 // Don't log the response here - it has the credentials in it
448 if (options.cloud && options.joinGroup && bigIp.isBigIp()) {
449 logger.info('Got primary credentials.');
450
451 options.remoteUser = response.username;
452 options.remotePassword = response.password;
453 options.passwordEncrypted = false;
454 }
455
456 if (options.joinGroup && bigIp.isBigIp()) {
457 logger.info('Joining group.');
458
459 return bigIp.cluster.joinCluster(
460 options.deviceGroup,
461 options.remoteHost,
462 options.remoteUser,
463 options.remotePassword || options.remotePasswordUrl,
464 false,
465 {
466 remotePort: options.remotePort,
467 sync: options.sync,
468 passwordIsUrl: typeof options.remotePasswordUrl !== 'undefined',
469 passwordEncrypted: options.passwordEncrypted
470 }
471 );
472 }
473 return q();
474 })
475 .then((response) => {
476 logger.debug(response);
477
478 if (options.removeFromCluster) {
479 logger.info('Removing', options.device, 'from', options.deviceGroup);
480 return bigIp.cluster.removeFromCluster(options.device);
481 }
482 return q();
483 })
484 .then((response) => {
485 logger.debug(response);
486 logger.info('Waiting for BIG-IP to be active.');
487 return bigIp.active();
488 })
489 .catch((err) => {
490 let message;
491
492 if (!err) {
493 message = 'unknown reason';
494 } else {
495 message = err.message;
496 }
497
498 if (err) {
499 if (err instanceof ActiveError || err.name === 'ActiveError') {
500 logger.warn('BIG-IP active check failed.');
501 rebooting = true;
502 return util.reboot(bigIp, { signalOnly: !(options.reboot) });
503 }
504 }
505
506 ipc.send(signals.CLOUD_LIBS_ERROR);
507
508 const error = `Cluster failed: ${message}`;
509 util.logError(error, loggerOptions);
510 util.logAndExit(error, 'error', 1);
511
512 exiting = true;
513 return q();
514 })
515 .done((response) => {
516 logger.debug(response);
517
518 if ((!rebooting || !options.reboot) && !exiting) {
519 ipc.send(options.signal || signals.CLUSTER_DONE);
520 }
521
522 // Perform callback before final logAndExit
523 if (cb) {
524 cb();
525 }
526
527 if (!rebooting) {
528 util.deleteArgs(ARGS_FILE_ID);
529
530 if (!exiting) {
531 util.logAndExit('Cluster finished.');
532 }
533 } else if (!options.reboot) {
534 // If we are rebooting, but we were called with --no-reboot, send signal
535 if (!exiting) {
536 util.logAndExit('Cluster finished. Reboot required but not rebooting.');
537 }
538 } else {
539 util.logAndExit('Cluster finished. Reboot required.');
540 }
541 });
542
543 // If another script has signaled an error, exit, marking ourselves as DONE
544 ipc.once(signals.CLOUD_LIBS_ERROR)
545 .then(() => {
546 ipc.send(options.signal || signals.CLUSTER_DONE);
547 util.logAndExit('ERROR signaled from other script. Exiting');
548 });
549
550 // If we reboot due to some other script, exit - otherwise cloud providers
551 // won't know we're done. If we forced the reboot ourselves, we will exit
552 // when that call completes.
553 ipc.once('REBOOT')
554 .then(() => {
555 if (!rebooting) {
556 util.logAndExit('REBOOT signaled. Exiting.');
557 }
558 });
559 } catch (err) {
560 if (logger) {
561 logger.error('Clustering error:', err);
562 }
563 }
564 }
565 };
566
567 module.exports = runner;
568
569 // If we're called from the command line, run
570 // This allows for test code to call us as a module
571 if (!module.parent) {
572 runner.run(process.argv);
573 }
574}());