unfinished and broken work :)
This commit is contained in:
parent
1882dbf18e
commit
dfca3e0ace
86
TODO.md
86
TODO.md
@ -1,18 +1,56 @@
|
|||||||
# first
|
# 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:
|
- machines:
|
||||||
- short default output
|
- 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
|
- long '-l' output, -H, -o, -s
|
||||||
- get image defaults and fill those in
|
- get image defaults and fill those in
|
||||||
|
|
||||||
- few more commands? provision (create-machine?)
|
- few more commands? provision (create-machine?)
|
||||||
|
|
||||||
|
|
||||||
- uuid caching
|
- uuid caching
|
||||||
- UUID prefix support
|
- UUID prefix support
|
||||||
- profile command (adding profile, edit, etc.)
|
- 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)
|
# 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
|
- signing: should sigstr include more than just the date? How about the request
|
||||||
path??? Not according to the cloudapi docs.
|
path??? Not according to the cloudapi docs.
|
||||||
- restify-client and bunyan-light without dtrace-provider
|
- restify-client and bunyan-light without dtrace-provider
|
||||||
@ -47,3 +85,51 @@
|
|||||||
add a "joyentcloud foo" subcmd. Reasonable?
|
add a "joyentcloud foo" subcmd. Reasonable?
|
||||||
- windows testing
|
- 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",
|
"defaultProfile": "env",
|
||||||
"dcs": {
|
"dc": {
|
||||||
"us-east-1": "https://us-east-1.api.joyent.com",
|
"us-east-1": "https://us-east-1.api.joyent.com",
|
||||||
"us-west-1": "https://us-west-1.api.joyent.com",
|
"us-west-1": "https://us-west-1.api.joyent.com",
|
||||||
"us-sw-1": "https://us-sw-1.api.joyent.com",
|
"us-sw-1": "https://us-sw-1.api.joyent.com",
|
||||||
"eu-ams-1": "https://eu-ams-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) {
|
CLI.prototype.do_profile = function (subcmd, opts, args, callback) {
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], callback);
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
@ -136,26 +232,65 @@ CLI.prototype.do_profile.help = (
|
|||||||
|
|
||||||
|
|
||||||
CLI.prototype.do_dcs = function (subcmd, opts, args, callback) {
|
CLI.prototype.do_dcs = function (subcmd, opts, args, callback) {
|
||||||
|
var self = this;
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], callback);
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
return;
|
return;
|
||||||
} else if (args.length > 1) {
|
|
||||||
return callback(new Error('too many args: ' + args));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var dcs = this.sdc.config.dcs;
|
var action = args[0] || 'list';
|
||||||
var dcsArray = Object.keys(dcs).map(
|
var name;
|
||||||
function (n) { return {name: n, url: dcs[n]}; });
|
var url;
|
||||||
if (opts.json) {
|
switch (action) {
|
||||||
p(JSON.stringify(dcsArray, null, 4));
|
case 'list':
|
||||||
} else {
|
if (args.length !== 0) {
|
||||||
common.tabulate(dcsArray, {
|
return callback(new errors.UsageError('too many args: ' + args));
|
||||||
columns: 'name,url',
|
}
|
||||||
sort: 'name',
|
var dcs = self.sdc.config.dc;
|
||||||
validFields: 'name,url'
|
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 = [
|
CLI.prototype.do_dcs.options = [
|
||||||
{
|
{
|
||||||
@ -173,12 +308,107 @@ CLI.prototype.do_dcs.help = (
|
|||||||
'List, add or remove datacenters.\n'
|
'List, add or remove datacenters.\n'
|
||||||
+ '\n'
|
+ '\n'
|
||||||
+ 'Usage:\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'
|
+ '\n'
|
||||||
+ '{{options}}'
|
+ '{{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) {
|
CLI.prototype.do_machines = function (subcmd, opts, args, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (opts.help) {
|
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'
|
// 'us-west-1 e91897cf testforyunong2 ubuntu/13.3.0 running 2013-11-08'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
common.tabulate(machines, {
|
common.tabulate(machines, {
|
||||||
columns: 'dc,id,name,state,created',
|
columns: 'dc,id,name,image,state,created',
|
||||||
sort: 'created',
|
sort: 'created',
|
||||||
validFields: 'dc,id,name,type,state,image,package,memory,'
|
validFields: 'dc,id,name,type,state,image,package,memory,'
|
||||||
+ 'disk,created,updated,compute_node,primaryIp'
|
+ '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
|
//---- exports
|
||||||
|
|
||||||
|
@ -189,6 +189,43 @@ CloudAPI.prototype.getAccount = function (options, callback) {
|
|||||||
|
|
||||||
// ---- machines
|
// ---- 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.
|
* List the user's machines.
|
||||||
* <http://apidocs.joyent.com/cloudapi/#ListMachines>
|
* <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
|
* the machines. ListMachines has a max number of machines, so can require
|
||||||
* multiple requests to list all of them.
|
* multiple requests to list all of them.
|
||||||
*/
|
*/
|
||||||
CloudAPI.prototype.listMachines = function (options, callback) {
|
CloudAPI.prototype.listMachines = function listMachines(options, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (callback === undefined) {
|
if (callback === undefined) {
|
||||||
callback = options;
|
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
|
// --- Exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -113,7 +113,7 @@ function tabulate(items, options) {
|
|||||||
// Function to lookup each column field in a row.
|
// Function to lookup each column field in a row.
|
||||||
var colFuncs = columns.map(function (lookup) {
|
var colFuncs = columns.map(function (lookup) {
|
||||||
return new Function(
|
return new Function(
|
||||||
'try { return (this.' + lookup + '); } catch (e) {}');
|
'try { return (this["' + lookup + '"]); } catch (e) {}');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine columns and widths.
|
// Determine columns and widths.
|
||||||
|
@ -10,18 +10,52 @@ var path = require('path');
|
|||||||
var sprintf = require('extsprintf').sprintf;
|
var sprintf = require('extsprintf').sprintf;
|
||||||
|
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
|
var errors = require('./errors');
|
||||||
|
|
||||||
|
|
||||||
var CONFIG_PATH = path.resolve(process.env.HOME, '.sdcconfig.json');
|
var CONFIG_PATH = path.resolve(process.env.HOME, '.sdcconfig.json');
|
||||||
var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.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() {
|
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)) {
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
var userConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
c = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||||
common.objCopy(userConfig, config);
|
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.
|
// Add 'env' profile.
|
||||||
if (!config.profiles) {
|
if (!config.profiles) {
|
||||||
@ -29,6 +63,7 @@ function loadConfigSync() {
|
|||||||
}
|
}
|
||||||
config.profiles.push({
|
config.profiles.push({
|
||||||
name: 'env',
|
name: 'env',
|
||||||
|
dcs: ['joyent'],
|
||||||
user: process.env.SDC_USER || process.env.SDC_ACCOUNT,
|
user: process.env.SDC_USER || process.env.SDC_ACCOUNT,
|
||||||
keyId: process.env.SDC_KEY_ID,
|
keyId: process.env.SDC_KEY_ID,
|
||||||
rejectUnauthorized: common.boolFromString(
|
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
|
//---- exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CONFIG_PATH: CONFIG_PATH,
|
CONFIG_PATH: CONFIG_PATH,
|
||||||
loadConfigSync: loadConfigSync
|
loadConfigSync: loadConfigSync,
|
||||||
|
//XXX
|
||||||
|
//updateConfigSync: updateConfigSync
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
@ -56,6 +56,25 @@ function InternalError(cause, message) {
|
|||||||
util.inherits(InternalError, SDCError);
|
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
|
* CLI usage error
|
||||||
*/
|
*/
|
||||||
@ -116,6 +135,7 @@ util.inherits(MultiError, SDCError);
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
SDCError: SDCError,
|
SDCError: SDCError,
|
||||||
InternalError: InternalError,
|
InternalError: InternalError,
|
||||||
|
ConfigError: ConfigError,
|
||||||
UsageError: UsageError,
|
UsageError: UsageError,
|
||||||
SigningError: SigningError,
|
SigningError: SigningError,
|
||||||
MultiError: MultiError
|
MultiError: MultiError
|
||||||
|
123
lib/sdc.js
123
lib/sdc.js
@ -9,13 +9,15 @@ var assert = require('assert-plus');
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
var auth = require('smartdc-auth');
|
var auth = require('smartdc-auth');
|
||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var format = require('util').format;
|
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var once = require('once');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var restify = require('restify');
|
var restify = require('restify');
|
||||||
|
var sprintf = require('util').format;
|
||||||
|
|
||||||
var cloudapi = require('./cloudapi2');
|
var cloudapi = require('./cloudapi2');
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
|
var errors = require('./errors');
|
||||||
var loadConfigSync = require('./config').loadConfigSync;
|
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.
|
* List machines for the current profile.
|
||||||
@ -188,16 +275,15 @@ SDC.prototype.listMachines = function listMachines(options) {
|
|||||||
assert.object(options, 'options');
|
assert.object(options, 'options');
|
||||||
|
|
||||||
var emitter = new EventEmitter();
|
var emitter = new EventEmitter();
|
||||||
|
|
||||||
async.each(
|
async.each(
|
||||||
self.profile.dcs || Object.keys(self.config.dcs),
|
self.dcs(),
|
||||||
function oneDc(dc, next) {
|
function oneDc(dc, next) {
|
||||||
var client = self._clientFromDc(dc);
|
var client = self._clientFromDc(dc.name);
|
||||||
client.listMachines(function (err, machines) {
|
client.listMachines(function (err, machines) {
|
||||||
if (err) {
|
if (err) {
|
||||||
emitter.emit('dcError', dc, err);
|
emitter.emit('dcError', dc.name, err);
|
||||||
} else {
|
} else {
|
||||||
emitter.emit('data', dc, machines);
|
emitter.emit('data', dc.name, machines);
|
||||||
}
|
}
|
||||||
next();
|
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
|
//---- exports
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user