diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6041da6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Joyent, Inc., All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d30c8f --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +node-smartdc is a node.js client library for interacting with the Joyent +SmartDataCenter API. This package additionally contains a CLI you can use +to write scripts encapsulating most common tasks. + +## Installation + +You probably want to install this package globally, so the CLI commands are +always in your path. + + npm install smartdc -g + +## Usage + +### CLI + +There are CLI commands corresponding to almost every action available in the +SmartDataCenter API; see the +[SmartDataCenter documentation][http://apidocs.joyent.com/napi/cloudapi/] for +complete information, but to get started, you'll want to run the following: + + sdc-setup + +The `sdc-setup` command will prompt you for your username and password, and +upload your SSH key. All the rest of the CLI commands use your RSA private +key for signing requests to the API, rather than sending your password to the +Joyent API. Once you've run `sdc-setup` (and set the environment variables +it indicates), you can provision a machine, and check it's status. For example, +here's an example that creates a new node.js machine and tags it as a +'test' machine, then you can grab the status a few times until it's `running`. + +Note this assumes you've also got [jsontool][https://github.com/trentm/json] +installed: + + sdc-createmachine -e nodejs -n demo -t group=test + ... + sdc-listmachines | json 0.state + provisioning + sdc-listmachines | json 0.state + provisioning + sdc-listmachines | json 0.state + running + +At that point, you can ssh into the machine; try this: + + ssh-add + ssh -A admin@`./sdc-listmachines | json 0.ips[0]` + +Note that we added your keys to the SSH agent, so that you can use the CLI +seamlessly on your new SmartMachine. Once you've played around and are done, +you can dispose of it; shut it down, then poll until it's `stopped`. + + sdc-listmachines | json 0.id | xargs sdc-stopmachine + sdc-listmachines | json 0.state + stopped + sdc-listmachines | json 0.id | xargs sdc-deletemachine + +There's a lot more you can do, like manage snapshots, analytics, keys, tags, +etc. + +### Programmatic Usage + + var fs = require('fs'); + var smartdc = require('smartdc'); + + // Read in the SSH private key + var home = process.env.HOME; + var key = fs.readFileSync(home + '/.ssh/id_rsa', 'ascii'); + + var client = smartdc.createClient({ + url: 'https://api.no.de', + key: key, + keyId: '//keys/id_rsa' + }); + + client.listMachines(function(err, machines) { + if (err) { + console.log('Unable to list machines: ' + e); + return; + } + + machines.forEach(function(m) { + console.log('Machine: ' + JSON.stringify(m, null, 2)); + }); + }); + +Check out the source documentation for JSDocs on the API. + +## License + +MIT. + +## Bugs + +See . diff --git a/bin/sdc-addmachinetags b/bin/sdc-addmachinetags new file mode 100755 index 0000000..3b83541 --- /dev/null +++ b/bin/sdc-addmachinetags @@ -0,0 +1,65 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "tag": [String, Array], + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "t": ["--tag"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var opts = {}; + if (parsed.name) + opts.name = parsed.name; + if (!parsed.tag) + common.usage(usageStr, 1, '--tag required'); + + var tags = {}; + for (var i = 0; i < parsed.tag.length; i++) { + var tmp = parsed.tag[i].split('='); + if (!tmp || tmp.length !== 2) { + console.error(parsed.tag[i] + ' is an invalid tag; try foo=bar'); + process.exit(1); + } + tags[tmp[0]] = tmp[1]; + } + + var client = common.newClient(parsed); + client.addMachineTags(parsed.argv.remain[0], tags, common.callback); +}, usageStr); diff --git a/bin/sdc-createinstrumentation b/bin/sdc-createinstrumentation new file mode 100755 index 0000000..a1920a2 --- /dev/null +++ b/bin/sdc-createinstrumentation @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "clone": Number, + "debug": Boolean, + "decomposition": String, + "help": Boolean, + "identity": path, + "keyId": String, + "module": String, + "predicate": String, + "stat": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "c": ["--clone"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--module"], + "n": ["--decomposition"], + "p": ["--predicate"], + "s": ["--stat"], + "u": ["--url"] +}; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var opts = {}; + if (parsed.module) + opts.module = parsed.module; + + if (parsed.stat) + opts.stat = parsed.stat; + + if (parsed.decomposition) + opts.decomposition = parsed.decomposition; + + if (parsed.predicate) + opts.predicate = parsed.predicate; + + if (parsed.clone) + opts.clone = parsed.clone; + + var client = common.newClient(parsed); + client.createInstrumentation(opts, common.callback); +}); + + + + diff --git a/bin/sdc-createkey b/bin/sdc-createkey new file mode 100755 index 0000000..4a7d4ad --- /dev/null +++ b/bin/sdc-createkey @@ -0,0 +1,71 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "name": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "n": ["--name"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' public_ssh_key'; + + +///--- Internal Functions + +function loadNewKey(key) { + try { + return fs.readFileSync(key, 'ascii'); + } catch(e) { + common.usage(usageStr, 2, 'Unable to load key ' + identity + ': ' + e); + } +} + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'ssh_key required'); + + var opts = { + key: loadNewKey(parsed.argv.remain[0]) + }; + if (parsed.name) { + opts.name = parsed.name; + } else { + var name = parsed.argv.remain[0].split('/'); + opts.name = name[name.length - 1]; + } + + var client = common.newClient(parsed); + client.createKey(opts, common.callback); +}, usageStr); diff --git a/bin/sdc-createmachine b/bin/sdc-createmachine new file mode 100755 index 0000000..744f9d2 --- /dev/null +++ b/bin/sdc-createmachine @@ -0,0 +1,63 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var https = require('https'); +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "dataset": String, + "help": Boolean, + "identity": path, + "keyId": String, + "name": String, + "package": String, + "tag": [String, Array], + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "e": ["--dataset"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "n": ["--name"], + "p": ["--package"], + "t": ["--tag"], + "u": ["--url"] +}; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var opts = {} + if (parsed.dataset) opts.dataset = parsed.dataet; + if (parsed.name) opts.name = parsed.name; + if (parsed['package']) opts['package'] = parsed['package']; + if (parsed.tag) { + for (var i = 0; i < parsed.tag.length; i++) { + var tmp = parsed.tag[i].split('='); + if (!tmp || tmp.length !== 2) { + console.error(parsed.tag[i] + ' is an invalid tag; try foo=bar'); + process.exit(1); + } + opts['tag.' + tmp[0]] = tmp[1]; + } + } + + var client = common.newClient(parsed); + client.createMachine(opts, common.callback); +}); diff --git a/bin/sdc-createmachinesnapshot b/bin/sdc-createmachinesnapshot new file mode 100755 index 0000000..1f6e4a1 --- /dev/null +++ b/bin/sdc-createmachinesnapshot @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "name": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--machine"], + "n": ["--name"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var opts = {}; + if (parsed.name) + opts.name = parsed.name; + + var client = common.newClient(parsed); + client.createMachineSnapshot(parsed.argv.remain[0], opts, common.callback); +}, usageStr); diff --git a/bin/sdc-deleteinstrumentation b/bin/sdc-deleteinstrumentation new file mode 100755 index 0000000..1f24deb --- /dev/null +++ b/bin/sdc-deleteinstrumentation @@ -0,0 +1,47 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' instrumentation_id'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'instrumentation required'); + + var id = parseInt(parsed.argv.remain[0], 10); + var client = common.newClient(parsed); + + client.deleteInstrumentation(id, common.callback); +}, usageStr); diff --git a/bin/sdc-deletekey b/bin/sdc-deletekey new file mode 100755 index 0000000..dc38466 --- /dev/null +++ b/bin/sdc-deletekey @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' key_name'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'key_name required'); + + var client = common.newClient(parsed); + client.deleteKey(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-deletemachine b/bin/sdc-deletemachine new file mode 100755 index 0000000..abd6a4c --- /dev/null +++ b/bin/sdc-deletemachine @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.deleteMachine(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-deletemachinesnapshot b/bin/sdc-deletemachinesnapshot new file mode 100755 index 0000000..2fa749c --- /dev/null +++ b/bin/sdc-deletemachinesnapshot @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "machine": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--machine"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' snapshot'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (!parsed.machine) + common.usage(usageStr, 1, 'machine required'); + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'snapshot required'); + + var client = common.newClient(parsed); + client.deleteMachineSnapshot(parsed.machine, + parsed.argv.remain[0], + common.callback); +}, usageStr); diff --git a/bin/sdc-deletemachinetag b/bin/sdc-deletemachinetag new file mode 100755 index 0000000..5f8ba3f --- /dev/null +++ b/bin/sdc-deletemachinetag @@ -0,0 +1,55 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "machine": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--machine"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' tag (use * to delete all)'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (!parsed.machine) + common.usage(usageStr, 1, 'machine required'); + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'snapshot required'); + + var client = common.newClient(parsed); + if (parsed.argv.remain[0] === '*') { + client.deleteMachineTags(parsed.machine, common.callback); + } else { + client.deleteMachineTag(parsed.machine, + parsed.argv.remain[0], + common.callback); + } +}, usageStr); diff --git a/bin/sdc-describeanalytics b/bin/sdc-describeanalytics new file mode 100755 index 0000000..3f5fb43 --- /dev/null +++ b/bin/sdc-describeanalytics @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.describeAnalytics(common.callback); +}); diff --git a/bin/sdc-getdataset b/bin/sdc-getdataset new file mode 100755 index 0000000..5fc877a --- /dev/null +++ b/bin/sdc-getdataset @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' dataset'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'dataset required'); + + var client = common.newClient(parsed); + client.getDataset(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-getinstrumentation b/bin/sdc-getinstrumentation new file mode 100755 index 0000000..923dd80 --- /dev/null +++ b/bin/sdc-getinstrumentation @@ -0,0 +1,52 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "value": Boolean, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"], + "v": ["--value"] +}; + +var usageStr = common.buildUsageString(Options) + ' instrumentation_id'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'instrumentation required'); + + var id = parseInt(parsed.argv.remain[0], 10); + var client = common.newClient(parsed); + + if (parsed.value) + return client.getInstrumentationValue(id, common.callback); + + return client.getInstrumentation(id, common.callback); +}, usageStr); diff --git a/bin/sdc-getkey b/bin/sdc-getkey new file mode 100755 index 0000000..cbc3e5f --- /dev/null +++ b/bin/sdc-getkey @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' key_name'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'key_name required'); + + var client = common.newClient(parsed); + client.getKey(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-getmachine b/bin/sdc-getmachine new file mode 100755 index 0000000..532791e --- /dev/null +++ b/bin/sdc-getmachine @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' machine(id)'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine(id) required'); + + var client = common.newClient(parsed); + client.getMachine(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-getmachinesnapshot b/bin/sdc-getmachinesnapshot new file mode 100755 index 0000000..9a18da1 --- /dev/null +++ b/bin/sdc-getmachinesnapshot @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "machine": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--machine"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' snapshot'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (!parsed.machine) + common.usage(usageStr, 1, 'machine required'); + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'snapshot required'); + + var client = common.newClient(parsed); + client.getMachineSnapshot(parsed.machine, + parsed.argv.remain[0], + common.callback); +}, usageStr); diff --git a/bin/sdc-getmachinetag b/bin/sdc-getmachinetag new file mode 100755 index 0000000..00cf8e3 --- /dev/null +++ b/bin/sdc-getmachinetag @@ -0,0 +1,58 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "machine": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "m": ["--machine"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' tag'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (!parsed.machine) + common.usage(usageStr, 1, 'machine required'); + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'tag required'); + + function callback(err, obj) { + if (err) { + console.error(JSON.parse(err.message).message); + process.exit(3); + } + + console.log(obj); + } + + var client = common.newClient(parsed); + client.getMachineTag(parsed.machine, parsed.argv.remain[0], callback); +}, usageStr); diff --git a/bin/sdc-getpackage b/bin/sdc-getpackage new file mode 100755 index 0000000..29a0d19 --- /dev/null +++ b/bin/sdc-getpackage @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' package'; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'package required'); + + var client = common.newClient(parsed); + client.getPackage(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-listdatacenters b/bin/sdc-listdatacenters new file mode 100755 index 0000000..a955ad8 --- /dev/null +++ b/bin/sdc-listdatacenters @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.listDatacenters(common.callback); +}); diff --git a/bin/sdc-listdatasets b/bin/sdc-listdatasets new file mode 100755 index 0000000..0c283e1 --- /dev/null +++ b/bin/sdc-listdatasets @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.listDatasets(common.callback); +}); diff --git a/bin/sdc-listinstrumentations b/bin/sdc-listinstrumentations new file mode 100755 index 0000000..7604bfe --- /dev/null +++ b/bin/sdc-listinstrumentations @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.listInstrumentations(common.callback); +}); diff --git a/bin/sdc-listkeys b/bin/sdc-listkeys new file mode 100755 index 0000000..79a72e8 --- /dev/null +++ b/bin/sdc-listkeys @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.listKeys(common.callback); +}); diff --git a/bin/sdc-listmachines b/bin/sdc-listmachines new file mode 100755 index 0000000..13d545a --- /dev/null +++ b/bin/sdc-listmachines @@ -0,0 +1,82 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "dataset": String, + "help": Boolean, + "identity": path, + "keyId": String, + "limit": Number, + "memory": Number, + "name": String, + "offset": Number, + "state": String, + "tag": [String, Array], + "type": String, + "tombstone": Number, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "b": ["--tombstone"], + "d": ["--debug"], + "e": ["--dataset"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "l": ["--limit"], + "m": ["--memory"], + "n": ["--name"], + "o": ["--offset"], + "s": ["--state"], + "t": ["--tag"], + "y": ["--type"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + + var opts = {}; + var tags = {}; + if (parsed.dataset) opts.dataset = parsed.dataet; + if (parsed.limit) opts.limit = parsed.limit; + if (parsed.memory) opts.memory = parsed.memory; + if (parsed.name) opts.name = parsed.name; + if (parsed.offset) opts.offset = parsed.offset; + if (parsed.state) opts.state = parsed.state; + if (parsed.tombstone) opts.tombstone = parsed.tombstone; + if (parsed.type) opts.type = parsed.type; + + if (parsed.tag) { + for (var i = 0; i < parsed.tag.length; i++) { + var tmp = parsed.tag[i].split('='); + if (!tmp || tmp.length !== 2) { + console.error(parsed.tag[i] + ' is an invalid tag; try foo=bar'); + process.exit(1); + } + tags[tmp[0]] = tmp[1]; + } + } + + var client = common.newClient(parsed); + client.listMachines(opts, tags, common.callback); + +}); diff --git a/bin/sdc-listmachinesnapshots b/bin/sdc-listmachinesnapshots new file mode 100755 index 0000000..2bbb200 --- /dev/null +++ b/bin/sdc-listmachinesnapshots @@ -0,0 +1,44 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.listMachineSnapshots(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-listmachinetags b/bin/sdc-listmachinetags new file mode 100755 index 0000000..95bfa69 --- /dev/null +++ b/bin/sdc-listmachinetags @@ -0,0 +1,44 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options) + ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.listMachineTags(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-listpackages b/bin/sdc-listpackages new file mode 100755 index 0000000..06438cc --- /dev/null +++ b/bin/sdc-listpackages @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + var client = common.newClient(parsed); + client.listPackages(common.callback); +}); diff --git a/bin/sdc-rebootmachine b/bin/sdc-rebootmachine new file mode 100755 index 0000000..2b73efa --- /dev/null +++ b/bin/sdc-rebootmachine @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.rebootMachine(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-resizemachine b/bin/sdc-resizemachine new file mode 100755 index 0000000..15383cc --- /dev/null +++ b/bin/sdc-resizemachine @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "package": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "p": ["--package"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var opts = {}; + if (parsed['package']) + opts['package'] = parsed['package']; + + + var client = common.newClient(parsed); + return client.resizeMachine(parsed.argv.remain[0], opts, common.callback); + +}, usageStr); diff --git a/bin/sdc-setup b/bin/sdc-setup new file mode 100755 index 0000000..74892bd --- /dev/null +++ b/bin/sdc-setup @@ -0,0 +1,194 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var nopt = require('nopt'); +var restify = require('restify'); + +var common = require('../lib/cli_common'); +var CloudAPI = require('../lib/index').CloudAPI; + + + +///--- Globals + +var Options = { + "debug": Boolean, + "help": Boolean +}; + +var ShortOptions = { + "d": ["--debug"], + "h": ["--help"] +}; + +var buffer = ''; +var log = restify.log; +var stdio = process.binding('stdio'); + +var usageStr = common.buildUsageString(Options); +usageStr += ' url'; + + +///--- Internal Functions + +// Totally ripped off from npm. Thanks isaacs@! + +function read(def, cb) { + var stdin = process.openStdin(); + var val = ''; + stdin.resume(); + stdin.setEncoding('utf8'); + stdin.on('error', cb) + stdin.on('data', function D (chunk) { + val += buffer + chunk; + buffer = ''; + val = val.replace(/\r/g, ''); + if (val.indexOf('\n') !== -1) { + if (val !== '\n') + val = val.replace(/^\n+/, ''); + buffer = val.substr(val.indexOf('\n')); + val = val.substr(0, val.indexOf('\n')); + stdin.pause(); + stdin.removeListener('data', D); + stdin.removeListener('error', cb); + val = val.trim() || def; + cb(null, val); + } + }); +} + + +function silentRead(def, cb) { + var stdin = process.openStdin(); + var val = ''; + stdio.setRawMode(true); + stdin.resume(); + stdin.on('error', cb) + stdin.on('data', function D (c) { + c = '' + c + switch (c) { + case '\n': + case '\r': + case '\r\n': + case '\u0004': + stdio.setRawMode(false); + stdin.removeListener('data', D); + stdin.removeListener('error', cb); + val = val.trim() || def; + process.stdout.write('\n'); + process.stdout.flush(); + stdin.pause(); + return cb(null, val); + case '\u0003': + case '\0': + return cb('cancelled'); + break; + default: + val += buffer + c; + buffer = ''; + break; + } + }); +} + + +function prompt(p, def, silent, cb) { + if (!cb) cb = silent, silent = false; + if (!cb) cb = def, def = undefined; + if (def) p += '('+ (silent ? '' : def)+') '; + var r = (silent ? silentRead : read).bind(null, def, cb); + if (!process.stdout.write(p)) { + process.stdout.on('drain', function D () { + process.stdout.removeListener('drain', D); + r() + }); + } else { + r(); + } +} + + + +///--- Mainline + +var parsed = nopt(Options, ShortOptions, process.argv, 2); +if (parsed.help) + common.usage(usageStr); +if (parsed.debug) + restify.log.level(restify.LogLevel.Trace); +if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'url required'); +var url = parsed.argv.remain[0]; + + +prompt('Username (login): ', process.env.USER, function(err, username) { + if (err) { + console.error('Unable to read username: ' + err.message); + process.exit(5); + } + + return prompt('Password: ', null, true, function(err, password) { + if (err) { + console.error('Unable to read password: ' + err.message); + process.exit(5); + } + + if (!password || !password.length) { + console.error('You did not enter a password.'); + process.exit(6); + } + + var DEF_KEY = process.env.HOME + '/.ssh/id_rsa.pub'; + return prompt('SSH public key: ', DEF_KEY, function(err, file) { + if (err) { + console.error('Unable to read key: ' + err.message); + process.exit(5); + } + log.debug('setup: user=%s, pass=%s, key=%s', username, password, file); + var key = common.loadKey(file); + var client = new CloudAPI({ + url: url, + account: username, + noCache: true, + username: username, + password: password + }); + + // So that SSH agent support automagically works (if using the CLI), + // pick the name to be the same as what SSH Agent will pass in the + // comment field (well, the file name, not the full path). + var keyName = file.split('/'); + var opts = { + name: keyName[keyName.length - 1].split('.')[0], + key: key + } + client.createKey(opts, function(err, key) { + if (err) { + if (err.httpCode >= 500) { + if (err.details && err.details.body) { + try { + console.error(JSON.parse(err.details.body).message); + } catch(e) { + console.error(err.message); + } + } + } else { + console.error(err.message); + } + process.exit(3); + } + + console.log('\n\nIf you set these environment variables, your life will be easier:'); + console.log('export SDC_CLI_URL=' + url); + console.log('export SDC_CLI_ACCOUNT=' + username); + console.log('export SDC_CLI_KEY_ID=' + key.name); + }); + }); + }); +}); + diff --git a/bin/sdc-startmachine b/bin/sdc-startmachine new file mode 100755 index 0000000..75cbf2d --- /dev/null +++ b/bin/sdc-startmachine @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.startMachine(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/bin/sdc-startmachinefromsnapshot b/bin/sdc-startmachinefromsnapshot new file mode 100755 index 0000000..4cf78e3 --- /dev/null +++ b/bin/sdc-startmachinefromsnapshot @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "snapshot": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "n": ["--snapshot"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (!parsed.snapshot) + common.usage(usageStr, 1, 'snapshot required'); + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.startMachineFromSnapshot(parsed.argv.remain[0], + parsed.snapshot, + common.callback); +}, usageStr); diff --git a/bin/sdc-stopmachine b/bin/sdc-stopmachine new file mode 100755 index 0000000..16b1c91 --- /dev/null +++ b/bin/sdc-stopmachine @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// -*- mode: js -*- +// Copyright 2011 Joyent, Inc. All rights reserved. + +var path = require('path'); +var url = require('url'); + +var common = require('../lib/cli_common'); + + + +///--- Globals + +var Options = { + "account": String, + "debug": Boolean, + "help": Boolean, + "identity": path, + "keyId": String, + "url": url +}; + +var ShortOptions = { + "a": ["--account"], + "d": ["--debug"], + "h": ["--help"], + "?": ["--help"], + "i": ["--identity"], + "k": ["--keyId"], + "u": ["--url"] +}; + +var usageStr = common.buildUsageString(Options); +usageStr += ' machine'; + + +///--- Mainline + +common.parseArguments(Options, ShortOptions, function(parsed) { + if (parsed.argv.remain.length < 1) + common.usage(usageStr, 1, 'machine required'); + + var client = common.newClient(parsed); + client.stopMachine(parsed.argv.remain[0], common.callback); +}, usageStr); diff --git a/lib/cli_common.js b/lib/cli_common.js new file mode 100644 index 0000000..20974a9 --- /dev/null +++ b/lib/cli_common.js @@ -0,0 +1,227 @@ +// Copyright 2011 Joyent, Inc. All rights reserved. + +var assert = require('assert'); +var fs = require('fs'); +var path = require('path'); +var url = require('url'); + +var nopt = require('nopt'); +var restify = require('restify'); +var SSHAgentClient = require('ssh-agent'); + +var CloudAPI = require('../lib/index').CloudAPI; + + +path.name = 'path'; +url.name = 'url'; + + + +///--- Globals + +var log = restify.log; + + +///--- Internal Functions + +function usage(str, code, message) { + assert.ok(str); + + var writer = console.log; + if (code) + writer = console.error; + + if (message) + writer(message); + writer(path.basename(process.argv[1]) + ' ' + str); + process.exit(code || 0); +} + + +function buildUsageString(options) { + assert.ok(options); + + var str = ''; + for (var k in options) { + if (options.hasOwnProperty(k)) { + var o = options[k].name ? options[k].name.toLowerCase() : ''; + str += '[--' + k + ' ' + o + '] '; + } + } + return str; +} + + +function loadKeyFromAgent(parsed, callback) { + assert.ok(parsed); + assert.ok(callback); + + try { + var agent = new SSHAgentClient(); + agent.requestIdentities(function(err, keys) { + if (err || !keys || !keys.length) { + log.debug('No ssh-agent identities found'); + return callback(null); + } + + var path = parsed.identity.split('/'); + for (var i = 0; i < keys.length; i++) { + if (keys[i].type !== 'ssh-rsa') + continue; + + var comment = keys[i].comment.split('/'); + if (path[path.length - 1] === comment[comment.length - 1]) { + log.debug('Using ssh-agent identity: ' + keys[i].comment); + parsed.signingKey = keys[i]; + parsed.sshAgent = agent; + return callback(parsed); + } + + } + + log.debug('No ssh-agent identity suitable: %o', keys); + return callback(null); + }); + } catch (e) { + log.debug('Unable to load ssh-agent identities: ' + e); + return callback(null); + } +} + + +function loadSigningKey(parsed, callback) { + assert.ok(parsed); + assert.ok(callback); + + fs.readFile(parsed.identity, 'ascii', function(err, file) { + if (err) { + console.error(err.message); + process.exit(2); + } + parsed.signingKey = file; + return callback(parsed); + }); +} + + + +///--- Exported API + +module.exports = { + + /** + * Common callback for all CLI operations. + * + * @param {Error} err optional error object. + * @param {Object} obj optional response object. + */ + callback: function(err, obj) { + if (err) { + console.error(err.message); + process.exit(3); + } + + if (obj) + console.log(JSON.stringify(obj, null, 2)); + }, + + + usage: usage, + + + buildUsageString: buildUsageString, + + + parseArguments: function(options, shortOptions, callback, usageStr) { + assert.ok(options); + assert.ok(shortOptions); + assert.ok(callback); + + if (!usageStr) + usageStr = buildUsageString(options); + + var parsed = nopt(options, shortOptions, process.argv, 2); + if (parsed.help) + usage(usageStr); + + if (parsed.debug) + restify.log.level(restify.LogLevel.Trace); + + if (!parsed.identity) + parsed.identity = process.env.HOME + '/.ssh/id_rsa'; + + if (!parsed.keyId) { + if (process.env.SDC_CLI_KEY_ID) { + parsed.keyId = process.env.SDC_CLI_KEY_ID; + } else { + parsed.keyId = 'id_rsa'; + } + } + + if (!parsed.account) + parsed.account = process.env.SDC_CLI_ACCOUNT; + if (!parsed.account) { + usage(usageStr, 1, + 'Either -a or (env) SDC_CLI_ACCOUNT must be specified'); + } + + if (!parsed.url) + parsed.url = process.env.SDC_CLI_URL; + if (!parsed.url) { + usage(usageStr, 1, + 'Either -a or (env) SDC_CLI_URL must be specified'); + } + + + return loadKeyFromAgent(parsed, function(_parsed) { + + if (_parsed) { + log.debug('Found private key in SSH-Agent: %s', parsed.keyId); + return callback(_parsed); + } + + return loadSigningKey(parsed, function(_parsed) { + if (!_parsed) { + console.error('Unable to load a private key for signing (not found)'); + process.exit(2); + } + + log.debug('Using private key from: %s', parsed.identity); + return callback(_parsed); + }); + }); + }, + + + newClient: function(parsed) { + assert.ok(parsed); + assert.ok(parsed.keyId); + assert.ok(parsed.signingKey); + + try { + return new CloudAPI({ + url: parsed.url, + account: parsed.account, + noCache: true, + logLevel: restify.log.level(), + key: parsed.signingKey, + keyId: '/' + parsed.account + '/keys/' + parsed.keyId, + sshAgent: parsed.sshAgent + }); + } catch (e) { + console.error(e.message); + process.exit(1); + } + }, + + + loadKey: function(key) { + try { + return fs.readFileSync(key, 'ascii'); + } catch (e) { + console.error('Unable to load key ' + key + ': ' + e); + process.exit(2); + } + } + +}; diff --git a/lib/cloudapi.js b/lib/cloudapi.js new file mode 100644 index 0000000..bc9b08f --- /dev/null +++ b/lib/cloudapi.js @@ -0,0 +1,1841 @@ +// Copyright 2011 Joyent, Inc. All rights reserved. + +var assert = require('assert'); +var crypto = require('crypto'); + +var createCache = require('lru-cache'); +var restify = require('restify'); +var sprintf = require('sprintf').sprintf; + +var utils = require('./utils'); + + + +///--- Globals + +var date = restify.httpDate; +var log = restify.log; +var RestCodes = restify.RestCodes; + +var SIGNATURE = 'Signature keyId="%s",algorithm="%s" %s'; + +var ROOT = '/%s'; +var KEYS = ROOT + '/keys'; +var KEY = KEYS + '/%s'; +var PACKAGES = ROOT + '/packages'; +var PACKAGE = PACKAGES + '/%s'; +var DATASETS = ROOT + '/datasets'; +var DATASET = DATASETS + '/%s'; +var DATACENTERS = ROOT + '/datacenters'; +var MACHINES = ROOT + '/machines'; +var MACHINE = MACHINES + '/%s'; +var SNAPSHOTS = MACHINE + '/snapshots'; +var SNAPSHOT = SNAPSHOTS + '/%s'; +var TAGS = MACHINE + '/tags'; +var TAG = TAGS + '/%s'; +var ANALYTICS = ROOT + '/analytics'; +var INSTS = ANALYTICS + '/instrumentations'; +var INST = INSTS + '/%s'; +var INST_RAW = INST + '/value/raw'; +var INST_HMAP = INST + '/value/heatmap/image'; +var INST_HMAP_DETAILS = INST + '/value/heatmap/details'; + + + +///--- Internal Helpers + +function _clone(object) { + assert.ok(object); + + var clone = {}; + + var keys = Object.getOwnPropertyNames(object); + keys.forEach(function(k) { + var property = Object.getOwnPropertyDescriptor(object, k); + Object.defineProperty(clone, k, property); + }); + + return clone; +} + + +///--- Exported CloudAPI Client + +/** + * Constructor. + * + * Note that in options you can pass in any parameters that the restify + * RestClient constructor takes (for example retry/backoff settings). + * + * In order to create a client, you either have to specify username and + * password, in which case HTTP Basic Authentication will be used, or + * preferably keyId and key, in which case HTTP Signature Authentication will + * be used (much more secure). + * + * @param {Object} options object (required): + * - {String} url (required) CloudAPI location. + * - {String} account (optional) the login name to use (default my). + * - {Number} logLevel (optional) an enum value for the logging level. + * - {String} version (optional) api version (default 6.1.0). + * - {String} username (optional) login name. + * - {String} password (optional) login password. + * - {String} keyId (optional) SSH key id in cloudapi to sign with. + * - {String} key (optional) SSH key (PEM) that goes with `keyId`. + * - {Boolean} noCache (optional) disable client caching (default false). + * - {Boolean} cacheSize (optional) number of cache entries (default 1k). + * - {Boolean} cacheExpiry (optional) entry age in seconds (default 60). + * @throws {TypeError} on bad input. + * @constructor + */ +function CloudAPI(options) { + if (!options) throw new TypeError('options required'); + if (!options.url) throw new TypeError('options.url required'); + if (!(options.username && options.password) && + !(options.keyId && options.key)) + throw new TypeError('Either username/password or keyId/key are required'); + + if (options.logLevel) + log.level(options.logLevel); + if (!options.version) + options.version = '6.1.0'; + this.account = options.account || 'my'; + + options.contentType = 'application/json'; + + this.client = restify.createClient(options); + + this.options = _clone(options); + + // Try to use RSA Signing over BasicAuth + if (options.key) { + this.keyId = options.keyId; + this.key = options.key; + this.sshAgent = options.sshAgent; + } else { + this.basicAuth = utils.basicAuth(options.username, options.password); + } + + // Initialize the cache + if (!options.noCache) { + this.cacheSize = options.cacheSize || 1000; + this.cacheExpiry = (options.cacheExpiry || 60) * 1000; + this.cache = createCache(this.cacheSize); + } + + // Secret ENV var to not provision (testing) + if (process.env.SDC_TESTING) { + log.warn('SDC_TESTING env var set: provisioning will *not* happen'); + this.__no_op = true; + } +} + + +/** + * Looks up your account record. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, account). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getAccount = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(ROOT, account), null, function(req) { + return self._get(req, callback, noCache); + }); + +}; +CloudAPI.prototype.GetAccount = CloudAPI.prototype.getAccount; + + +/** + * Creates an SSH key on your account. + * + * Returns a JS object (the created key). Note that options can actually + * be just the key PEM, if you don't care about names. + * + * @param {String} account (optional) the login name of the account. + * @param {Object} options object containing: + * - {String} name (optional) name for your ssh key. + * - {String} key SSH public key. + * @param {Function} callback of the form f(err, key). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.createKey = function(account, options, callback) { + if (typeof(options) === 'function') { + callback = options; + options = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (!options || + (typeof(options) !== 'string' && typeof(options) !== 'object')) + throw new TypeError('options (object) required'); + if (typeof(account) === 'object') + account = account.login; + + if (typeof(options) === 'string') { + options = { + key: options + }; + } + + var self = this; + return this._request(sprintf(KEYS, account), options, function(req) { + return self._post(req, callback); + }); +}; +CloudAPI.prototype.CreateKey = CloudAPI.prototype.createKey; + + +/** + * Lists all SSH keys on file for your account. + * + * Returns an array of objects. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, keys). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listKeys = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + this._request(sprintf(KEYS, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.ListKeys = CloudAPI.prototype.listKeys; + + +/** + * Retrieves an SSH key from your account. + * + * Returns a JS object. + * + * @param {String} account (optional) the login name of the account. + * @param {String} key can be either the string name of the key, or the object + * returned from create/get. + * @param {Function} callback of the form f(err, key). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getKey = function(account, key, callback, noCache) { + if (typeof(key) === 'function') { + callback = key; + key = account; + account = this.account; + } + if (!key || (typeof(key) !== 'object' && typeof(key) !== 'string')) + throw new TypeError('key (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(key) === 'object' ? key.name : key); + + var self = this; + this._request(sprintf(KEY, account, name), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.GetKey = CloudAPI.prototype.getKey; + + +/** + * Deletes an SSH key from your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} key can be either the string name of the key, or the object + * returned from create/get. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.deleteKey = function(account, key, callback) { + if (typeof(key) === 'function') { + callback = key; + key = account; + account = this.account; + } + + if (!key || (typeof(key) !== 'object' && typeof(key) !== 'string')) + throw new TypeError('key (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(key) === 'object' ? key.name : key); + + var self = this; + return this._request(sprintf(KEY, account, name), null, function(req) { + return self._del(req, callback); + }); +}; +CloudAPI.prototype.DeleteKey = CloudAPI.prototype.deleteKey; + + +/** + * Lists all packages available to your account. + * + * Returns an array of objects. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, packages). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listPackages = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(PACKAGES, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.ListPackages = CloudAPI.prototype.listPackages; + + +/** + * Retrieves a single package available to your account. + * + * Returns a JS object. + * + * @param {String} account (optional) the login name of the account. + * @param {String} pkg can be either the string name of the package, or an + * object returned from listPackages. + * @param {Function} callback of the form f(err, package). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getPackage = function(account, pkg, callback, noCache) { + if (typeof(pkg) === 'function') { + callback = pkg; + pkg = account; + account = this.account; + } + if (!pkg || (typeof(pkg) !== 'object' && typeof(pkg) !== 'string')) + throw new TypeError('key (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(pkg) === 'object' ? pkg.name : pkg); + + var self = this; + return this._request(sprintf(PACKAGE, account, name), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.GetPackage = CloudAPI.prototype.getPackage; + + +/** + * Lists all datasets available to your account. + * + * Returns an array of objects. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, datasets). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listDatasets = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(DATASETS, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.ListDatasets = CloudAPI.prototype.listDatasets; + + +/** + * Retrieves a single dataset available to your account. + * + * Returns a JS object. + * + * @param {String} account (optional) the login name of the account. + * @param {String} dataset can be either the string name of the dataset, or an + * object returned from listDatasets. + * @param {Function} callback of the form f(err, package). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getDataset = function(account, dataset, callback, noCache) { + if (typeof(dataset) === 'function') { + callback = dataset; + dataset = account; + account = this.account; + } + if (!dataset || + (typeof(dataset) !== 'object' && typeof(dataset) !== 'string')) + throw new TypeError('dataset (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(dataset) === 'object' ? dataset.id : dataset); + + var self = this; + return this._request(sprintf(DATASET, account, name), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.GetDataset = CloudAPI.prototype.getDataset; + + +/** + * Lists all datacenters available to your account. + * + * Returns an array of objects. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, datacenters). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listDatacenters = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + this._request(sprintf(DATACENTERS, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.ListDatacenters = CloudAPI.prototype.listDatacenters; + + +/** + * Creates a new CloudAPI client connected to the specified datacenter. + * + * Returns a JS object. + * + * @param {String} account (optional) the login name of the account. + * @param {String} datacenter can be either the string name of the datacenter, + * or an object returned from listDatacenters. + * @param {Function} callback of the form f(err, package). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.createClientForDatacenter = + function(account, datacenter, callback, noCache) { + if (typeof(datacenter) === 'function') { + callback = datacenter; + datacenter = account; + account = this.account; + } + if (typeof(datacenter) !== 'string') + throw new TypeError('datacenter (string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this.listDatacenters(account, function(err, datacenters) { + if (err) + return callback(self._error(err)); + + if (!datacenters[datacenter]) { + var e = new Error(); + e.name = 'CloudApiError'; + e.code = RestCodes.ResourceNotFound; + e.message = 'datacenter ' + datacenter + ' not found'; + return callback(e); + } + + var opts = _clone(self.options); + opts.url = datacenters[datacenter]; + return callback(null, new CloudAPI(opts)); + }); + }; +CloudAPI.prototype.CreateClientForDatacenter = + CloudAPI.prototype.createClientForDatacenter; + + +/** + * Provisions a new smartmachine or virtualmachine. + * + * Returns a JS object (the created machine). Note that the options + * object parameters like dataset/package can actually be the JS objects + * returned from the respective APIs. + * + * @param {String} account (optional) the login name of the account. + * @param {Object} options (optional) object containing: + * - {String} name (optional) name for your machine. + * - {String} dataset (optional) dataset to provision. + * - {String} package (optional) package to provision. + * @param {Function} callback of the form f(err, machine). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.createMachine = function(account, options, callback) { + if (typeof(account) === 'function') { + callback = account; + options = {}; + account = this.account; + } + if (typeof(options) === 'function') { + callback = options; + options = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (options.name && typeof(options.name) !== 'string') + throw new TypeError('options.name must be a string'); + if (typeof(account) === 'object') + account = account.login; + + if (options.dataset) { + switch (typeof(options.dataset)) { + case 'string': + // noop + break; + case 'object': + options.dataset = options.dataset.id; + break; + default: + throw new TypeError('options.dataset must be a string or object'); + } + } + + if (options['package']) { + switch (typeof(options['package'])) { + case 'string': + // noop + break; + case 'object': + options['package'] = options['package'].id; + break; + default: + throw new TypeError('options.package must be a string or object'); + } + } + + // Undocumented flag to skip the actual call (testing only) + if (this.__no_op) + return callback(null, {}); + + var self = this; + return this._request(sprintf(MACHINES, account), options, function(req) { + return self._post(req, callback); + }); +}; +CloudAPI.prototype.CreateMachine = CloudAPI.prototype.createMachine; + + +/** + * Counts all machines running under your account. + * + * This API call takes all the same options as ListMachines. However, + * instead of returning a set of machine objects, it returns the count + * of machines that would be returned. + * + * achine listings are both potentially large and + * volatile, so this API explicitly does no caching. + * + * Returns an integer, and a boolean that indicates whether there + * are more records (i.e., you got paginated). If there are, call this + * again with offset=count. + * + * @param {String} account (optional) the login name of the account. + * @param {Object} options (optional) sets filtration/pagination: + * - {String} name (optional) machines with this name. + * - {String} dataset (optional) machines with this dataset. + * - {String} package (optional) machines with this package. + * - {String} type (optional) smartmachine or virtualmachine. + * - {String} state (optional) machines in this state. + * - {Number} memory (optional) machines with this memory. + * - {Number} offset (optional) pagination starting point. + * - {Number} limit (optional) cap on the number to return. + * @param {Function} callback of the form f(err, machines, moreRecords). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.countMachines = function(account, options, callback) { + if (typeof(account) === 'function') { + callback = account; + options = {}; + account = this.account; + } + if (typeof(options) === 'function') { + callback = options; + options = account; + account = this.account; + } + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(MACHINES, account), null, function(req) { + req.query = options; + req.cacheTTL = (15 * 1000); + return self.client.head(req, function(err, headers) { + if (err) + return callback(self._error(err)); + + var done = true; + if (headers['x-resource-count'] && headers['x-query-limit']) + done = (headers['x-resource-count'] < headers['x-query-limit']); + + var count = +headers['x-resource-count']; + + log.debug('CloudAPI._head(%s) -> err=%o, count=%d, done=%s', + req.path, err, count, done); + return callback(err, count, done); + }); + }); +}; +CloudAPI.prototype.CountMachines = CloudAPI.prototype.countMachines; + + +/** + * Lists all machines running under your account. + * + * This API call does a 'deep list', so you shouldn't need to go + * back over the wan on each id. Also, note that this API supports + * filters and pagination; use the options object. If you don't set + * them you'll get whatever the server has set for pagination/limits. + * + * Also, note that machine listings are both potentially large and + * volatile, so this API explicitly does no caching. + * + * Returns an array of objects, and a boolean that indicates whether there + * are more records (i.e., you got paginated). If there are, call this + * again with offset=machines.length. + * + * @param {String} account (optional) the login name of the account. + * @param {Object} options (optional) sets filtration/pagination: + * - {String} name (optional) machines with this name. + * - {String} dataset (optional) machines with this dataset. + * - {String} package (optional) machines with this package. + * - {String} type (optional) smartmachine or virtualmachine. + * - {String} state (optional) machines in this state. + * - {Number} memory (optional) machines with this memory. + * - {Number} offset (optional) pagination starting point. + * - {Number} limit (optional) cap on the number to return. + * @param {Object} tags (optional) k/v hash of tags. + * @param {Function} callback of the form f(err, machines, moreRecords). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listMachines = function(account, options, tags, callback) { + if (typeof(account) === 'function') { + callback = account; + tags = {}; + options = {}; + account = this.account; + } + if (typeof(options) === 'function') { + callback = options; + tags = {}; + options = account; + account = this.account; + } + if (typeof(tags) === 'function') { + callback = tags; + if (typeof(account) === 'object') { + tags = options; + options = account; + account = this.account; + } else { + tags = {}; + options = account; + account = this.account; + } + } + if (typeof(options) !== 'object') + throw new TypeError('options must be an object'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + for (var k in tags) { + if (tags.hasOwnProperty(k)) { + options['tag.' + k] = tags[k]; + } + } + + var self = this; + return this._request(sprintf(MACHINES, account), null, function(req) { + req.query = options; + return self.client.get(req, function(err, obj, headers) { + if (err) + return callback(self._error(err)); + + var done = true; + if (headers['x-resource-count'] && headers['x-query-limit']) + done = (headers['x-resource-count'] < headers['x-query-limit']); + + log.debug('CloudAPI._get(%s) -> err=%o, obj=%o, done=%s', + req.path, err, obj, done); + return callback(err, obj, done); + }); + }); +}; +CloudAPI.prototype.ListMachines = CloudAPI.prototype.listMachines; + + +/** + * Gets a single machine under your account. + * + * Also, note that machine listings are fairly volatile, so this API + * explicitly sets the cache TTL to 15s. You can bypass caching altogether + * with the `noCache` param. + * + * Returns a JS object. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err, machine). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getMachine = function(account, machine, callback, noCache) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(MACHINE, account, name), null, function(req) { + req.cacheTTL = (15 * 1000); + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.GetMachine = CloudAPI.prototype.getMachine; + + +/** + * Reboots a machine under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.rebootMachine = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + return this._updateMachine(account, machine, 'reboot', callback); +}; +CloudAPI.prototype.RebootMachine = CloudAPI.prototype.rebootMachine; + + +/** + * Shuts down a machine under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.stopMachine = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + return this._updateMachine(account, machine, 'stop', callback); +}; +CloudAPI.prototype.StopMachine = CloudAPI.prototype.stopMachine; + + +/** + * Boots up a machine under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.startMachine = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + return this._updateMachine(account, machine, 'start', callback); +}; +CloudAPI.prototype.StartMachine = CloudAPI.prototype.startMachine; + + +/** + * Resizes a machine under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.resizeMachine = function(account, + machine, + options, + callback) { + if (typeof(options) === 'function') { + callback = options; + options = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + return this._updateMachine(account, machine, 'resize', options, callback); +}; +CloudAPI.prototype.ResizeMachine = CloudAPI.prototype.resizeMachine; + + +/** + * Deletes a machine under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.deleteMachine = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(machine) === 'object' ? machine.id : machine); + var self = this; + return this._request(sprintf(MACHINE, account, name), null, function(req) { + return self._del(req, callback); + }); +}; +CloudAPI.prototype.DeleteMachine = CloudAPI.prototype.deleteMachine; + + +/** + * Creates a new snapshots for a given machine. + * + * Note that the machine must be a smartmachine for snapshots to work. + * This API explicitly disables caching. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.createMachineSnapshot = function(account, + machine, + options, + callback) { + if (typeof(options) === 'function') { + callback = options; + options = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(SNAPSHOTS, account, m), options, function(req) { + return self._post(req, callback); + }); +}; +CloudAPI.prototype.CreateMachineSnapshot = + CloudAPI.prototype.createMachineSnapshot; + + +/** + * Lists all snapshots for a given machine. + * + * Note that the machine must be a smartmachine for snapshots to work. + * This API explicitly disables caching. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listMachineSnapshots = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(SNAPSHOTS, account, m), null, function(req) { + return self._get(req, callback, true); + }); +}; +CloudAPI.prototype.ListMachineSnapshots = + CloudAPI.prototype.listMachineSnapshots; + + +/** + * Gets a single snapshot for a given machine. + * + * Note that the machine must be a smartmachine for snapshots to work. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {String} snapshot either the name, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @param {Boolean} noCache disable caching of this result. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getMachineSnapshot = function(account, + machine, + snapshot, + callback, + noCache) { + if (typeof(snapshot) === 'function') { + callback = snapshot; + snapshot = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!snapshot || + (typeof(snapshot) !== 'object' && typeof(snapshot) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var a = (typeof(account) === 'object' ? account.login : account); + var m = (typeof(machine) === 'object' ? machine.id : machine); + var s = (typeof(snapshot) === 'object' ? snapshot.name : snapshot); + + var self = this; + return this._request(sprintf(SNAPSHOT, a, m, s), null, function(req) { + return self._get(req, callback, noCache); + }); + +}; +CloudAPI.prototype.GetMachineSnapshot = CloudAPI.prototype.getMachineSnapshot; + + +/** + * Boots a machine from a snapshot. + * + * Note that the machine must be a smartmachine for snapshots to work. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {String} snapshot either the name, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.startMachineFromSnapshot = function(account, + machine, + snapshot, + callback) { + + if (typeof(snapshot) === 'function') { + callback = snapshot; + snapshot = machine; + machine = account; + account = this.account; + } + + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!snapshot || + (typeof(snapshot) !== 'object' && typeof(snapshot) !== 'string')) + throw new TypeError('snapshot (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + + var a = (typeof(account) === 'object' ? account.login : account); + var m = (typeof(machine) === 'object' ? machine.id : machine); + var s = (typeof(snapshot) === 'object' ? snapshot.name : snapshot); + + var self = this; + return this._request(sprintf(SNAPSHOT, a, m, s), null, function(req) { + req.expect = 202; + return self._post(req, callback); + }); + +}; +CloudAPI.prototype.StartMachineFromSnapshot = + CloudAPI.prototype.startMachineFromSnapshot; + + +/** + * Deletes a machine snapshot. + * + * Note that the machine must be a smartmachine for snapshots to work. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {String} snapshot either the name, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.deleteMachineSnapshot = function(account, + machine, + snapshot, + callback) { + if (typeof(snapshot) === 'function') { + callback = snapshot; + snapshot = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!snapshot || + (typeof(snapshot) !== 'object' && typeof(snapshot) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var a = (typeof(account) === 'object' ? account.login : account); + var m = (typeof(machine) === 'object' ? machine.id : machine); + var s = (typeof(snapshot) === 'object' ? snapshot.name : snapshot); + + var self = this; + return this._request(sprintf(SNAPSHOT, a, m, s), null, function(req) { + return self._del(req, callback); + }); + +}; +CloudAPI.prototype.DeleteMachineSnapshot = + CloudAPI.prototype.deleteMachineSnapshot; + + + +/** + * Adds the set of tags to the machine. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Object} tags tags dictionary. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.addMachineTags = function(account, machine, tags, callback) { + if (typeof(tags) === 'function') { + callback = tags; + tags = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!tags || typeof(tags) !== 'object') + throw new TypeError('tags (object) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(TAGS, account, m), tags, function(req) { + return self._post(req, callback); + }); +}; +CloudAPI.prototype.AddMachineTags = CloudAPI.prototype.addMachineTags; + + +/** + * Gets the set of tags from a machine + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listMachineTags = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(TAGS, account, m), null, function(req) { + return self._get(req, callback); + }); +}; +CloudAPI.prototype.ListMachineTags = CloudAPI.prototype.listMachineTags; + + +/** + * Retrieves a single tag from a machine. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {String} tag a tag name to get. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getMachineTag = function(account, machine, tag, callback) { + if (typeof(tag) === 'function') { + callback = tag; + tag = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!tag || typeof(tag) !== 'string') + throw new TypeError('tag (string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(TAG, account, m, tag), null, function(req) { + req.headers.Accept = 'text/plain'; + return self._get(req, callback); + }); +}; +CloudAPI.prototype.GetMachineTag = CloudAPI.prototype.getMachineTag; + + +/** + * Deletes ALL tags from a machine + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.deleteMachineTags = function(account, machine, callback) { + if (typeof(machine) === 'function') { + callback = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(TAGS, account, m), null, function(req) { + return self._del(req, callback); + }); +}; +CloudAPI.prototype.DeleteMachineTags = CloudAPI.prototype.deleteMachineTags; + + +/** + * Deletes a single tag from a machine. + * + * @param {String} account (optional) the login name of the account. + * @param {String} machine either the id, or can be the object returned in list + * or create. + * @param {String} tag a tag name to purge. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.deleteMachineTag = function(account, + machine, + tag, + callback) { + if (typeof(tag) === 'function') { + callback = tag; + tag = machine; + machine = account; + account = this.account; + } + if (!machine || + (typeof(machine) !== 'object' && typeof(machine) !== 'string')) + throw new TypeError('machine (object|string) required'); + if (!tag || typeof(tag) !== 'string') + throw new TypeError('tag (string) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var m = (typeof(machine) === 'object' ? machine.id : machine); + + var self = this; + return this._request(sprintf(TAG, account, m, tag), null, function(req) { + return self._del(req, callback); + }); +}; +CloudAPI.prototype.DeleteMachineTag = CloudAPI.prototype.deleteMachineTag; + + +/** + * Dumps the "metrics" used in all requets to /analytics. + * + * Returns a big object. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, metrics). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.describeAnalytics = function(account, callback, noCache) { + if (typeof(account) === 'function') { + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(ANALYTICS, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.DescribeAnalytics = CloudAPI.prototype.describeAnalytics; +CloudAPI.prototype.getMetrics = CloudAPI.prototype.describeAnalytics; +CloudAPI.prototype.GetMetrics = CloudAPI.prototype.describeAnalytics; + + +/** + * Creates an instrumentation under your account. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Object} options instrumentation options. (see CA docs). + * @param {Function} callback of the form f(err, instrumentation). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.createInst = function(account, options, callback, noCache) { + if (typeof(options) === 'function') { + callback = options; + options = account; + account = this.account; + } + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(INSTS, account), options, function(req) { + return self._post(req, callback); + }); +}; +CloudAPI.prototype.createInstrumentation = CloudAPI.prototype.createInst; +CloudAPI.prototype.CreateInstrumentation = CloudAPI.prototype.createInst; + + +/** + * Lists instrumentations under your account. + * + * Returns an array of objects. + * + * @param {String} account (optional) the login name of the account. + * @param {Function} callback of the form f(err, schema). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.listInsts = function(account, callback, noCache) { + if (typeof(account) === 'function') { + noCache = callback; + callback = account; + account = this.account; + } + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var self = this; + return this._request(sprintf(INSTS, account), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.listInstrumentations = CloudAPI.prototype.listInsts; +CloudAPI.prototype.ListInstrumentations = CloudAPI.prototype.listInsts; + + +/** + * Gets an instrumentation under your account. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Number} inst either the id, or can be the object returned + * in list or create. + * @param {Function} callback of the form f(err, instrumentation). + * @param {Boolean} noCache optional flag to force skipping the cache. + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getInst = function(account, inst, callback, noCache) { + if (typeof(inst) === 'function') { + noCache = callback; + callback = inst; + inst = account; + account = this.account; + } + + if (!inst || (typeof(inst) !== 'object' && typeof(inst) !== 'number')) + throw new TypeError('inst (object|number) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(inst) === 'object' ? inst.id : inst); + var self = this; + return this._request(sprintf(INST, account, name), null, function(req) { + return self._get(req, callback, noCache); + }); +}; +CloudAPI.prototype.getInstrumentation = CloudAPI.prototype.getInst; +CloudAPI.prototype.GetInstrumentation = CloudAPI.prototype.getInst; + + +/** + * Gets an instrumentation raw value under your account. + * + * This call is not cachable. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Number} inst either the id, or can be the object returned + * in list or create. + * @param {Function} callback of the form f(err, instrumentation). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getInstValue = function(account, inst, callback) { + if (typeof(inst) === 'function') { + callback = inst; + inst = account; + account = this.account; + } + if (!inst || (typeof(inst) !== 'object' && typeof(inst) !== 'number')) + throw new TypeError('inst (object|number) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(inst) === 'object' ? inst.id : inst); + var self = this; + return this._request(sprintf(INST_RAW, account, name), null, function(req) { + return self._get(req, callback, true); + }); +}; +CloudAPI.prototype.getInstrumentationValue = CloudAPI.prototype.getInstValue; +CloudAPI.prototype.GetInstrumentationValue = CloudAPI.prototype.getInstValue; + + +/** + * Gets an instrumentation heatmap image under your account. + * + * This call is not cachable. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Number} inst either the id, or can be the object returned + * in list or create. + * @param {Function} callback of the form f(err, instrumentation). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getInstHmap = function(account, inst, callback) { + if (typeof(inst) === 'function') { + callback = inst; + inst = account; + account = this.account; + } + if (!inst || (typeof(inst) !== 'object' && typeof(inst) !== 'number')) + throw new TypeError('inst (object|number) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(inst) === 'object' ? inst.id : inst); + var self = this; + this._request(sprintf(INST_HMAP, account, name), null, function(req) { + return self._get(req, callback, true); + }); +}; +CloudAPI.prototype.getInstrumentationHeatmap = CloudAPI.prototype.getInstHmap; +CloudAPI.prototype.GetInstrumentationHeatmap = CloudAPI.prototype.getInstHmap; + + +/** + * Gets an instrumentation heatmap image details. + * + * This call is not cachable. + * + * Returns an object. + * + * @param {String} account (optional) the login name of the account. + * @param {Number} inst either the id, or can be the object returned + * in list or create. + * @param {Object} options with x and y, as {Number}. Required. + * @param {Function} callback of the form f(err, instrumentation). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.getInstHmapDetails = function(account, + inst, + options, + callback) { + if (typeof(options) === 'function') { + callback = options; + options = inst; + inst = account; + account = this.account; + } + if (!inst || (typeof(inst) !== 'object' && typeof(inst) !== 'number')) + throw new TypeError('inst (object|number) required'); + if (!options || typeof(options) !== 'object') + throw new TypeError('options (object) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(inst) === 'object' ? inst.id : inst); + var self = this; + return this._request(sprintf(INST_HMAP_DETAILS, account, name), null, + function(req) { + req.query = options; + return self._get(req, callback, true); + }); +}; +CloudAPI.prototype.getInstrumentationHeatmapDetails = + CloudAPI.prototype.getInstHmapDetails; +CloudAPI.prototype.GetInstrumentationHeatmapDetails = + CloudAPI.prototype.getInstHmapDetails; + + +/** + * Deletes an instrumentation under your account. + * + * @param {String} account (optional) the login name of the account. + * @param {Number} inst either the id, or can be the object returned + * in list or create. + * @param {Function} callback of the form f(err). + * @throws {TypeError} on bad input. + */ +CloudAPI.prototype.delInst = function(account, inst, callback) { + if (typeof(inst) === 'function') { + callback = inst; + inst = account; + account = this.account; + } + if (!inst || (typeof(inst) !== 'object' && typeof(inst) !== 'number')) + throw new TypeError('inst (object|number) required'); + if (!callback || typeof(callback) !== 'function') + throw new TypeError('callback (function) required'); + if (typeof(account) === 'object') + account = account.login; + + var name = (typeof(inst) === 'object' ? inst.id + '' : inst); + var self = this; + return this._request(sprintf(INST, account, name), null, function(req) { + return self._del(req, callback); + }); +}; +CloudAPI.prototype.deleteInstrumentation = CloudAPI.prototype.delInst; +CloudAPI.prototype.DeleteInstrumentation = CloudAPI.prototype.delInst; + + + +///--- Private Functions + +CloudAPI.prototype._updateMachine = function(account, + machine, + action, + params, + callback) { + assert.ok(account); + assert.ok(machine); + assert.ok(action); + assert.ok(params); + if (typeof(params) === 'function') { + callback = params; + params = {}; + } + assert.ok(callback); + + params.action = action; + + var name = (typeof(machine) === 'object' ? machine.id : machine); + var self = this; + return this._request(sprintf(MACHINE, account, name), null, function(req) { + req.expect = 202; + req.query = params; + return self._post(req, callback); + }); +}; + + +CloudAPI.prototype._error = function(err) { + assert.ok(err); + + function _newError(code, message) { + var e = new Error(); + e.name = 'CloudApiError'; + e.code = code; + e.message = message; + return e; + } + + if (err.httpCode >= 500 && err.details && err.details.body) { + try { + var response = JSON.parse(err.details.body); + if (response && response.code && response.message) + return _newError(response.code, response.message); + + } catch (e) { + log.warn('Invalid JSON for err=%o => %o', err, e); + } + } else { + if (err.details && err.details.object && err.details.object.code) + return _newError(err.details.object.code, err.details.object.message); + + } + + return err; +}; + + +CloudAPI.prototype._get = function(req, callback, noCache) { + assert.ok(req); + assert.ok(callback); + + var self = this; + + // Check the cache first + if (!noCache) { + var cached = this._cacheGet(req.path, req.cacheTTL); + if (cached) { + if (cached instanceof Error) + return callback(cached); + + return callback(null, cached); + } + } + + // Issue HTTP request + return this.client.get(req, function(err, obj, headers) { + if (err) + err = self._error(err); + + if (obj) + self._cachePut(req.path, obj); + + log.debug('CloudAPI._get(%s) -> err=%o, obj=%s', req.path, err, obj); + return callback(err, obj); + }); +}; + + +CloudAPI.prototype._post = function(req, callback) { + assert.ok(req); + assert.ok(callback); + + var self = this; + + // Issue HTTP request + return this.client.post(req, function(err, obj, headers) { + if (err) + err = self._error(err); + + log.debug('CloudAPI._post(%s) -> err=%o, obj=%o', req.path, err, obj); + return callback(err, obj); + }); +}; + + +CloudAPI.prototype._del = function(req, callback) { + assert.ok(req); + assert.ok(callback); + + var self = this; + + // Issue HTTP request + return this.client.del(req, function(err, headers) { + if (err) { + err = self._error(err); + } else { + self._cachePut(req.path, null); + } + + log.debug('CloudAPI._del(%s) -> err=%o', req.path, err); + return callback(err); + }); +}; + + +CloudAPI.prototype._request = function(path, body, callback) { + assert.ok(path); + assert.ok(body !== undefined); + assert.ok(callback); + + var now = restify.httpDate(); + + var obj = { + path: path, + headers: { + Authorization: authz, + Date: now + } + }; + if (body) + obj.body = body; + + var authz; + if (this.basicAuth) { + obj.headers.Authorization = this.basicAuth; + } else { + if (!this.sshAgent) { + var signer = crypto.createSign('RSA-SHA256'); + signer.update(now); + obj.headers.Authorization = sprintf(SIGNATURE, + this.keyId, + 'rsa-sha256', + signer.sign(this.key, 'base64')); + } else { + var self = this; + return this.sshAgent.sign(this.key, new Buffer(now), function(err, sig) { + if (!err && sig) + obj.headers.Authorization = sprintf(SIGNATURE, + self.keyId, + 'rsa-sha1', + sig.signature); + + return callback(obj); + }); + } + } + + return callback(obj); +}; + + +CloudAPI.prototype._cachePut = function(key, value) { + assert.ok(key); + + if (!this.cache) + return false; + + if (value === null) { + // Do a purge + log.debug('CloudAPI._cachePut(%s): purging', key); + return this.cache.set(key, null); + } + + var obj = { + value: value, + ctime: new Date().getTime() + }; + log.debug('CloudAPI._cachePut(%s): writing %o', key, obj); + this.cache.set(key, obj); + return true; +}; + + +CloudAPI.prototype._cacheGet = function(key, expiry) { + assert.ok(key); + + if (!this.cache) + return null; + + var maxAge = expiry || this.cacheExpiry; + + var obj = this.cache.get(key); + if (obj) { + assert.ok(obj.ctime); + assert.ok(obj.value); + var now = new Date().getTime(); + if ((now - obj.ctime) <= maxAge) { + log.debug('CloudAPI._cacheGet(%s): cache hit => %o', key, obj); + return obj.value; + } + } + + log.debug('CloudAPI._cacheGet(%s): cache miss', key); + return null; +}; + + + +///--- Exports + +module.exports = { + + CloudAPI: CloudAPI, + + + createClient: function(options) { + return new CloudAPI(options); + } + +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..560bc11 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,11 @@ +// Copyright 2011 Joyent, Inc. All rights reserved. + +var cloudapi = require('./cloudapi'); + +module.exports = { + + CloudAPI: cloudapi.CloudAPI, + + createClient: cloudapi.createClient + +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..ae63d85 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,27 @@ +// Copyright 2011 Joyent, Inc. All rights reserved. + +var sprintf = require('sprintf').sprintf; + +module.exports = { + + /** + * Constructs a new HTTP Authorization header with the 'Basic' scheme. + * + * HTTP defines basic auth to be nothing but: + * Authorization: Basic Base64(:) + * + * So that's what this gives back (the value for an Authorization header). + * + * @param {String} username duh. + * @param {String} password another duh. + * @return {String} value for an HTTP Authorization header. + */ + basicAuth: function(username, password) { + if (!username) throw new TypeError('username required'); + if (!password) throw new TypeError('password required'); + + var buffer = new Buffer(username + ':' + password, 'utf8'); + return 'Basic ' + buffer.toString('base64'); + } + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2a71615 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "author": "Joyent, Inc. (http://www.joyent.com)", + "name": "smartdc", + "description": "Client SDK and CLI for the Joyent SmartDataCenter API", + "version": "6.1.0", + "homepage": "http://www.joyent.com/software/smartdatacenter", + "repository": { + "type": "git", + "url": "git://github.com/joyent/node-smartdc.git" + }, + "main": "lib/index.js", + "engines": { + "node": "~0.4.9" + }, + "directories": { + "bin": "./bin", + "lib": "./lib" + }, + "dependencies": { + "lru-cache": "~1.0.2", + "nopt": "~1.0.6", + "restify": "~0.3.12", + "sprintf": "~0.1.1", + "ssh-agent": "~0.1.0" + }, + "devDependencies": {} +}