UNPKG

35 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 options = require('commander');
21const BigIp = require('../lib/bigIp');
22const Logger = require('../lib/logger');
23const ipc = require('../lib/ipc');
24const signals = require('../lib/signals');
25const util = require('../lib/util');
26const cryptoUtil = require('../lib/cryptoUtil');
27
28(function run() {
29 const runner = {
30
31 /**
32 * Runs the network setup script
33 *
34 * @param {String[]} argv - The process arguments
35 * @param {Object} testOpts - Options used during testing
36 * @param {Object} testOpts.bigIp - BigIp object to use for testing
37 * @param {Function} cb - Optional cb to call when done
38 */
39 run(argv, testOpts, cb) {
40 const DEFAULT_LOG_FILE = '/tmp/network.log';
41 const ARGS_FILE_ID = `network_${Date.now()}`;
42 const KEYS_TO_MASK = ['-p', '--password', '--set-password', '--set-root-password'];
43 const REQUIRED_OPTIONS = ['host'];
44 const DEFAULT_CIDR = '/24';
45
46 const OPTIONS_TO_UNDEFINE = [
47 'password',
48 'passwordUrl'
49 ];
50
51 const optionsForTest = {};
52 const vlans = [];
53 const selfIps = [];
54 const routes = [];
55 const mgmtRoutes = [];
56 const loggerOptions = {};
57
58 let loggableArgs;
59 let logger;
60 let logFileName;
61 let bigIp;
62 let randomUser;
63 let exiting;
64
65 Object.assign(optionsForTest, testOpts);
66
67
68 try {
69 /* eslint-disable max-len */
70
71 // Can't use getCommonOptions here because of the special reboot handling
72 options
73 .version('4.23.0-beta.3')
74 .option(
75 '--host <ip_address>',
76 'BIG-IP management IP to which to send commands.'
77 )
78 .option(
79 '-u, --user <user>',
80 'BIG-IP admin user name. Default is to create a temporary user (this only works when running on the device).'
81 )
82 .option(
83 '-p, --password [password]',
84 'BIG-IP admin user password. Use this or --password-url. One of these is required when specifying the user.'
85 )
86 .option(
87 '--password-url [password_url]',
88 'URL (file, http(s)) to location that contains BIG-IP admin user password. Use this or --password. One of these is required when specifying the user.'
89 )
90 .option(
91 '--password-encrypted',
92 'Indicates that the password is encrypted (either with encryptDataToFile or generatePassword)'
93 )
94 .option(
95 '--port <port>',
96 'BIG-IP management SSL port to connect to. Default 443.',
97 parseInt
98 )
99 .option(
100 '--background',
101 'Spawn a background process to do the work. If you are running in cloud init, you probably want this option.'
102 )
103 .option(
104 '--signal <signal>',
105 'Signal to send when done. Default NETWORK_DONE.'
106 )
107 .option(
108 '--wait-for <signal>',
109 'Wait for the named signal before running.'
110 )
111 .option(
112 '--log-level <level>',
113 'Log level (none, error, warn, info, verbose, debug, silly). Default is info.', 'info'
114 )
115 .option(
116 '-o, --output <file>',
117 `Log to file as well as console. This is the default if background process is spawned. Default is ${DEFAULT_LOG_FILE}`
118 )
119 .option(
120 '-e, --error-file <file>',
121 'Log exceptions to a specific file. Default is /tmp/cloudLibsError.log, or cloudLibsError.log in --output file directory'
122 )
123 .option(
124 '--no-console',
125 'Do not log to console. Default false (log to console).'
126 )
127 .option(
128 '--single-nic',
129 'Set db variables for single NIC configuration.'
130 )
131 .option(
132 '--multi-nic',
133 'Set db variables for multi NIC configuration.'
134 )
135 .option(
136 '--default-gw <gateway_address>',
137 'Set default gateway to gateway_address.'
138 )
139 .option(
140 '--route <name:name, gw:address, network:network, interface:interface_name>',
141 'Create arbitrary route with name for destination network via gateway address or interface name',
142 util.mapArray,
143 routes
144 )
145 .option(
146 '--mgmt-route <name:name, gw:address, network:network>',
147 'Create management route with name for destination network via gateway address.',
148 util.mapArray,
149 mgmtRoutes
150 )
151 .option(
152 '--local-only',
153 'Create LOCAL_ONLY partition for gateway and assign to traffic-group-local-only.'
154 )
155 .option(
156 '--vlan <name:name, nic:nic, [mtu:mtu], [tag:tag]>',
157 'Create vlan with name on nic (for example, 1.1). Optionally specify mtu and tag. For multiple vlans, use multiple --vlan entries.',
158 util.mapArray,
159 vlans
160 )
161 .option(
162 '--self-ip <name:name, address:ip_address, vlan:vlan_name, [allow:service1:port1 service2:port2], [trafficGroup:traffic_group_name]>',
163 'Create self IP with name and ip_address on vlan with optional port lockdown. For multiple self IPs, use multiple --self-ip entries. Default CIDR prefix is 24 if not specified.',
164 util.mapArray,
165 selfIps
166 )
167 .option(
168 '--discovery-address <ip_address>',
169 'IP address that the BIG-IQ will use for device discovery. This is required for onboarding a BIG-IQ. The IP address must already exist on the BIG-IQ device. For clustering, this should be a Self IP address.'
170 )
171 .option(
172 '--force-reboot',
173 'Force a reboot at the end. This may be necessary for certain configurations.'
174 )
175 .parse(argv);
176 /* eslint-enable max-len */
177
178 loggerOptions.console = options.console;
179 loggerOptions.logLevel = options.logLevel;
180 loggerOptions.module = module;
181
182 if (options.output) {
183 loggerOptions.fileName = options.output;
184 }
185
186 if (options.errorFile) {
187 loggerOptions.errorFile = options.errorFile;
188 }
189
190 logger = Logger.getLogger(loggerOptions);
191 ipc.setLoggerOptions(loggerOptions);
192 util.setLoggerOptions(loggerOptions);
193
194 // Remove specific options with no provided value
195 OPTIONS_TO_UNDEFINE.forEach((opt) => {
196 if (typeof options[opt] === 'boolean') {
197 logger.debug(`No value set for option ${opt}. Removing option.`);
198 options[opt] = undefined;
199 }
200 });
201
202 // Expose options for test code
203 this.options = options;
204
205 for (let i = 0; i < REQUIRED_OPTIONS.length; i++) {
206 if (!options[REQUIRED_OPTIONS[i]]) {
207 const error = `${REQUIRED_OPTIONS[i]} is a required command line option.`;
208
209 ipc.send(signals.CLOUD_LIBS_ERROR);
210
211 util.logError(error, loggerOptions);
212 util.logAndExit(error, 'error', 1);
213 }
214 }
215
216 if (options.user && !(options.password || options.passwordUrl)) {
217 const error = 'If specifying --user, --password or --password-url is required.';
218
219 ipc.send(signals.CLOUD_LIBS_ERROR);
220
221 util.logError(error, loggerOptions);
222 util.logAndExit(error, 'error', 1);
223 }
224
225 // When running in cloud init, we need to exit so that cloud init can complete and
226 // allow the BIG-IP services to start
227 if (options.background) {
228 logFileName = options.output || DEFAULT_LOG_FILE;
229 logger.info('Spawning child process to do the work. Output will be in', logFileName);
230 util.runInBackgroundAndExit(process, logFileName);
231 }
232
233 // Log the input, but don't log passwords
234 loggableArgs = argv.slice();
235 for (let i = 0; i < loggableArgs.length; i++) {
236 if (KEYS_TO_MASK.indexOf(loggableArgs[i]) !== -1) {
237 loggableArgs[i + 1] = '*******';
238 }
239 }
240 logger.info(`${loggableArgs[1]} called with`, loggableArgs.join(' '));
241
242 if (options.singleNic && options.multiNic) {
243 const error = 'Only one of single-nic or multi-nic can be specified.';
244
245 ipc.send(signals.CLOUD_LIBS_ERROR);
246
247 util.logError(error, loggerOptions);
248 util.logAndExit(error, 'error', 1);
249 }
250
251 // Save args in restart script in case we need to reboot to recover from an error
252 util.saveArgs(argv, ARGS_FILE_ID)
253 .then(() => {
254 if (options.waitFor) {
255 logger.info('Waiting for', options.waitFor);
256 return ipc.once(options.waitFor);
257 }
258 return q();
259 })
260 .then(() => {
261 // Whatever we're waiting for is done, so don't wait for
262 // that again in case of a reboot
263 return util.saveArgs(argv, ARGS_FILE_ID, ['--wait-for']);
264 })
265 .then(() => {
266 logger.info('Network setup starting.');
267 ipc.send(signals.NETWORK_RUNNING);
268
269 if (!options.user) {
270 logger.info('Generating temporary user');
271 return cryptoUtil.nextRandomUser();
272 }
273
274 return q(
275 {
276 user: options.user,
277 password: options.password || options.passwordUrl
278 }
279 );
280 })
281 .then((credentials) => {
282 randomUser = credentials.user; // we need this info later to delete it
283
284 // Create the bigIp client object
285 if (optionsForTest.bigIp) {
286 logger.warn('Using test BIG-IP.');
287 bigIp = optionsForTest.bigIp;
288 return q();
289 }
290
291 bigIp = new BigIp({ loggerOptions });
292
293 logger.info('Initializing BIG-IP.');
294 return bigIp.init(
295 options.host,
296 credentials.user,
297 credentials.password,
298 {
299 port: options.port,
300 passwordIsUrl: typeof options.passwordUrl !== 'undefined',
301 passwordEncrypted: options.passwordEncrypted
302 }
303 );
304 })
305 .then(() => {
306 logger.info('Waiting for BIG-IP to be ready.');
307 return bigIp.ready();
308 })
309 .then(() => {
310 logger.info('BIG-IP is ready.');
311
312 if (options.singleNic || options.multiNic) {
313 logger.info('Setting single/multi NIC options.');
314 return bigIp.modify(
315 '/tm/sys/db/provision.1nic',
316 {
317 value: options.singleNic ? 'enable' : 'forced_enable'
318 }
319 )
320 .then((response) => {
321 logger.debug(response);
322
323 return bigIp.modify(
324 '/tm/sys/db/provision.1nicautoconfig',
325 {
326 value: 'disable'
327 }
328 );
329 })
330 .then((response) => {
331 logger.debug(response);
332
333 logger.info('Restarting services.');
334 return bigIp.create(
335 '/tm/util/bash',
336 {
337 command: 'run',
338 utilCmdArgs: "-c 'bigstart restart'"
339 },
340 {
341 noWait: true
342 }
343 );
344 })
345 .then((response) => {
346 logger.debug(response);
347
348 logger.info('Waiting for BIG-IP to be ready after bigstart restart.');
349 return bigIp.ready();
350 });
351 }
352
353 return q();
354 })
355 .then((response) => {
356 logger.debug(response);
357
358 const promises = [];
359 let vlanBody;
360
361 if (vlans.length > 0) {
362 vlans.forEach((vlan) => {
363 if (!vlan.name || !vlan.nic) {
364 q.reject(new Error('Invalid vlan parameters. name and nic are required'));
365 } else {
366 vlanBody = {
367 name: vlan.name,
368 interfaces: [
369 {
370 name: vlan.nic,
371 tagged: !!vlan.tag
372 }
373 ]
374 };
375
376 if (vlan.mtu) {
377 vlanBody.mtu = vlan.mtu;
378 }
379
380 if (vlan.tag) {
381 vlanBody.tag = vlan.tag;
382 }
383
384 promises.push(
385 {
386 promise: bigIp.create,
387 arguments: [
388 '/tm/net/vlan',
389 vlanBody
390 ],
391 // eslint-disable-next-line max-len
392 message: `Creating vlan ${vlan.name} on interface ${vlan.nic} ${(vlan.mtu ? ` mtu ${vlan.mtu}` : '')} ${(vlan.tag ? ` with tag ${vlan.tag}` : ' untagged')}`
393 }
394 );
395 }
396 });
397
398 return util.callInSerial(bigIp, promises);
399 }
400
401 return q();
402 })
403 .then((response) => {
404 logger.debug(response);
405
406 const promises = [];
407
408 for (let i = 0; i < selfIps.length; i++) {
409 const selfIp = selfIps[i];
410 if (selfIp.trafficGroup) {
411 const trafficGroup = selfIp.trafficGroup;
412 promises.push(createTrafficGroup(bigIp, trafficGroup));
413 }
414 }
415
416 q.all(promises);
417 })
418 .then((response) => {
419 logger.debug(response);
420
421 const promises = [];
422 let selfIpBody;
423 let portLockdown;
424
425 for (let i = 0; i < selfIps.length; i++) {
426 const selfIp = selfIps[i];
427
428 if (!selfIp.name || !selfIp.address || !selfIp.vlan) {
429 const message = 'Bad self-ip params. name, address, vlan are required';
430 return q.reject(new Error(message));
431 }
432
433 let address = selfIp.address;
434
435 if (address.indexOf('/') === -1) {
436 address += DEFAULT_CIDR;
437 }
438
439 // general terms (default, all, none) have to be single words
440 // per port terms go in an array
441 portLockdown = 'default';
442 if (selfIp.allow) {
443 portLockdown = selfIp.allow.split(/\s+/);
444 if (
445 portLockdown.length === 1 &&
446 portLockdown[0].indexOf(':') === -1
447 ) {
448 portLockdown = portLockdown[0];
449 }
450 }
451
452 selfIpBody = {
453 address,
454 name: selfIp.name,
455 vlan: `/Common/${selfIp.vlan}`,
456 allowService: portLockdown
457 };
458
459 // eslint-disable-next-line max-len
460 let message = `Creating self IP ${selfIp.name} with address ${address} on vlan ${selfIp.vlan} allowing ${(selfIp.allow ? selfIp.allow : 'default')}`;
461
462 // If traffic group provided, add to create call
463 if (selfIp.trafficGroup) {
464 selfIpBody.trafficGroup = selfIp.trafficGroup;
465 message = `${message} in traffic group ${selfIp.trafficGroup}`;
466 }
467
468 const continueOnErrorMessage = /(Traffic group \(.+?\) does not exist)/;
469 promises.push(
470 {
471 promise: bigIp.create,
472 arguments: [
473 '/tm/net/self',
474 selfIpBody,
475 undefined,
476 {
477 maxRetries: 60,
478 retryIntervalMs: 1000,
479 continueOnErrorMessage
480 }
481 ],
482 message
483 }
484 );
485 }
486
487 return promises.length > 0 ? util.callInSerial(bigIp, promises) : q();
488 })
489 .then((response) => {
490 logger.debug(response);
491
492 // BIG-IQs must set their Discovery Address
493 if (bigIp.isBigIq()) {
494 if (options.discoveryAddress) {
495 logger.info('Setting BIG-IQ Discovery Address to discovery-address option.');
496 return q(options.discoveryAddress);
497 }
498
499 return bigIp.list('/tm/sys/management-ip')
500 .then((mgmtIp) => {
501 logger.info('Setting BIG-IQ Discovery Address to management-ip address');
502 return q(mgmtIp[0].name.split('/')[0]);
503 });
504 }
505
506 return q();
507 })
508 .then((response) => {
509 if (bigIp.isBigIq() && response) {
510 logger.info(`BIG-IQ Discovery Address: ${response}`);
511 return bigIp.replace(
512 '/shared/identified-devices/config/discovery',
513 {
514 discoveryAddress: response
515 },
516 undefined,
517 {
518 maxRetries: 60,
519 retryIntervalMs: 1000,
520 continueOnErrorMessage:
521 'Address does not match a configured self-ip'
522 }
523 );
524 }
525 return q();
526 })
527 .then((response) => {
528 logger.debug(response);
529
530 if (options.localOnly) {
531 logger.info('Creating LOCAL_ONLY partition.');
532 return bigIp.create(
533 '/tm/sys/folder',
534 {
535 name: 'LOCAL_ONLY',
536 partition: '/',
537 deviceGroup: 'none',
538 trafficGroup: 'traffic-group-local-only'
539 }
540 );
541 }
542
543 return q();
544 })
545 .then((response) => {
546 logger.debug(response);
547
548 let routeBody;
549
550 if (options.defaultGw) {
551 logger.info(`Setting default gateway ${options.defaultGw}`);
552
553 routeBody = {
554 name: 'default',
555 gw: options.defaultGw
556 };
557
558 if (options.localOnly) {
559 routeBody.partition = 'LOCAL_ONLY';
560 routeBody.network = 'default';
561 }
562
563 return bigIp.create(
564 '/tm/net/route',
565 routeBody
566 );
567 }
568
569 return q();
570 })
571 .then((response) => {
572 logger.debug(response);
573
574 const promises = [];
575 let routeBody;
576
577 for (let i = 0; i < mgmtRoutes.length; i++) {
578 const route = mgmtRoutes[i];
579 if (!route.name || !route.gw || !route.network) {
580 const message
581 = 'Bad management route params. Name, gateway, network required';
582 return q.reject(new Error(message));
583 }
584
585 let network = route.network;
586 if (network.indexOf('/') === -1) {
587 network += DEFAULT_CIDR;
588 }
589
590 routeBody = {
591 network,
592 name: route.name,
593 gateway: route.gw
594 };
595
596 promises.push(
597 {
598 promise: bigIp.create,
599 arguments: [
600 '/tm/sys/management-route',
601 routeBody
602 ],
603 message:
604 `Creating management route ${route.name} with gateway ${route.gw}`
605 }
606 );
607 }
608
609 return promises.length > 0 ? util.callInSerial(bigIp, promises) : q();
610 })
611 .then((response) => {
612 logger.debug(response);
613
614 const promises = [];
615 let routeBody;
616
617 for (let i = 0; i < routes.length; i++) {
618 const route = routes[i];
619 if (!route.name || !route.network || (!route.gw && !route.interface)) {
620 return q.reject(new Error(
621 'Bad route params. Name, network, and (gateway or interface) required'
622 ));
623 }
624
625 if (route.gw && route.interface) {
626 return q.reject(new Error(
627 'Bad route params. Should provide only 1 of gateway or interface'
628 ));
629 }
630
631 let network = route.network;
632 if (network.indexOf('/') === -1) {
633 network += DEFAULT_CIDR;
634 }
635
636 routeBody = {
637 network,
638 name: route.name
639 };
640
641 let message = `Creating route ${route.name} with`;
642
643 if (route.gw) {
644 routeBody.gw = route.gw;
645 message = `${message} gateway ${route.gw}`;
646 } else if (route.interface) {
647 routeBody.interface = route.interface;
648 message = `${message} interface ${route.interface}`;
649 }
650
651 promises.push(
652 {
653 promise: bigIp.create,
654 arguments: [
655 '/tm/net/route',
656 routeBody
657 ],
658 message
659 }
660 );
661 }
662
663 return promises.length > 0 ? util.callInSerial(bigIp, promises) : q();
664 })
665 .then((response) => {
666 logger.debug(response);
667 logger.info('Saving config.');
668 return bigIp.save();
669 })
670 .then((response) => {
671 logger.debug(response);
672
673 if (options.forceReboot) {
674 // After reboot, we just want to send our done signal,
675 // in case any other scripts are waiting on us. So, modify
676 // the saved args for that
677 const ARGS_TO_STRIP = util.getArgsToStripDuringForcedReboot(options);
678 return util.saveArgs(argv, ARGS_FILE_ID, ARGS_TO_STRIP)
679 .then(() => {
680 logger.info('Rebooting and exiting. Will continue after reboot.');
681 return util.reboot(bigIp);
682 });
683 }
684
685 return q();
686 })
687 .catch((err) => {
688 let message;
689
690 if (!err) {
691 message = 'unknown reason';
692 } else {
693 message = err.message;
694 }
695
696 ipc.send(signals.CLOUD_LIBS_ERROR);
697
698 const error = `Network setup failed: ${message}`;
699 util.logError(error, loggerOptions);
700 util.logAndExit(error, 'error', 1);
701
702 exiting = true;
703 return q();
704 })
705 .done((response) => {
706 logger.debug(response);
707
708 if (!options.user) {
709 logger.info('Deleting temporary user');
710 util.deleteUser(randomUser);
711 }
712
713 if (!options.forceReboot) {
714 util.deleteArgs(ARGS_FILE_ID);
715
716 if (!exiting) {
717 logger.info('BIG-IP network setup complete.');
718 ipc.send(options.signal || signals.NETWORK_DONE);
719 }
720
721 if (cb) {
722 cb();
723 }
724 if (!exiting) {
725 util.logAndExit('Network setup finished.');
726 }
727 } else if (cb) {
728 cb();
729 }
730 });
731
732 // If another script has signaled an error, exit, marking ourselves as DONE
733 ipc.once(signals.CLOUD_LIBS_ERROR)
734 .then(() => {
735 ipc.send(options.signal || signals.NETWORK_DONE);
736 util.logAndExit('ERROR signaled from other script. Exiting');
737 });
738
739 // If we reboot, exit - otherwise cloud providers won't know we're done.
740 // But, if we're the one doing the reboot, we'll exit on our own through
741 // the normal path.
742 if (!options.forceReboot) {
743 ipc.once('REBOOT')
744 .then(() => {
745 // Make sure the last log message is flushed before exiting.
746 util.logAndExit('REBOOT signaled. Exiting.');
747 });
748 }
749 } catch (err) {
750 if (logger) {
751 logger.error('Network setup error:', err);
752 }
753
754 if (cb) {
755 cb();
756 }
757 }
758 }
759 };
760
761 /**
762 * Creates a Traffic Group on BigIP, if the Traffic Group does not exist
763 *
764 * @param {Object} bigIp - bigIp client object
765 * @param {String} trafficGroup - Traffic Group name
766 *
767 * @returns {Promise} Promise that will be resolved when Traffic Group is created,
768 * already exists, or if an error occurs.
769 */
770 function createTrafficGroup(bigIp, trafficGroup) {
771 let createGroup = true;
772 bigIp.list('/tm/cm/traffic-group')
773 .then((response) => {
774 response.forEach((group) => {
775 if (group.name === trafficGroup) {
776 createGroup = false;
777 }
778 });
779 if (createGroup) {
780 return bigIp.create(
781 '/tm/cm/traffic-group',
782 {
783 name: trafficGroup,
784 partition: '/Common'
785 }
786 );
787 }
788 return q();
789 })
790 .catch((err) => {
791 return q.reject(err);
792 });
793 }
794
795 module.exports = runner;
796
797 // If we're called from the command line, run
798 // This allows for test code to call us as a module
799 if (!module.parent) {
800 runner.run(process.argv);
801 }
802}());