unfinished and broken work :)

This commit is contained in:
Trent Mick 2015-07-25 22:45:20 -07:00
parent 1882dbf18e
commit dfca3e0ace
8 changed files with 659 additions and 29 deletions

86
TODO.md
View File

@ -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.

View File

@ -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"]
}
}

View File

@ -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 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: 'name',
validFields: '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))
}
};
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

View File

@ -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 = {

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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