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 options = require('commander');
|
20 | const fs = require('fs');
|
21 | const q = require('q');
|
22 | const childProcess = require('child_process');
|
23 | const Logger = require('../lib/logger');
|
24 | const ipc = require('../lib/ipc');
|
25 | const signals = require('../lib/signals');
|
26 | const 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 | }());
|