UNPKG

12.4 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 options = require('commander');
20const fs = require('fs');
21const q = require('q');
22const childProcess = require('child_process');
23const Logger = require('../lib/logger');
24const ipc = require('../lib/ipc');
25const signals = require('../lib/signals');
26const util = require('../lib/util');
27
28(function run() {
29 const runner = {
30 /**
31 * Runs an arbitrary script
32 *
33 * @param {String[]} argv - The process arguments
34 * @param {Function} cb - Optional cb to call when done
35 */
36 run(argv, cb) {
37 const DEFAULT_LOG_FILE = '/tmp/runScript.log';
38 const ARGS_FILE_ID = `runScript_${Date.now()}`;
39 const KEYS_TO_MASK = ['--cl-args'];
40
41 const loggerOptions = {};
42
43 let loggableArgs;
44 let logger;
45 let logFileName;
46 let clArgIndex;
47 let exiting;
48
49 try {
50 /* eslint-disable max-len */
51 options
52 .version('4.23.0-beta.3')
53 .option(
54 '--background',
55 'Spawn a background process to do the work. If you are running in cloud init, you probably want this option.'
56 )
57 .option(
58 '-f, --file <script>',
59 'File name of script to run.'
60 )
61 .option(
62 '-u, --url <url>',
63 'URL from which to download script to run. This will override --file.'
64 )
65 .option(
66 '--cl-args <command_line_args>',
67 'String of arguments to send to the script as command line arguments.'
68 )
69 .option(
70 '--shell <full_path_to_shell>',
71 'Specify the shell to run the command in. Default is to run command as a separate process (not through a shell).'
72 )
73 .option(
74 '--signal <signal>',
75 'Signal to send when done. Default SCRIPT_DONE.'
76 )
77 .option(
78 '--wait-for <signal>',
79 'Wait for the named signal before running.'
80 )
81 .option(
82 '--cwd <directory>',
83 'Current working directory for the script to run in.'
84 )
85 .option(
86 '--log-level <level>',
87 'Log level (none, error, warn, info, verbose, debug, silly). Default is info.',
88 'info'
89 )
90 .option(
91 '-o, --output <file>',
92 `Log to file as well as console. This is the default if background process is spawned. Default is ${DEFAULT_LOG_FILE}`
93 )
94 .option(
95 '-e, --error-file <file>',
96 'Log exceptions to a specific file. Default is /tmp/cloudLibsError.log, or cloudLibsError.log in --output file directory'
97 )
98 .option(
99 '--no-console',
100 'Do not log to console. Default false (log to console).'
101 )
102 .parse(argv);
103 /* eslint-enable max-len */
104
105 loggerOptions.console = options.console;
106 loggerOptions.logLevel = options.logLevel;
107 loggerOptions.module = module;
108
109 if (options.output) {
110 loggerOptions.fileName = options.output;
111 }
112
113 if (options.errorFile) {
114 loggerOptions.errorFile = options.errorFile;
115 }
116
117 logger = Logger.getLogger(loggerOptions);
118 ipc.setLoggerOptions(loggerOptions);
119 util.setLoggerOptions(loggerOptions);
120
121 // When running in cloud init, we need to exit so that cloud init can complete and
122 // allow the BIG-IP services to start
123 if (options.background) {
124 logFileName = options.output || DEFAULT_LOG_FILE;
125 logger.info('Spawning child process to do the work. Output will be in', logFileName);
126 util.runInBackgroundAndExit(process, logFileName);
127 }
128
129 // Log the input, but don't cl-args since it could contain a password
130 loggableArgs = argv.slice();
131 for (let i = 0; i < loggableArgs.length; i++) {
132 if (KEYS_TO_MASK.indexOf(loggableArgs[i]) !== -1) {
133 loggableArgs[i + 1] = '*******';
134 }
135 }
136 logger.info(`${loggableArgs[1]} called with`, loggableArgs.join(' '));
137
138 const mungedArgs = argv.slice();
139
140 // With cl-args, we need to restore the single quotes around the args - shells remove them
141 if (options.clArgs) {
142 logger.debug('Found clArgs - checking for single quotes');
143 clArgIndex = mungedArgs.indexOf('--cl-args') + 1;
144 logger.debug('clArgIndex:', clArgIndex);
145 if (mungedArgs[clArgIndex][0] !== "'") {
146 logger.debug('Wrapping clArgs in single quotes');
147 mungedArgs[clArgIndex] = `'${mungedArgs[clArgIndex]}'`;
148 }
149 }
150
151 // Save args in restart script in case we need to reboot to recover from an error
152 logger.debug('Saving args for', options.file || options.url);
153 util.saveArgs(mungedArgs, ARGS_FILE_ID)
154 .then(() => {
155 logger.debug('Args saved for', options.file || options.url);
156 if (options.waitFor) {
157 logger.info('Waiting for', options.waitFor);
158 return ipc.once(options.waitFor);
159 }
160 return q();
161 })
162 .then(() => {
163 // Whatever we're waiting for is done, so don't wait for
164 // that again in case of a reboot
165 if (options.waitFor) {
166 logger.debug('Signal received.');
167 return util.saveArgs(mungedArgs, ARGS_FILE_ID, ['--wait-for']);
168 }
169 return q();
170 })
171 .then(() => {
172 const deferred = q.defer();
173
174 if (options.url) {
175 logger.debug('Downloading', options.url);
176 util.download(options.url)
177 .then((fileName) => {
178 options.file = fileName;
179 fs.chmod(fileName, 0o755, () => {
180 deferred.resolve();
181 });
182 })
183 .catch((err) => {
184 deferred.reject(err);
185 })
186 .done();
187 } else {
188 deferred.resolve();
189 }
190
191 return deferred.promise;
192 })
193 .then(() => {
194 const deferred = q.defer();
195 const cpOptions = {};
196
197 let args = [];
198 let cp;
199
200 logger.info(options.file, 'starting.');
201 if (options.file) {
202 ipc.send(signals.SCRIPT_RUNNING);
203
204 if (options.cwd) {
205 cpOptions.cwd = options.cwd;
206 }
207
208 if (options.shell) {
209 cpOptions.shell = options.shell;
210 cp = childProcess.exec(`${options.file} ${options.clArgs}`, cpOptions);
211 } else {
212 if (options.clArgs) {
213 args = options.clArgs.split(/\s+/);
214 }
215 cp = childProcess.spawn(options.file, args, cpOptions);
216 }
217
218 cp.stdout.on('data', (data) => {
219 logger.info(data.toString().trim());
220 });
221
222 cp.stderr.on('data', (data) => {
223 logger.error(data.toString().trim());
224 });
225
226 cp.on('exit', (code, signal) => {
227 const status = signal || code.toString();
228 logger.info(
229 options.file,
230 'exited with',
231 (signal ? 'signal' : 'code'),
232 status
233 );
234 deferred.resolve();
235 });
236
237 cp.on('error', (err) => {
238 logger.error(options.file, 'error', err);
239 });
240 } else {
241 deferred.resolve();
242 }
243
244 return deferred.promise;
245 })
246 .catch((err) => {
247 ipc.send(signals.CLOUD_LIBS_ERROR);
248
249 const error = `Running custom script failed: ${err}`;
250 util.logError(error, loggerOptions);
251 util.logAndExit(error, 'error', 1);
252 exiting = true;
253 })
254 .done((response) => {
255 logger.debug(response);
256
257 util.deleteArgs(ARGS_FILE_ID);
258 if (!exiting) {
259 ipc.send(options.signal || signals.SCRIPT_DONE);
260 }
261
262 if (cb) {
263 cb();
264 }
265
266 if (!exiting) {
267 util.logAndExit('Custom script finished.');
268 }
269 });
270
271 // If another script has signaled an error, exit, marking ourselves as DONE
272 ipc.once(signals.CLOUD_LIBS_ERROR)
273 .then(() => {
274 ipc.send(options.signal || signals.SCRIPT_DONE);
275 util.logAndExit('ERROR signaled from other script. Exiting');
276 });
277 } catch (err) {
278 if (logger) {
279 logger.error('Custom script error:', err);
280 }
281 }
282
283 // If we reboot, exit - otherwise cloud providers won't know we're done
284 ipc.once('REBOOT')
285 .then(() => {
286 util.logAndExit('REBOOT signaled. Exiting.');
287 });
288 }
289 };
290
291 module.exports = runner;
292
293 // If we're called from the command line, run
294 // This allows for test code to call us as a module
295 if (!module.parent) {
296 runner.run(process.argv);
297 }
298}());