unfinished and broken work :)
This commit is contained in:
parent
1882dbf18e
commit
dfca3e0ace
86
TODO.md
86
TODO.md
@ -1,18 +1,56 @@
|
||||
# first
|
||||
|
||||
- Adding/removing DCs. Want this to work reasonably mainly to support dogfooding
|
||||
with internal DCs. Also to allow this to be a general tool for *SDC*,
|
||||
with default values for JPC, but not restricted to. Also allow the right thing
|
||||
to happen if JPC adds new DCs.
|
||||
|
||||
- Don't use "all" catch all DC. Use "joyent" alias for the default set.
|
||||
- Add DC aliases (starting a generic aliasing).
|
||||
- Show the aliases in `sdc dcs`
|
||||
- support aliases in the command lookups. Method to get the DCs for the current
|
||||
profile
|
||||
XXX START HERE
|
||||
- changing dcs:
|
||||
sdc dcs add us-beta-4 https://beta4-cloudapi.joyent.us
|
||||
sdc dcs set-url us-beta-4 https://beta4-cloudapi.joyent.us
|
||||
sdc dcs rm us-beta-4
|
||||
Note: If having config.dcs override this means that any DC change means
|
||||
that user doesn't "see" DC changes by new node-sdc versions.
|
||||
- Impl 'sdc config' to edit these easily on the CLI.
|
||||
sdc config alias.dc.<alias> <dc-name-1> <dc-name-2> ...
|
||||
sdc config alias.image.<alias> <image-uuid> ...
|
||||
|
||||
- machines:
|
||||
- short default output
|
||||
- 'cdate' short created, just the date
|
||||
- 'img' is 'name/version'
|
||||
- 'sid' is the short id prefix
|
||||
- long '-l' output, -H, -o, -s
|
||||
- get image defaults and fill those in
|
||||
|
||||
- few more commands? provision (create-machine?)
|
||||
|
||||
|
||||
- uuid caching
|
||||
- UUID prefix support
|
||||
- profile command (adding profile, edit, etc.)
|
||||
- `sdc config` command similar to git config
|
||||
|
||||
|
||||
# account vs user vs subuser vs role
|
||||
|
||||
See MANTA-2401 and scrum discussion from 14 Aug 2014..
|
||||
Suggestion: use "account" and "user" since "since those are the documented
|
||||
tools for the abstractions and that's what smartdc uses."
|
||||
Envvars: SDC_ACCOUNT and SDC_USER.
|
||||
|
||||
|
||||
# later (in no particular order)
|
||||
|
||||
- adding a dc:
|
||||
sdc dcs -a us-beta-4 https://beta4-cloudapi.joyent.us
|
||||
or
|
||||
- signing: should sigstr include more than just the date? How about the request
|
||||
path??? Not according to the cloudapi docs.
|
||||
- restify-client and bunyan-light without dtrace-provider
|
||||
@ -47,3 +85,51 @@
|
||||
add a "joyentcloud foo" subcmd. Reasonable?
|
||||
- windows testing
|
||||
|
||||
# ideas
|
||||
|
||||
- `sdc whatsnew` grabs current images and packages and compares to last time
|
||||
it was called to short new images/packages. Perhaps for other resources too.
|
||||
|
||||
|
||||
|
||||
# notes on `sdc provision` (in progress)
|
||||
|
||||
- Lame: I <# that our packages are separate for kvm vs smartos usage. Do they
|
||||
have conflicting data?
|
||||
- Q: "package" or "instance-type"? Probably package for now.
|
||||
|
||||
Need: dc (if profile has multiple, have a settable preferred dc for provisions),
|
||||
image (uuid, name to get latest, have a settable preferred?), package (settable
|
||||
preferred, settable preferred ram).
|
||||
|
||||
What about using "same as last time" or a way to say that?
|
||||
|
||||
Want interactive asking for missing params if TTY? -f to avoid.
|
||||
|
||||
$ sdc provision ...
|
||||
Datacenter [us-west-1]: <prompt>
|
||||
...
|
||||
|
||||
Name: AWS equiv is 'aws-cli ec2 run-instances'
|
||||
http://docs.aws.amazon.com/cli/latest/reference/ec2/run-instances.html
|
||||
E.g.:
|
||||
|
||||
aws ec2 run-instances --image-id ami-c3b8d6aa --count 1 --instance-type t1.micro --key-name MyKeyPair --security-groups MySecurityGroup
|
||||
|
||||
sdc create-machine ...
|
||||
sdc provision ...
|
||||
sdc provision -i IMAGE -p PACKAGE
|
||||
shortcut?
|
||||
sdc provision IMAGE:PKG ?
|
||||
sdc provision IMAGE PKG ?
|
||||
sdc provision image=IMAGE package=PKG ? no
|
||||
|
||||
sdc provision -i IMAGE -p PKG -c 3 --name 'test%d' # printf codes for the count
|
||||
sdc provision -d east -i base -p g3-standard-1 -n shirley # -d|--dc
|
||||
|
||||
Clarify what IMAGE can be. "Name" matching is first against one's own private
|
||||
images, then against public ones. UUID. UUID prefix. "Name/version" matching.
|
||||
Image alias (`sdc config alias.bob $uuid`, though for git that alias is for
|
||||
*commands*. Perhaps `sdc alias image.bob $uuid`. Dunno. Later.).
|
||||
|
||||
Similar matching for PKG.
|
||||
|
@ -1,9 +1,12 @@
|
||||
{
|
||||
"defaultProfile": "env",
|
||||
"dcs": {
|
||||
"dc": {
|
||||
"us-east-1": "https://us-east-1.api.joyent.com",
|
||||
"us-west-1": "https://us-west-1.api.joyent.com",
|
||||
"us-sw-1": "https://us-sw-1.api.joyent.com",
|
||||
"eu-ams-1": "https://eu-ams-1.api.joyent.com"
|
||||
},
|
||||
"dcAlias": {
|
||||
"joyent": ["us-east-1", "us-sw-1", "us-west-1", "eu-ams-1"]
|
||||
}
|
||||
}
|
||||
|
319
lib/cli.js
319
lib/cli.js
@ -87,6 +87,102 @@ CLI.prototype.init = function (opts, args, callback) {
|
||||
};
|
||||
|
||||
|
||||
CLI.prototype.do_config = function (subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var action;
|
||||
var actions = [];
|
||||
if (opts.add) actions.push('add');
|
||||
if (opts['delete']) actions.push('delete');
|
||||
if (opts.edit) actions.push('edit');
|
||||
if (actions.length === 0) {
|
||||
action = 'show';
|
||||
} else if (actions.length > 1) {
|
||||
return callback(new errors.UsageError(
|
||||
'cannot specify more than one action: ' + actions.join(', ')));
|
||||
} else {
|
||||
action = actions[0];
|
||||
}
|
||||
var numArgs = {
|
||||
|
||||
}
|
||||
|
||||
if (action === 'show') {
|
||||
var c = common.objCopy(this.sdc.config);
|
||||
delete c._defaults;
|
||||
delete c._user;
|
||||
if (args.length > 1) {
|
||||
return callback(new errors.UsageError('too many args'));
|
||||
} else if (args.length === 1) {
|
||||
var lookups = args[0].split(/\./g);
|
||||
for (var i = 0; i < lookups.length; i++) {
|
||||
c = c[lookups[i]];
|
||||
if (c === undefined) {
|
||||
return callback(new errors.UsageError(
|
||||
'no such config var: ' + args[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof(c) === 'string') {
|
||||
console.log(c)
|
||||
} else {
|
||||
console.log(JSON.stringify(c, null, 4));
|
||||
}
|
||||
} else if (action === 'add') {
|
||||
if (args.length !== 2)
|
||||
return callback(new errors.UsageError('incorrect number of args'));
|
||||
XXX
|
||||
} else if (action === 'delete') {
|
||||
if (args.length !== 1)
|
||||
return callback(new errors.UsageError('incorrect number of args'));
|
||||
XXX
|
||||
} else if (action === 'edit') {
|
||||
if (args.length !== 0)
|
||||
return callback(new errors.UsageError('incorrect number of args'));
|
||||
XXX
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
CLI.prototype.do_config.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['add', 'a'],
|
||||
type: 'bool',
|
||||
help: 'Add a config var.'
|
||||
},
|
||||
{
|
||||
names: ['delete', 'd'],
|
||||
type: 'bool',
|
||||
help: 'Delete a config var.'
|
||||
},
|
||||
{
|
||||
names: ['edit', 'e'],
|
||||
type: 'bool',
|
||||
help: 'Edit config in $EDITOR.'
|
||||
}
|
||||
];
|
||||
CLI.prototype.do_config.help = (
|
||||
'Show and edit the `sdc` CLI config.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} config # show config\n'
|
||||
+ ' {{name}} config <name> # show particular config var\n'
|
||||
+ ' {{name}} config -a <name> <value> # add/set a config var\n'
|
||||
+ ' {{name}} config -d <name> # delete a config var\n'
|
||||
+ ' {{name}} config -e # edit config in $EDITOR\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
|
||||
CLI.prototype.do_profile = function (subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
@ -136,26 +232,65 @@ CLI.prototype.do_profile.help = (
|
||||
|
||||
|
||||
CLI.prototype.do_dcs = function (subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
return callback(new Error('too many args: ' + args));
|
||||
}
|
||||
|
||||
var dcs = this.sdc.config.dcs;
|
||||
var dcsArray = Object.keys(dcs).map(
|
||||
function (n) { return {name: n, url: dcs[n]}; });
|
||||
if (opts.json) {
|
||||
p(JSON.stringify(dcsArray, null, 4));
|
||||
} else {
|
||||
common.tabulate(dcsArray, {
|
||||
columns: 'name,url',
|
||||
sort: 'name',
|
||||
validFields: 'name,url'
|
||||
});
|
||||
var action = args[0] || 'list';
|
||||
var name;
|
||||
var url;
|
||||
switch (action) {
|
||||
case 'list':
|
||||
if (args.length !== 0) {
|
||||
return callback(new errors.UsageError('too many args: ' + args));
|
||||
}
|
||||
var dcs = self.sdc.config.dc;
|
||||
var dcsArray = Object.keys(dcs).map(
|
||||
function (n) { return {name: n, url: dcs[n]}; });
|
||||
if (self.sdc.config.dcAlias) {
|
||||
Object.keys(self.sdc.config.dcAlias).forEach(function (alias) {
|
||||
dcsArray.push(
|
||||
{alias: alias, names: self.sdc.config.dcAlias[alias]});
|
||||
});
|
||||
}
|
||||
if (opts.json) {
|
||||
p(JSON.stringify(dcsArray, null, 4));
|
||||
} else {
|
||||
for (var i = 0; i < dcsArray.length; i++) {
|
||||
var d = dcsArray[i];
|
||||
d.name = (d.name ? d.name : d.alias + '*');
|
||||
d.url = d.url || d.names.join(', ');
|
||||
}
|
||||
common.tabulate(dcsArray, {
|
||||
columns: 'name,url',
|
||||
sort: 'alias,name',
|
||||
validFields: 'name,url,alias,names'
|
||||
});
|
||||
}
|
||||
callback();
|
||||
break;
|
||||
case 'rm':
|
||||
if (args.length !== 2) {
|
||||
return callback(new errors.UsageError(
|
||||
'incorrect number of args: ' + args));
|
||||
}
|
||||
name = args[1];
|
||||
XXX
|
||||
break;
|
||||
case 'add':
|
||||
if (args.length !== 3) {
|
||||
return callback(new errors.UsageError(
|
||||
'incorrect number of args: ' + args));
|
||||
}
|
||||
name = args[1];
|
||||
url = args[2];
|
||||
XXX
|
||||
break;
|
||||
default:
|
||||
return callback(new errors.UsageError('unknown dcs command: ' + args))
|
||||
}
|
||||
callback();
|
||||
};
|
||||
CLI.prototype.do_dcs.options = [
|
||||
{
|
||||
@ -173,12 +308,107 @@ CLI.prototype.do_dcs.help = (
|
||||
'List, add or remove datacenters.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} dcs\n'
|
||||
+ ' {{name}} dcs # list DCs (and DC aliases marked with "*")\n'
|
||||
+ ' {{name}} dcs add <name> <url> # add an SDC cloudapi endpoint\n'
|
||||
+ ' {{name}} dcs rm <name> # remove a DC\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
|
||||
CLI.prototype.do_provision = function (subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
return callback(new Error('too many args: ' + args));
|
||||
}
|
||||
var sdc = this.sdc;
|
||||
|
||||
assert.string(opts.image, '--image <img>');
|
||||
assert.string(opts['package'], '--package <pkg>');
|
||||
assert.number(opts.count)
|
||||
|
||||
// XXX
|
||||
/*
|
||||
* Should all this move into sdc.createMachine? yes
|
||||
*
|
||||
* - lookup image, package, networks from args
|
||||
* - assign names
|
||||
* - start provisions (slight stagger, max N at a time)
|
||||
* - return immediately, or '-w|--wait'
|
||||
*/
|
||||
async.series([
|
||||
function lookups(next) {
|
||||
async.parallel([
|
||||
//XXX
|
||||
//sdc.lookup(image)
|
||||
])
|
||||
},
|
||||
function provisions(next) {
|
||||
|
||||
},
|
||||
function wait(next) {
|
||||
next();
|
||||
}
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
CLI.prototype.do_provision.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['dc', 'd'],
|
||||
type: 'string',
|
||||
helpArg: '<dc>',
|
||||
help: 'The datacenter in which to provision. Required if the current'
|
||||
+ ' profile includes more than one datacenter. Use `sdc profile`'
|
||||
+ ' to list profiles and `sdc dcs` to list available datacenters.'
|
||||
},
|
||||
{
|
||||
names: ['image', 'i'],
|
||||
type: 'string',
|
||||
helpArg: '<img>',
|
||||
help: 'The machine image with which to provision. Required.'
|
||||
},
|
||||
{
|
||||
names: ['package', 'p'],
|
||||
type: 'string',
|
||||
helpArg: '<pkg>',
|
||||
help: 'The package or instance type for the new machine(s). Required.'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
type: 'string',
|
||||
helpArg: '<name>',
|
||||
help: 'A name for the machine. If not specified, a short random name'
|
||||
+ ' will be generated.',
|
||||
// TODO: for count>1 support '%d' code in name: foo0, foo1, ...
|
||||
},
|
||||
{
|
||||
names: ['count', 'c'],
|
||||
type: 'positiveInteger',
|
||||
'default': 1,
|
||||
helpArg: '<n>',
|
||||
help: 'The number of machines to provision. Default is 1.'
|
||||
},
|
||||
];
|
||||
CLI.prototype.do_provision.help = (
|
||||
'Provision a new virtual machine instance.\n'
|
||||
+ 'Alias: create-machine.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} provision <options>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
CLI.prototype.do_provision.aliases = ['create-machine'];
|
||||
|
||||
|
||||
CLI.prototype.do_machines = function (subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
@ -211,7 +441,7 @@ CLI.prototype.do_machines = function (subcmd, opts, args, callback) {
|
||||
// 'us-west-1 e91897cf testforyunong2 ubuntu/13.3.0 running 2013-11-08'
|
||||
/* END JSSTYLED */
|
||||
common.tabulate(machines, {
|
||||
columns: 'dc,id,name,state,created',
|
||||
columns: 'dc,id,name,image,state,created',
|
||||
sort: 'created',
|
||||
validFields: 'dc,id,name,type,state,image,package,memory,'
|
||||
+ 'disk,created,updated,compute_node,primaryIp'
|
||||
@ -249,6 +479,63 @@ CLI.prototype.do_machines.help = (
|
||||
|
||||
|
||||
|
||||
CLI.prototype.do_machine_audit = function (subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
//XXX Support multiple machines.
|
||||
return callback(new Error('too many args: ' + args));
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
this.sdc.machineAudit({machine: id}, function (err, audit, dc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
for (var i = 0; i < audit.length; i++) {
|
||||
audit[i].dc = dc;
|
||||
}
|
||||
if (opts.json) {
|
||||
p(JSON.stringify(audit, null, 4));
|
||||
} else {
|
||||
return callback(new error.InternalError("tabular output for audit NYI")); // XXX
|
||||
//common.tabulate(audit, {
|
||||
// columns: 'dc,id,name,state,created',
|
||||
// sort: 'created',
|
||||
// validFields: 'dc,id,name,type,state,image,package,memory,'
|
||||
// + 'disk,created,updated,compute_node,primaryIp'
|
||||
//});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
CLI.prototype.do_machine_audit.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
CLI.prototype.do_machine_audit.help = (
|
||||
'List machine actions.\n'
|
||||
+ '\n'
|
||||
+ 'Note: On the *client*-side, this adds the "dc" attribute to each\n'
|
||||
+ 'audit record.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} machine-audit <machine>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
|
@ -189,6 +189,43 @@ CloudAPI.prototype.getAccount = function (options, callback) {
|
||||
|
||||
// ---- machines
|
||||
|
||||
/**
|
||||
* Get a machine by id.
|
||||
*
|
||||
* XXX add getCredentials equivalent
|
||||
* XXX cloudapi docs don't doc the credentials=true option
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} id (required) The machine id.
|
||||
* @param {Function} callback of the form `function (err, machine, response)`
|
||||
*/
|
||||
CloudAPI.prototype.getMachine = function getMachine(options, callback) {
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.id, 'options.id');
|
||||
assert.func(callback, 'callback');
|
||||
|
||||
var path = sprintf('/%s/machines/%s', self.user, options.id);
|
||||
self._getAuthHeaders(function (hErr, headers) {
|
||||
if (hErr) {
|
||||
callback(hErr);
|
||||
return;
|
||||
}
|
||||
var opts = {
|
||||
path: path,
|
||||
headers: headers
|
||||
};
|
||||
self.client.get(opts, function (err, req, res, body) {
|
||||
if (err) {
|
||||
callback(err, null, res);
|
||||
} else {
|
||||
callback(null, body, res);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* List the user's machines.
|
||||
* <http://apidocs.joyent.com/cloudapi/#ListMachines>
|
||||
@ -206,7 +243,7 @@ CloudAPI.prototype.getAccount = function (options, callback) {
|
||||
* the machines. ListMachines has a max number of machines, so can require
|
||||
* multiple requests to list all of them.
|
||||
*/
|
||||
CloudAPI.prototype.listMachines = function (options, callback) {
|
||||
CloudAPI.prototype.listMachines = function listMachines(options, callback) {
|
||||
var self = this;
|
||||
if (callback === undefined) {
|
||||
callback = options;
|
||||
@ -272,6 +309,44 @@ CloudAPI.prototype.listMachines = function (options, callback) {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List machine audit (successful actions on the machine).
|
||||
*
|
||||
* XXX IMO this endpoint should be called ListMachineAudit in cloudapi.
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} id (required) The machine id.
|
||||
* @param {Function} callback of the form `function (err, audit, response)`
|
||||
*/
|
||||
CloudAPI.prototype.machineAudit = function machineAudit(options, callback) {
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.id, 'options.id');
|
||||
assert.func(callback, 'callback');
|
||||
|
||||
var path = sprintf('/%s/machines/%s/audit', self.user, options.id);
|
||||
//XXX This `client.get` block is duplicated. Add a convenience function for it:
|
||||
self._getAuthHeaders(function (hErr, headers) {
|
||||
if (hErr) {
|
||||
callback(hErr);
|
||||
return;
|
||||
}
|
||||
var opts = {
|
||||
path: path,
|
||||
headers: headers
|
||||
};
|
||||
self.client.get(opts, function (err, req, res, body) {
|
||||
if (err) {
|
||||
callback(err, null, res);
|
||||
} else {
|
||||
callback(null, body, res);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// --- Exports
|
||||
|
||||
module.exports = {
|
||||
|
@ -113,7 +113,7 @@ function tabulate(items, options) {
|
||||
// Function to lookup each column field in a row.
|
||||
var colFuncs = columns.map(function (lookup) {
|
||||
return new Function(
|
||||
'try { return (this.' + lookup + '); } catch (e) {}');
|
||||
'try { return (this["' + lookup + '"]); } catch (e) {}');
|
||||
});
|
||||
|
||||
// Determine columns and widths.
|
||||
|
@ -10,18 +10,52 @@ var path = require('path');
|
||||
var sprintf = require('extsprintf').sprintf;
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
var CONFIG_PATH = path.resolve(process.env.HOME, '.sdcconfig.json');
|
||||
var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json');
|
||||
var OVERRIDE_KEYS = ['dc', 'dcAlias'];
|
||||
|
||||
|
||||
/**
|
||||
* Load the 'sdc' config. This is a merge of the built-in "defaults" (at
|
||||
* etc/defaults.json) and the "user" config (at ~/.sdcconfig.json if it
|
||||
* exists).
|
||||
*
|
||||
* This includes some internal data on keys with a leading underscore.
|
||||
*/
|
||||
function loadConfigSync() {
|
||||
var config = JSON.parse(fs.readFileSync(DEFAULTS_PATH, 'utf8'));
|
||||
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
|
||||
var _defaults = JSON.parse(c);
|
||||
var config = JSON.parse(c);
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
var userConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
common.objCopy(userConfig, config);
|
||||
c = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
var _user = JSON.parse(c);
|
||||
var userConfig = JSON.parse(c);
|
||||
if (typeof(userConfig) !== 'object' || Array.isArray(userConfig)) {
|
||||
throw new errors.ConfigError(
|
||||
sprintf('"%s" is not an object', CONFIG_PATH));
|
||||
}
|
||||
// These special keys are merged into the key of the same name in the
|
||||
// base "defaults.json".
|
||||
Object.keys(userConfig).forEach(function (key) {
|
||||
if (~OVERRIDE_KEYS.indexOf(key) && config[key] !== undefined) {
|
||||
Object.keys(userConfig[key]).forEach(function (subKey) {
|
||||
if (userConfig[key][subKey] === null) {
|
||||
delete config[key][subKey];
|
||||
} else {
|
||||
config[key][subKey] = userConfig[key][subKey];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
config[key] = userConfig[key];
|
||||
}
|
||||
});
|
||||
|
||||
config._user = _user;
|
||||
}
|
||||
config._defaults = _defaults;
|
||||
|
||||
// Add 'env' profile.
|
||||
if (!config.profiles) {
|
||||
@ -29,6 +63,7 @@ function loadConfigSync() {
|
||||
}
|
||||
config.profiles.push({
|
||||
name: 'env',
|
||||
dcs: ['joyent'],
|
||||
user: process.env.SDC_USER || process.env.SDC_ACCOUNT,
|
||||
keyId: process.env.SDC_KEY_ID,
|
||||
rejectUnauthorized: common.boolFromString(
|
||||
@ -39,11 +74,24 @@ function loadConfigSync() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply the given key:value updates to the user config and save it out.
|
||||
*
|
||||
* @param config {Object} The loaded config, as from `loadConfigSync`.
|
||||
* @param updates {Object} key/value pairs to update.
|
||||
*/
|
||||
function updateUserConfigSync(config, updates) {
|
||||
XXX
|
||||
///XXX START HERE: to implement for 'sdc dcs add foo bar'
|
||||
}
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
module.exports = {
|
||||
CONFIG_PATH: CONFIG_PATH,
|
||||
loadConfigSync: loadConfigSync
|
||||
loadConfigSync: loadConfigSync,
|
||||
//XXX
|
||||
//updateConfigSync: updateConfigSync
|
||||
};
|
||||
// vim: set softtabstop=4 shiftwidth=4:
|
||||
|
@ -56,6 +56,25 @@ function InternalError(cause, message) {
|
||||
util.inherits(InternalError, SDCError);
|
||||
|
||||
|
||||
/**
|
||||
* CLI usage error
|
||||
*/
|
||||
function ConfigError(cause, message) {
|
||||
if (message === undefined) {
|
||||
message = cause;
|
||||
cause = undefined;
|
||||
}
|
||||
assert.string(message);
|
||||
SDCError.call(this, {
|
||||
cause: cause,
|
||||
message: message,
|
||||
code: 'Config',
|
||||
exitStatus: 1
|
||||
});
|
||||
}
|
||||
util.inherits(ConfigError, SDCError);
|
||||
|
||||
|
||||
/**
|
||||
* CLI usage error
|
||||
*/
|
||||
@ -116,6 +135,7 @@ util.inherits(MultiError, SDCError);
|
||||
module.exports = {
|
||||
SDCError: SDCError,
|
||||
InternalError: InternalError,
|
||||
ConfigError: ConfigError,
|
||||
UsageError: UsageError,
|
||||
SigningError: SigningError,
|
||||
MultiError: MultiError
|
||||
|
123
lib/sdc.js
123
lib/sdc.js
@ -9,13 +9,15 @@ var assert = require('assert-plus');
|
||||
var async = require('async');
|
||||
var auth = require('smartdc-auth');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var once = require('once');
|
||||
var path = require('path');
|
||||
var restify = require('restify');
|
||||
var sprintf = require('util').format;
|
||||
|
||||
var cloudapi = require('./cloudapi2');
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
var loadConfigSync = require('./config').loadConfigSync;
|
||||
|
||||
|
||||
@ -163,6 +165,91 @@ SDC.prototype._clientFromDc = function _clientFromDc(dc) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the resolved array of `{name: <dc-name>, url: <dc-url>}` for all
|
||||
* DCs for the current profile.
|
||||
*
|
||||
* @throws {Error} If an unknown DC name is encountered.
|
||||
* XXX make that UnknownDcError.
|
||||
*/
|
||||
SDC.prototype.dcs = function dcs() {
|
||||
var self = this;
|
||||
var aliases = self.config.dcAlias || {};
|
||||
var resolved = [];
|
||||
(self.profile.dcs || Object.keys(self.config.dcs)).forEach(function (n) {
|
||||
var names = aliases[n] || [n];
|
||||
names.forEach(function (name) {
|
||||
if (!self.config.dcs[name]) {
|
||||
throw new Error(sprintf('unknown dc "%s" for "%s" profile',
|
||||
name, self.profile.name));
|
||||
}
|
||||
resolved.push({
|
||||
name: name,
|
||||
url: self.config.dcs[name]
|
||||
});
|
||||
});
|
||||
});
|
||||
return resolved;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Find a machine in the set of DCs for the current profile.
|
||||
*
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} machine (required) The machine id.
|
||||
* XXX support name matching, prefix, etc.
|
||||
* @param {Function} callback `function (err, machine, dc)`
|
||||
* Returns the machine object (as from cloudapi GetMachine) and the `dc`,
|
||||
* e.g. "us-west-1".
|
||||
*/
|
||||
SDC.prototype.findMachine = function findMachine(options, callback) {
|
||||
//XXX Eventually this can be cached for a *full* uuid. Arguably for a
|
||||
// uuid prefix or machine alias match, it cannot be cached, because an
|
||||
// ambiguous machine could have been added.
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.machine, 'options.machine');
|
||||
assert.func(callback, 'callback');
|
||||
var callback = once(callback);
|
||||
|
||||
var errs = [];
|
||||
var foundMachine;
|
||||
var foundDc;
|
||||
async.each(
|
||||
self.dcs(),
|
||||
function oneDc(dc, next) {
|
||||
var client = self._clientFromDc(dc.name);
|
||||
client.getMachine({id: options.machine}, function (err, machine) {
|
||||
if (err) {
|
||||
errs.push(err);
|
||||
} else if (machine) {
|
||||
foundMachine = machine;
|
||||
foundDc = dc.name;
|
||||
// Return early on an unambiguous match.
|
||||
// XXX When other than full 'id' is supported, this isn't unambiguous.
|
||||
callback(null, foundMachine, foundDc);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function done(surpriseErr) {
|
||||
if (surpriseErr) {
|
||||
callback(surpriseErr);
|
||||
} else if (foundMachine) {
|
||||
callback(null, foundMachine, foundDc)
|
||||
} else if (errs.length) {
|
||||
callback(errs.length === 1 ?
|
||||
errs[0] : new errors.MultiError(errs));
|
||||
} else {
|
||||
callback(new errors.InternalError(
|
||||
'unexpected error finding machine ' + options.id));
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* List machines for the current profile.
|
||||
@ -188,16 +275,15 @@ SDC.prototype.listMachines = function listMachines(options) {
|
||||
assert.object(options, 'options');
|
||||
|
||||
var emitter = new EventEmitter();
|
||||
|
||||
async.each(
|
||||
self.profile.dcs || Object.keys(self.config.dcs),
|
||||
self.dcs(),
|
||||
function oneDc(dc, next) {
|
||||
var client = self._clientFromDc(dc);
|
||||
var client = self._clientFromDc(dc.name);
|
||||
client.listMachines(function (err, machines) {
|
||||
if (err) {
|
||||
emitter.emit('dcError', dc, err);
|
||||
emitter.emit('dcError', dc.name, err);
|
||||
} else {
|
||||
emitter.emit('data', dc, machines);
|
||||
emitter.emit('data', dc.name, machines);
|
||||
}
|
||||
next();
|
||||
});
|
||||
@ -210,6 +296,31 @@ SDC.prototype.listMachines = function listMachines(options) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the audit for the given machine.
|
||||
*
|
||||
* @param {Object} options
|
||||
* - {String} machine (required) The machine id.
|
||||
* XXX support `machine` being more than just the UUID.
|
||||
* @param {Function} callback of the form `function (err, audit, dc)`
|
||||
*/
|
||||
SDC.prototype.machineAudit = function machineAudit(options, callback) {
|
||||
var self = this;
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.machine, 'options.machine');
|
||||
|
||||
self.findMachine({machine: options.machine}, function (err, machine, dc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var client = self._clientFromDc(dc);
|
||||
client.machineAudit({id: machine.id}, function (err, audit) {
|
||||
callback(err, audit, dc);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
|
Reference in New Issue
Block a user