diff --git a/TODO.md b/TODO.md index 5bfa80a..6ff376c 100644 --- a/TODO.md +++ b/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. ... + sdc config alias.image. ... + - 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]: + ... + +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. diff --git a/etc/defaults.json b/etc/defaults.json index de1cbc6..5966933 100644 --- a/etc/defaults.json +++ b/etc/defaults.json @@ -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"] } } diff --git a/lib/cli.js b/lib/cli.js index a218f86..863e8f9 100644 --- a/lib/cli.js +++ b/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 # show particular config var\n' + + ' {{name}} config -a # add/set a config var\n' + + ' {{name}} config -d # 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 # add an SDC cloudapi endpoint\n' + + ' {{name}} dcs rm # 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 '); + assert.string(opts['package'], '--package '); + 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: '', + 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: '', + help: 'The machine image with which to provision. Required.' + }, + { + names: ['package', 'p'], + type: 'string', + helpArg: '', + help: 'The package or instance type for the new machine(s). Required.' + }, + { + names: ['name', 'n'], + type: 'string', + helpArg: '', + 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: '', + 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 \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 \n' + + '\n' + + '{{options}}' +); + + //---- exports diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index a8a0ea5..5436378 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -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. * @@ -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 = { diff --git a/lib/common.js b/lib/common.js index 8e9512a..9381993 100755 --- a/lib/common.js +++ b/lib/common.js @@ -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. diff --git a/lib/config.js b/lib/config.js index 719d4a5..e527f1c 100755 --- a/lib/config.js +++ b/lib/config.js @@ -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: diff --git a/lib/errors.js b/lib/errors.js index 40ab66e..56a2c30 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -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 diff --git a/lib/sdc.js b/lib/sdc.js index 2f0f525..ab25a5a 100644 --- a/lib/sdc.js +++ b/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: , 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