azure-cli
Version:
Microsoft Azure Cross Platform Command Line tool
659 lines (565 loc) • 20.2 kB
JavaScript
//
// Copyright (c) Microsoft and contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
// If running from MSI installed version, don't use the
// compile on the fly streamline files. MSI install precompiles
// the streamline files
if (!process.env.PRECOMPILE_STREAMLINE_FILES) {
require('streamline').register({ cache: true });
}
var fs = require('fs');
var path = require('path');
var util = require('util');
var _ = require('underscore');
var callerId = require('caller-id');
var CmdLoader = require('./cmdLoader');
var ExtendedCommand = require('./util/extendedcommand');
var log = require('./util/logging');
var telemetry = require('./util/telemetry');
var utilsCore = require('./util/utilsCore');
var Interactor = require('./util/interaction');
//'genMode' is only used on generating command metadata, value: 'asm' or 'arm'
function AzureCli(name, parent, genMode) {
this.parent = parent;
this.output = log;
this.interaction = new Interactor(this);
AzureCli['super_'].call(this, name);
if (parent) {
this._mode = parent._mode;
}
else {
this.initSetup();
this.enableNestedCommands(this);
// Check node.js version.
// Do it after changing exception handler.
this.checkVersion();
this._mode = genMode;
if (!this._mode) {
this._mode = utilsCore.getMode();
}
var loader = new CmdLoader(this, this._mode);
if (genMode) {
log.info('Generating command metadata file: ' + loader.cmdMetadataFile);
loader.harvestPlugins();
loader.harvestModules();
loader.saveCmdMetadata();
log.info('Done');
return;
} else if (loader.cmdMetadataExists()) {
loader.initFromCmdMetadata(AzureCli);
} else {
log.warn('No existing command metadata files. Command will run slow.');
loader.harvestPlugins();
loader.harvestModules();
}
}
}
util.inherits(AzureCli, ExtendedCommand);
_.extend(AzureCli.prototype, {
initSetup: function () {
var self = this;
self.debug = process.env.AZURE_DEBUG === '1';
// Install global unhandled exception handler to make unexpected errors more user-friendly.
if (!self.debug && process.listeners('uncaughtException').length === 0) {
self.uncaughExceptionHandler = function (err) {
self.interaction.clearProgress();
// Exceptions should always be logged to the console
var noConsole = false;
if (!log['default'].transports.console) {
noConsole = true;
self.output.add(self.output.transports.Console);
}
var loggedFullError = false;
if (err.message) {
log.error(err.message);
} else if (err.Message) {
log.error(err.Message);
} else {
log.json('error', err);
loggedFullError = true;
}
if (!loggedFullError) {
if (err.stack) {
log.verbose('stack', err.stack);
}
log.json('silly', err);
}
self.recordError(err);
if (noConsole) {
self.output.remove(self.output.transports.Console);
}
self.exit('error', null, 1);
};
process.addListener('uncaughtException', self.uncaughExceptionHandler);
}
},
getErrorFile: function () {
return path.join(utilsCore.azureDir(), 'azure.err');
},
recordError: function (err) {
if (err) {
var self = this;
var errorFile = self.getErrorFile();
try {
var writeFileFunction = process.env.AZURE_CLI_APPEND_LOGS ? fs.appendFileSync : fs.writeFileSync;
writeFileFunction(errorFile, (new Date().toISOString()) + ':\n' +
util.inspect(err) + '\n' + err.stack + '\n');
log.error('Error information has been recorded to ' + errorFile);
} catch (err2) {
log.warn('Cannot save error information :' + util.inspect(err2));
}
}
},
exit: function (level, message, exitCode) {
var self = this;
self.interaction.clearProgress();
if (message) {
log.log(level, message);
}
if (self.uncaughtExceptionHandler) {
process.removeListener('uncaughtException', self.uncaughExceptionHandler);
}
self.exitProcess(exitCode);
},
exitProcess: function (exitCode) {
//for nice exiting, particularly the winston async logging, we try not to
//use "process.exit()", unless there's non-zero exit code we must return.
if (exitCode) {
process.exit(exitCode);
}
},
normalizeAuthorizationError: function (msg) {
var regex = /.*The \'Authorization\' header is not present or provided in an invalid format.*/ig;
if (msg.match(regex)) {
msg = 'Certificate based Authentication is not supported in current mode: \'' + this._mode +
'\'. Please authenticate using an organizational account via \'azure login\' command.';
}
return msg;
},
execute: function (fn) {
var self = this;
return self.action(function () {
self.setupCommandOutput();
telemetry.currentCommand(self);
telemetry.setMode(self.getMode());
log.info('Executing command ' + self.fullName().bold);
try {
// Expected arguments + options + callback
var argsCount = fn.length <= 1 ? self.args.length + 2 : fn.length;
var args = new Array(argsCount);
var optionIndex = arguments.length - 1;
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] === 'object') {
optionIndex = i;
break;
}
}
// append with options and callback
var options = arguments[optionIndex].optionValues;
args[args.length - 2] = options;
args[args.length - 1] = callback;
// set option arguments into their positional respective places
var freeArguments = 0;
for (var j = 0; j < self.args.length; j++) {
var optionName = utilsCore.camelcase(self.args[j].name);
if (options[optionName]) {
args[j] = options[optionName];
delete options[optionName];
} else if (freeArguments < arguments.length) {
args[j] = arguments[freeArguments];
freeArguments++;
}
}
fn.apply(this, args);
} catch (err) {
callback(err);
}
function callback(err) {
if (err) {
// Exceptions should always be logged to the console unless overturned by test run
var noConsole = false;
if (!process.env.AZURE_NO_ERROR_ON_CONSOLE && !log['default'].transports.console) {
noConsole = true;
self.output.add(self.output.transports.Console);
}
if (err.message) {
log.error(err.message);
log.json('silly', err);
} else if (err.Message) {
if (typeof err.Message === 'object' && typeof err.Message['#'] === 'string') {
var innerError;
try {
innerError = JSON.parse(err.Message['#']);
} catch (e) {
// empty
}
if (innerError) {
if (noConsole) {
self.output.remove(self.output.transports.Console);
}
return callback(innerError);
}
}
err.message = self.normalizeAuthorizationError(err.message);
log.error(err.Message);
log.json('verbose', err);
} else {
log.error(err);
}
telemetry.onError(err, function () {
self.recordError(err);
if (err.stack) {
(self.debug ? log.error : log.verbose)(err.stack);
}
if (noConsole) {
self.output.remove(self.output.transports.Console);
}
self.exit('error', self.fullName().bold + ' command ' + 'failed\n'.red.bold, 1);
});
} else {
telemetry.onFinish(function() {
self.exit('info', self.fullName().bold + ' command ' + 'OK'.green.bold, 0);
});
}
}
});
},
/*
* Extends the default parseOptions to support multiple levels in command parsing.
*/
parseOptions: function (argv) {
var args = [];
var len = argv.length;
var literal = false;
var option;
var arg;
var unknownOptions = [];
// parse options
for (var i = 0; i < len; ++i) {
arg = argv[i];
// literal args after --
if ('--' == arg) {
literal = true;
continue;
}
if (literal) {
args.push(arg);
continue;
}
// find matching Option
option = this.optionFor(arg);
//// patch begins
var commandOption = null;
if (!option && arg[0] === '-') {
var command = this;
var arga = null;
for (var a = 0; a < args.length && command && !commandOption; ++a) {
arga = args[a];
if (command.categories && (arga in command.categories)) {
command = command.categories[arga];
commandOption = command.optionFor(arg);
continue;
}
break;
}
if (!commandOption && arga && command && command.commands) {
for (var j in command.commands) {
if (command.commands[j].name === arga) {
commandOption = command.commands[j].optionFor(arg);
break;
}
}
}
}
//// patch ends
// option is defined
if (option) {
// requires arg
if (option.required) {
arg = argv[++i];
if (!arg) {
return this.optionMissingArgument(option);
}
if ('-' === arg[0]) {
return this.optionMissingArgument(option, arg);
}
this.emit(option.name(), arg);
} else if (option.optional) {
// optional arg
arg = argv[i + 1];
if (!arg || '-' === arg[0]) {
arg = null;
} else {
++i;
}
this.emit(option.name(), arg);
// bool
} else {
this.emit(option.name());
}
continue;
}
// looks like an option
if (arg.length > 1 && '-' == arg[0]) {
unknownOptions.push(arg);
// If the next argument looks like it might be
// an argument for this option, we pass it on.
//// patch: using commandOption if available to detect if the next value is an argument
// If it isn't, then it'll simply be ignored
commandOption = commandOption || { optional : 1 }; // default assumption
if (commandOption.required || (commandOption.optional && argv[i + 1] && '-' != argv[i + 1][0])) {
unknownOptions.push(argv[++i]);
}
continue;
}
// arg
args.push(arg);
}
return { args: args, unknown: unknownOptions };
},
setupCommandLogFormat: function (topMost) {
if (topMost) {
var opts = {
json: false,
level: 'info'
};
log.format(opts);
}
},
setupCommandOutput: function (raw) {
var self = this;
var verbose = 0;
var json = 0;
if (!raw) {
raw = self.normalize(self.parent.rawArgs.slice(2));
}
function hasOption(optionName) {
return self.options.some(function (o) { return o.long === optionName; });
}
for (var i = 0, len = raw.length; i < len; ++i) {
if (hasOption('--json') && raw[i] === '--json') {
++json;
} else if (hasOption('--verbose') &&
(raw[i] === '-v' || raw[i] === '--verbose')) {
++verbose;
}
}
var opts = { json: false };
if (json || verbose) {
if (json) {
opts.json = true;
opts.level = 'data';
}
if (verbose) {
opts.level = (verbose == 1) ? 'verbose' : 'silly';
}
} else {
opts.level = 'info';
}
log.format(opts);
},
enableNestedCommands: function (command) {
if (!command.parent) {
command.option('-v, --version', 'output the application version');
}
if (!command.categories) {
command.categories = {};
}
command.category = function (name) {
var category = command.categories[name];
if (!command.categories[name] || (command.categories[name]).stub && this.executingCmd) {
category = command.categories[name] = new AzureCli(name, this);
command.categories[name].stub = false;
category.helpInformation = command.categoryHelpInformation;
command.enableNestedCommands(category);
}
return category;
};
command.on('*', function () {
var args = command.rawArgs.slice(0, 2);
var raw = command.normalize(command.rawArgs.slice(2));
var category = '*';
if (raw.length > 0) {
category = raw[0];
args = args.concat(raw.slice(1));
}
var i, index;
var targetCmd;
var cat = command.categories[category];
var containsJson = args.some(function (item) { return item === '--json'; });
var containsHelp = args.some(function (item) { return (item === '-h' || item === '--help'); });
//see whether it is top level command, like 'login', 'logout', etc
if (!cat){
index = command.searchCommand(category, command.commands);
if (index !== -1){
targetCmd = require(command.commands[index].filePath);
targetCmd.init.apply(command, [command]);
//route it through the standard process of displaying help, if the command contains the help option.
if (containsHelp) {
targetCmd = command.commands[index];
return targetCmd.commandHelpInformation(containsJson);
}
//execute command by emitting event, which will be routed to the handler.
return this.parse(command.rawArgs);
}
}
//see whether it is a nested command
for (i = 2; cat && i < args.length && args[i] !== '-h' && args[i] !== '--help'; i++) {
index = command.searchCommand(args[i], cat.commands);
if (index !== -1) {
targetCmd = cat.commands[index];
break;
} else {
cat = cat.categories[args[i]];
}
}
//we have found the command, execute it.
if (targetCmd) {
//no need to load the command file, as we get help from the metadata file
if (i + 1 < args.length) {
if (containsHelp) {
return targetCmd.commandHelpInformation(containsJson);
}
}
this.executingCmd = true;
if (!this.workaroundOnAsmSiteCommands(targetCmd, command)) {
targetCmd = require(targetCmd.filePath);
targetCmd.init(command);
}
cat = command.categories[category];
return cat.parse(args);
}
if (!cat) {
var toBlame = (i > 2) ? args[i - 1] : category;
var longFormCurrentMode = this.getLongFormForMode(this._mode);
var otherMode = this.getOtherMode();
var longFormOtherMode = this.getLongFormForMode(otherMode);
var msg = util.format('\'%s\' is not an azure command. See \'azure help\'.\nThe current mode is: %s (%s).', toBlame, this._mode, longFormCurrentMode);
msg += util.format('\nThis *may be* a valid command in \'%s\' (%s) mode. \nUse \'azure config mode %s\' to switch mode and then retry the command.', otherMode, longFormOtherMode, otherMode);
this.exit('error', msg, 1);
} else {
//if we are here, then it is about display help.
command.categoryHelpInformation.apply(cat,[containsJson]);
}
});
},
//Contrary to all other commands, ASM\Site commands were written
//differently that loading the single file containing the command
//is not enough, due to cross referencing, so we load them all.
//For new commands, we will not approve using the style.
workaroundOnAsmSiteCommands: function (targetCmd, command) {
if (path.basename(targetCmd.filePath).indexOf('site.') !== -1) {
var siteCmdDir = path.dirname(targetCmd.filePath);
var siteCmdFiles = utilsCore.getFiles(siteCmdDir, false);
var filesToLoad = {};
var sitePlugins = [];
//It is possible that ._js and precompiled version (.js) co-exist when
//both are laid down by the installer. We should only load .js ones.
siteCmdFiles.forEach(function (f) {
var basename = path.basename(f);
if (basename.indexOf('site.') === 0) {
var nameWithoutExt = basename.substring(0, basename.lastIndexOf('.'));
var ext = path.extname(basename);
if (filesToLoad[nameWithoutExt]) {
if (ext === '.js') {
filesToLoad[nameWithoutExt] = f;
}
} else {
filesToLoad[nameWithoutExt] = f;
}
}
});
Object.keys(filesToLoad).forEach(function (f) {
sitePlugins.push(require(filesToLoad[f]));
});
sitePlugins.forEach(function (plugin) {
if (plugin.init) {
plugin.init(command);
}
});
return true;
} else {
return false;
}
},
command: function (name) {
var args = name.split(/ +/);
var cmd = new AzureCli(args.shift(), this);
cmd.option('-v, --verbose', 'use verbose output');
cmd.option('-vv', 'more verbose with debug output');
cmd.option('--json', 'use json output');
var caller = callerId.getData();
cmd.filePath = caller.filePath;
cmd.helpInformation = cmd.commandHelpInformation;
var index = this.searchCommand(cmd.name, this.commands);
if (index !== -1) {
this.commands[index] = cmd;
} else {
this.commands.push(cmd);
}
cmd.parseExpectedArgs(args);
return cmd;
},
searchCommand: function(name, commands) {
if ( !commands || !name ) return -1;
for (var i = 0; i < commands.length; i++) {
if (commands[i].name === name) {
return i;
}
}
return -1;
},
deprecatedDescription: function (text, newCommand) {
return this.description(util.format('%s (deprecated. This command is deprecated and will be removed in a future version. Please use \"%s\" instead', text, newCommand));
},
detailedDescription: function (str) {
if (0 === arguments.length) return this._detailedDescription;
this._detailedDescription = str;
return this;
},
getLongFormForMode: function (mode) {
return mode === 'arm' ? 'Azure Resource Manager' : 'Azure Service Management';
},
getOtherMode: function () {
return this.getMode() === 'arm' ? 'asm' : 'arm';
},
getMode: function () {
return this._mode;
},
isAsmMode: function () {
return utilsCore.ignoreCaseEquals(this._mode, 'asm');
},
isArmMode: function () {
return utilsCore.ignoreCaseEquals(this._mode, 'arm');
},
checkVersion: function () {
// Uploading VHD needs 0.6.15 on Windows
var version = process.version;
var ver = version.split('.');
var ver1num = parseInt(ver[1], 10);
var ver2num = parseInt(ver[2], 10);
if (ver[0] === 'v0') {
if (ver1num < 6 || (ver1num === 6 && ver2num < 15)) {
throw new Error('You need node.js v0.6.15 or higher to run this code. Your version: ' +
version);
}
if (ver1num === 7 && ver2num <= 7) {
throw new Error('You need node.js v0.6.15 or higher to run this code. Your version ' +
version + ' won\'t work either.');
}
}
}
});
exports = module.exports = AzureCli;