commit 120f3198cf6b72387ddb22356e87e20d43ffaa95 Author: Trent Mick Date: Fri Feb 7 13:21:24 2014 -0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4c0c89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/tmp +/docs/*.json +/docs/*.html +/build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d65f40b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "deps/javascriptlint"] + path = deps/javascriptlint + url = git://github.com/davepacheco/javascriptlint.git +[submodule "deps/jsstyle"] + path = deps/jsstyle + url = git://github.com/davepacheco/jsstyle.git diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..9c7cbaf --- /dev/null +++ b/CHANGES.md @@ -0,0 +1 @@ +# joyent CLI Changelog diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2940ede --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# +# Copyright (c) 2014, Joyent, Inc. All rights reserved. +# +# Makefile for joyent/joyent +# + +# +# Vars, Tools, Files, Flags +# +JS_FILES := bin/joyent \ + $(shell find lib -name '*.js' | grep -v '/tmp/') +JSL_CONF_NODE = tools/jsl.node.conf +JSL_FILES_NODE = $(JS_FILES) +JSSTYLE_FILES = $(JS_FILES) +JSSTYLE_FLAGS = -f tools/jsstyle.conf +CLEAN_FILES += ./node_modules + +include ./tools/mk/Makefile.defs + +# +# Targets +# +.PHONY: all +all: + npm install + +.PHONY: test +test: + ./test/runtests + +.PHONY: dumpvar +dumpvar: + @if [[ -z "$(VAR)" ]]; then \ + echo "error: set 'VAR' to dump a var"; \ + exit 1; \ + fi + @echo "$(VAR) is '$($(VAR))'" + + +include ./tools/mk/Makefile.deps +include ./tools/mk/Makefile.targ diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcc0d7b --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +`joyent` is a CLI for the Joyent Public Cloud (https://my.joyentcloud.com). + +# Installation + +1. Install [node.js](http://nodejs.org/). +2. `npm install -g joyent` + +Verify that installed and is on your PATH: + + $ joyent --version + joyent CLI 1.0.0 + +Before you can used the CLI you'll need a Joyent account, an SSH key uploaded +and `joyent` configured with those account details. + +# Setup + +TODO + +# Getting Started + +TODO diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..2e74ba4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,24 @@ +# first + +- machines: + - short default output + - long '-l' output, -H, -o, -s + - get image defaults and fill those in +- couple commands: machines, machine, provision (create-machine?) +- re-write of cloudapi.js (eventually a separate module) +- uuid caching +- UUID prefix support +- profile command (adding profile, edit, etc.) +- multi-dc support... profile.dcs + + +# naming + +node-joyentcloud.git that installs joyentcloud. Suggest 'jc' as alias if people want to. + +node-smartdc installs joyentcloud and warns about deprecation on stderr. + +# later (in no particular order) + +- how to add/exclude DCs? +- cmdln.js support for bash tab completion diff --git a/bin/joyent b/bin/joyent new file mode 100755 index 0000000..eadbcbe --- /dev/null +++ b/bin/joyent @@ -0,0 +1,14 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2014 Joyent Inc. All rights reserved. + * + * joyent - Joyent public cloud (https://my.joyentcloud.com/) CLI + */ + +var p = console.log; +var cmdln = require('cmdln'); +var CLI = require('../lib/cli'); + +if (require.main === module) { + cmdln.main(CLI, process.arg, {showCode: true}); +} diff --git a/deps/javascriptlint b/deps/javascriptlint new file mode 160000 index 0000000..e1bd0ab --- /dev/null +++ b/deps/javascriptlint @@ -0,0 +1 @@ +Subproject commit e1bd0abfd424811af469d1ece3af131d95443924 diff --git a/deps/jsstyle b/deps/jsstyle new file mode 160000 index 0000000..07d4f68 --- /dev/null +++ b/deps/jsstyle @@ -0,0 +1 @@ +Subproject commit 07d4f68251063be6496a42dd00a7f5bacd65c5e4 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..86c4d1a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,10 @@ +--- +title: sdcadm (Administer a SDC standup) +markdown2extras: wiki-tables, code-friendly, cuddled-lists, link-patterns +markdown2linkpatternsfile: link-patterns.txt +apisections: +--- + +# sdcadm + +TODO diff --git a/docs/link-patterns.txt b/docs/link-patterns.txt new file mode 100644 index 0000000..2e2a694 --- /dev/null +++ b/docs/link-patterns.txt @@ -0,0 +1 @@ +/([A-Z]+-\d+)/ https://devhub.joyent.com/jira/browse/\1 diff --git a/etc/defaults.json b/etc/defaults.json new file mode 100644 index 0000000..b52b747 --- /dev/null +++ b/etc/defaults.json @@ -0,0 +1,9 @@ +{ + "defaultProfile": "env", + "dcs": { + "us-east-1": "https://us-east-1.api.joyentcloud.com", + "us-west-1": "https://us-west-1.api.joyentcloud.com", + "us-sw-1": "https://us-sw-1.api.joyentcloud.com", + "eu-ams-1": "https://eu-ams-1.api.joyentcloud.com" + } +} diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..ffa531e --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2014 Joyent Inc. All rights reserved. + * + * The 'joyent' CLI class. + */ + +var p = console.log; +var e = console.error; +var util = require('util'), + format = util.format; +var child_process = require('child_process'), + spawn = child_process.spawn, + exec = child_process.exec; +var fs = require('fs'); + + +var assert = require('assert-plus'); +var async = require('async'); +var bunyan = require('bunyan'); +var cmdln = require('cmdln'), + Cmdln = cmdln.Cmdln; + +var common = require('./common'); +var Joyent = require('./joyent'); + + + +//---- globals + +var pkg = require('../package.json'); +var name = 'joyent'; +var log = bunyan.createLogger({ + name: name, + serializers: bunyan.stdSerializers, + stream: process.stderr, + level: 'warn' +}); + + + +//---- CLI class + +function CLI() { + Cmdln.call(this, { + name: pkg.name, + desc: pkg.description, + options: [ + {names: ['help', 'h'], type: 'bool', help: 'Print help and exit.'}, + {name: 'version', type: 'bool', help: 'Print version and exit.'}, + {names: ['verbose', 'v'], type: 'bool', + help: 'Verbose/debug output.'} + ], + helpOpts: { + includeEnv: true, + minHelpCol: 23 /* line up with option help */ + } + }); +} +util.inherits(CLI, Cmdln); + +CLI.prototype.init = function (opts, args, callback) { + var self = this; + + if (opts.version) { + p(this.name, pkg.version); + callback(false); + return; + } + this.opts = opts; + if (opts.verbose) { + process.env.DEBUG = 1; //TODO This is a lame requirement of cmdln.main(). + log.level('trace'); + log.src = true; + } + + this.__defineGetter__('joyent', function () { + if (self._joyent === undefined) { + self._joyent = new Joyent({log: log}); + } + return self._joyent; + }); + + // Cmdln class handles `opts.help`. + Cmdln.prototype.init.apply(this, arguments); +}; + + +CLI.prototype.do_profile = function (subcmd, opts, args, callback) { + if (opts.help) { + this.do_help('help', {}, [subcmd], callback); + return; + } else if (args.length > 1) { + return callback(new Error('too many args: ' + args)); + } + + var profs = common.deepObjCopy(this.joyent.profiles); + var currProfileName = this.joyent.profile.name; + for (var i = 0; i < profs.length; i++) { + profs[i].curr = (profs[i].name === currProfileName ? '*' : ' '); + profs[i].dcs = (profs[i].dcs ? Object.keys(profs[i].dcs) : ['all']) + .join(','); + } + if (opts.json) { + p(JSON.stringify(profs, null, 4)); + } else { + common.tabulate(profs, { + columns: 'curr,name,dcs,account,keyId', + sort: 'name,account', + validFields: 'curr,name,dcs,account,keyId' + }); + } + callback(); +}; +CLI.prototype.do_profile.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON output.' + } +]; +CLI.prototype.do_profile.help = ( + 'Create, update or inpect joyent CLI profiles.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} profile\n' + + '\n' + + '{{options}}' +); + + +CLI.prototype.do_dcs = function (subcmd, opts, args, callback) { + if (opts.help) { + this.do_help('help', {}, [subcmd], callback); + return; + } else if (args.length > 1) { + return callback(new Error('too many args: ' + args)); + } + + var dcs = this.joyent.config.dcs; + var dcsArray = Object.keys(dcs).map( + function (n) { return {name: n, url: dcs[n]}; }); + if (opts.json) { + p(JSON.stringify(dcsArray, null, 4)); + } else { + common.tabulate(dcsArray, { + columns: 'name,url', + sort: 'name', + validFields: 'name,url' + }); + } + callback(); +}; +CLI.prototype.do_dcs.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON output.' + } +]; +CLI.prototype.do_dcs.help = ( + 'List, add or remove datacenters.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} dcs\n' + + '\n' + + '{{options}}' +); + + +CLI.prototype.do_machines = function (subcmd, opts, args, callback) { + var self = this; + if (opts.help) { + this.do_help('help', {}, [subcmd], callback); + return; + } else if (args.length > 1) { + return callback(new Error('too many args: ' + args)); + } + + //XXX joyent.listMachines should change to return a 'res' event emitter + // that emits 'dcError' and 'data'. + var listOpts = { + onDcError: function (dc, dcErr) { + console.warn('%s machines: dc %s error: %s', self.name, dc, dcErr); + } + }; + this.joyent.listMachines(listOpts, function (err, machines) { + if (err) + return callback(err); + if (opts.json) { + p(JSON.stringify(machines, null, 4)); + } else { + // TODO: get short output down to something like + // 'us-west-1 e91897cf testforyunong2 linux running 2013-11-08' + // 'us-west-1 e91897cf testforyunong2 ubuntu/13.3.0 running 2013-11-08' + common.tabulate(machines, { + //columns: 'dc,id,name,type,state,created,image,package,memory,disk', + columns: 'dc,id,name,state,created', + sort: 'created', + validFields: 'dc,id,name,type,state,image,package,memory,disk,created,updated,compute_node,primaryIp' + }); + } + callback(); + }); +}; +CLI.prototype.do_machines.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON output.' + } +]; +CLI.prototype.do_machines.help = ( + 'List machines.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} machines [...]\n' + + '\n' + + '{{options}}' +); + + + + +//---- exports + +module.exports = CLI; diff --git a/lib/common.js b/lib/common.js new file mode 100755 index 0000000..8e9512a --- /dev/null +++ b/lib/common.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2014 Joyent Inc. All rights reserved. + */ + +var p = console.log; +var assert = require('assert-plus'); +var async = require('async'); +var backoff = require('backoff'); +var fs = require('fs'); +var once = require('once'); +var sprintf = require('extsprintf').sprintf; +var util = require('util'), + format = util.format; +var verror = require('verror'); + +var errors = require('./errors'), + InternalError = errors.InternalError; + + +function objCopy(obj, target) { + if (target === undefined) { + target = {}; + } + Object.keys(obj).forEach(function (k) { + target[k] = obj[k]; + }); + return target; +} + + +function deepObjCopy(obj) { + return JSON.parse(JSON.stringify(obj)); +} + + +function zeroPad(n, width) { + var s = String(n); + while (s.length < width) { + s = '0' + s; + } + return s; +} + + +/** + * Convert a boolean or string representation into a boolean, or + * raise TypeError trying. + * + * @param value {Boolean|String} The input value to convert. + * @param default_ {Boolean} The default value is `value` is undefined. + * @param errName {String} The variable name to quote in the possibly + * raised TypeError. + */ +function boolFromString(value, default_, errName) { + if (value === undefined) { + return default_; + } else if (value === 'false' || value === '0') { + return false; + } else if (value === 'true' || value === '1') { + return true; + } else if (typeof (value) === 'boolean') { + return value; + } else { + throw new TypeError( + format('invalid value for "%s": %j', errName, value)); + } +} + + + +/** + * Print a table of the given items. + * + * @params items {Array} of row objects. + * @params options {Object} + * - `columns` {String} of comma-separated field names for columns + * - `skipHeader` {Boolean} Default false. + * - `sort` {String} of comma-separate fields on which to alphabetically + * sort the rows. Optional. + * - `validFields` {String} valid fields for `columns` and `sort` + */ +function tabulate(items, options) { + assert.arrayOfObject(items, 'items'); + assert.object(options, 'options'); + assert.string(options.columns, 'options.columns'); + assert.optionalBool(options.skipHeader, 'options.skipHeader'); + assert.optionalString(options.sort, 'options.sort'); + assert.optionalString(options.validFields, 'options.validFields'); + + if (items.length === 0) { + return; + } + + // Validate. + var validFields = options.validFields && options.validFields.split(','); + var columns = options.columns.split(','); + var sort = options.sort ? options.sort.split(',') : []; + if (validFields) { + columns.forEach(function (c) { + if (validFields.indexOf(c) === -1) { + throw new TypeError(sprintf('invalid output field: "%s"', c)); + } + }); + } + sort.forEach(function (s) { + if (s[0] === '-') s = s.slice(1); + if (validFields && validFields.indexOf(s) === -1) { + throw new TypeError(sprintf('invalid sort field: "%s"', s)); + } + }); + + // Function to lookup each column field in a row. + var colFuncs = columns.map(function (lookup) { + return new Function( + 'try { return (this.' + lookup + '); } catch (e) {}'); + }); + + // Determine columns and widths. + var widths = {}; + columns.forEach(function (c) { widths[c] = c.length; }); + items.forEach(function (item) { + for (var j = 0; j < columns.length; j++) { + var col = columns[j]; + var cell = colFuncs[j].call(item); + if (cell === null || cell === undefined) { + continue; + } + widths[col] = Math.max( + widths[col], (cell ? String(cell).length : 0)); + } + }); + + var template = ''; + for (var i = 0; i < columns.length; i++) { + if (i === columns.length - 1) { + // Last column, don't have trailing whitespace. + template += '%s'; + } else { + template += '%-' + String(widths[columns[i]]) + 's '; + } + } + + function cmp(a, b) { + for (var j = 0; j < sort.length; j++) { + var field = sort[j]; + var invert = false; + if (field[0] === '-') { + invert = true; + field = field.slice(1); + } + assert.ok(field.length, 'zero-length sort field: ' + options.sort); + var a_cmp = Number(a[field]); + var b_cmp = Number(b[field]); + if (isNaN(a_cmp) || isNaN(b_cmp)) { + a_cmp = a[field] || ''; + b_cmp = b[field] || ''; + } + if (a_cmp < b_cmp) { + return (invert ? 1 : -1); + } else if (a_cmp > b_cmp) { + return (invert ? -1 : 1); + } + } + return 0; + } + if (sort.length) { + items.sort(cmp); + } + + if (!options.skipHeader) { + var header = columns.map(function (c) { return c.toUpperCase(); }); + header.unshift(template); + console.log(sprintf.apply(null, header)); + } + items.forEach(function (item) { + var row = []; + for (var j = 0; j < colFuncs.length; j++) { + var cell = colFuncs[j].call(item); + if (cell === null || cell === undefined) { + row.push('-'); + } else { + row.push(String(cell)); + } + } + row.unshift(template); + console.log(sprintf.apply(null, row)); + }); +} + + +//---- exports + +module.exports = { + objCopy: objCopy, + deepObjCopy: deepObjCopy, + zeroPad: zeroPad, + boolFromString: boolFromString, + tabulate: tabulate +}; +// vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/config.js b/lib/config.js new file mode 100755 index 0000000..c654917 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2014 Joyent Inc. All rights reserved. + */ + +var p = console.log; +var assert = require('assert-plus'); +var fs = require('fs'); +var path = require('path'); +var sprintf = require('extsprintf').sprintf; + +var common = require('./common'); + + +var CONFIG_PATH = path.resolve(process.env.HOME, '.joyentconfig.json'); +var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json'); + + +function loadConfigSync() { + var config = JSON.parse(fs.readFileSync(DEFAULTS_PATH, 'utf8')); + if (fs.existsSync(CONFIG_PATH)) { + var userConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); + common.objCopy(userConfig, config); + } + + // Add 'env' profile. + if (!config.profiles) { + config.profiles = []; + config.profiles.push({ + name: 'env', + account: process.env.SDC_USER || process.env.SDC_ACCOUNT, + keyId: process.env.SDC_KEY_ID, + //XXX true/false 0/1 handling + rejectUnauthorized: common.boolFromString( + process.env.SDC_TESTING || process.env.SDC_TLS_INSECURE) + }); + } + + return config; +} + + + +//---- exports + +module.exports = { + CONFIG_PATH: CONFIG_PATH, + loadConfigSync: loadConfigSync +}; +// vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..786b365 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2014, Joyent, Inc. All rights reserved. + * + * Error classes that the joyent CLI may produce. + */ + +var util = require('util'), + format = util.format; +var assert = require('assert-plus'); +var verror = require('verror'), + WError = verror.WError, + VError = verror.VError; + + + +// ---- error classes + +/** + * Base error. Instances will always have a string `message` and + * a string `code` (a CamelCase string). + */ +function JoyentError(options) { + assert.object(options, 'options'); + assert.string(options.message, 'options.message'); + assert.string(options.code, 'options.code'); + assert.optionalObject(options.cause, 'options.cause'); + assert.optionalNumber(options.statusCode, 'options.statusCode'); + var self = this; + + var args = []; + if (options.cause) args.push(options.cause); + args.push(options.message); + WError.apply(this, args); + + var extra = Object.keys(options).filter( + function (k) { return ['cause', 'message'].indexOf(k) === -1; }); + extra.forEach(function (k) { + self[k] = options[k]; + }); +} +util.inherits(JoyentError, VError); + +function InternalError(cause, message) { + if (message === undefined) { + message = cause; + cause = undefined; + } + assert.string(message); + JoyentError.call(this, { + cause: cause, + message: message, + code: 'InternalError', + exitStatus: 1 + }); +} +util.inherits(InternalError, JoyentError); + +function UsageError(cause, message) { + if (message === undefined) { + message = cause; + cause = undefined; + } + assert.string(message); + JoyentError.call(this, { + cause: cause, + message: message, + code: 'Usage', + exitStatus: 1 + }); +} +util.inherits(UsageError, JoyentError); + + +/** + * Multiple errors in a group. + */ +function MultiError(errs) { + assert.arrayOfObject(errs, 'errs'); + var lines = [format('multiple (%d) errors', errs.length)]; + for (var i = 0; i < errs.length; i++) { + var err = errs[i]; + lines.push(format(' error (%s): %s', err.code, err.message)); + } + JoyentError.call(this, { + cause: errs[0], + message: lines.join('\n'), + code: 'MultiError', + exitStatus: 1 + }); +} +MultiError.description = 'Multiple errors.'; +util.inherits(MultiError, JoyentError); + + + +// ---- exports + +module.exports = { + JoyentError: JoyentError, + InternalError: InternalError, + UsageError: UsageError, + MultiError: MultiError +}; +// vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/joyent.js b/lib/joyent.js new file mode 100644 index 0000000..ddc9a9f --- /dev/null +++ b/lib/joyent.js @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2014, Joyent, Inc. All rights reserved. + * + * Core Joyent driver class. + */ + +var p = console.log; +var assert = require('assert-plus'); +var async = require('async'); +var format = require('util').format; +var fs = require('fs'); +var path = require('path'); +var smartdc = require('smartdc'); + +var common = require('./common'); +var loadConfigSync = require('./config').loadConfigSync; + + + +//---- Joyent class + +/** + * Create a Joyent. + * + * @param options {Object} + * - log {Bunyan Logger} + * - profile {String} Optional. Name of profile to use. Defaults to + * 'defaultProfile' in the config. + */ +function Joyent(options) { + assert.object(options, 'options'); + assert.object(options.log, 'options.log'); + assert.optionalString(options.profile, 'options.profile'); + var self = this; + + this.config = loadConfigSync(); + this.profiles = this.config.profiles; + this.profile = this.getProfile( + options.profile || this.config.defaultProfile); + + this.log = options.log; + this.log.trace({profile: this.profile}, 'profile data'); +} + + +Joyent.prototype.setDefaultProfile = function setDefaultProfile(name, callback) { + if (!this.getProfile(name)) { + return callback(new Error('no such profile: ' + name)); + } + this.defaultProfileName = this.config.defaultProfile = name; + common.saveConfigSync(this.config); + callback(); +}; + +Joyent.prototype.getProfile = function getProfile(name) { + for (var i = 0; i < this.profiles.length; i++) { + if (this.profiles[i].name === name) { + return this.profiles[i]; + } + } +}; + +/** + * Create or update a profile. + * + * @param profile {Object} + * @param options {Object} + * - setDefault {Boolean} + * @param callback {Function} `function (err)` + */ +Joyent.prototype.createOrUpdateProfile = function createOrUpdateProfile( + profile, options, callback) { + assert.object(profile, 'profile'); + if (typeof(options) === 'function') { + callback = options; + options = {}; + } + assert.object(options, 'options') + assert.optionalBool(options.setDefault, 'options.setDefault') + assert.func(callback, 'callback') + + var found = false; + for (var i = 0; i < this.profiles.length; i++) { + if (this.profiles[i].name === profile.name) { + this.profiles[i] = profile; + found = true; + } + } + if (!found) { + this.profiles.push(profile); + } + if (options.setDefault) { + this.defaultProfileName = this.config.defaultProfile = profile.name; + } + common.saveConfigSync(this.config); + callback(); +}; + +Joyent.prototype.deleteProfile = function deleteProfile(name, callback) { + var found = false; + for (var i = 0; i < this.profiles.length; i++) { + if (this.profiles[i].name === name) { + found = true; + this.profiles.splice(i, 1); + } + } + if (!found) { + return callback(new Error('no such profile: ' + name)); + } + if (this.defaultProfileName === name) { + this.defaultProfileName = this.config.defaultProfile = null; + } + common.saveConfigSync(this.config); + callback(); +}; + + +Joyent.prototype._clientFromDc = function _clientFromDc(dc) { + assert.string(dc, 'dc'); + + if (!this._clientFromDcCache) { + this._clientFromDcCache = {}; + } + if (!this._clientFromDcCache[dc]) { + var prof = this.profile; + var sign; + if (prof.privKey) { + sign = smartdc.privateKeySigner({ + user: prof.account, + keyId: prof.keyId, + key: prof.privKey + }); + } else { + sign = smartdc.cliSigner({keyId: prof.keyId, user: prof.account}); + } + var client = smartdc.createClient({ + url: this.config.dcs[dc], + account: prof.account, + version: '*', + noCache: true, //XXX + rejectUnauthorized: Boolean(prof.rejectUnauthorized), + sign: sign, + // XXX cloudapi.js stupidly uses its own logger, but takes logLevel + logLevel: this.log && this.log.level(), + // Pass our logger to underlying restify client. + log: this.log + }); + this._clientFromDcCache[dc] = client; + } + return this._clientFromDcCache[dc]; +}; + + + +/** + * List machines for the current profile. + * + * @param {Object} options Optional + * - {Function} onDcError `function (dc, err)` called for each DC client + * error. + */ +Joyent.prototype.listMachines = function listMachines(options, callback) { + var self = this; + if (callback === undefined) { + callback = options; + options = {} + } + assert.object(options, 'options'); + assert.optionalFunc(options.onDcError, 'options.onDcError'); + assert.func(callback, 'callback'); + + var allMachines = []; + async.each( + self.profile.dcs || Object.keys(self.config.dcs), + function oneDc(dc, next) { + var client = self._clientFromDc(dc); + client.listMachines(function (err, machines) { + if (err) { + if (options.onDcError) { + options.onDcError(dc, err); + } + } else { + for (var i = 0; i < machines.length; i++) { + machines[i].dc = dc; + allMachines.push(machines[i]); + } + } + next(); + }); + }, + function done(err) { + callback(err, allMachines); + } + ); +}; + + + +//---- exports + +module.exports = Joyent; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6819e64 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "joyent", + "description": "Joyent Public Cloud CLI (https://my.joyentcloud.com/)", + "version": "1.0.0", + "author": "Joyent (joyent.com)", + "private": true, + "dependencies": { + "async": "0.2.9", + "assert-plus": "0.1.4", + "backoff": "2.3.0", + "bunyan": "0.22.0", + "cmdln": "1.3.1", + "dashdash": "1.3.2", + "extsprintf": "1.0.2", + "mkdirp": "0.3.5", + "node-uuid": "1.4.1", + "once": "1.3.0", + "smartdc": "git+ssh://git@github.com:joyent/node-smartdc.git#master", + "verror": "1.3.7" + }, + "engines": { + "node": ">=0.10" + } +} diff --git a/tools/jsl.node.conf b/tools/jsl.node.conf new file mode 100644 index 0000000..db416be --- /dev/null +++ b/tools/jsl.node.conf @@ -0,0 +1,139 @@ +# +# Configuration File for JavaScript Lint +# +# This configuration file can be used to lint a collection of scripts, or to enable +# or disable warnings for scripts that are linted via the command line. +# + +### Warnings +# Enable or disable warnings based on requirements. +# Use "+WarningName" to display or "-WarningName" to suppress. +# ++ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent ++ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity ++ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement +-anon_no_return_value # anonymous function does not always return value ++assign_to_function_call # assignment to a function call +-block_without_braces # block statement without curly braces ++comma_separated_stmts # multiple statements separated by commas (use semicolons?) ++comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) ++default_not_at_end # the default case is not at the end of the switch statement ++dup_option_explicit # duplicate "option explicit" control comment ++duplicate_case_in_switch # duplicate case in switch statement ++duplicate_formal # duplicate formal argument {name} ++empty_statement # empty statement or extra semicolon ++identifier_hides_another # identifer {name} hides an identifier in a parent scope +-inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement ++incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. ++invalid_fallthru # unexpected "fallthru" control comment ++invalid_pass # unexpected "pass" control comment ++jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax ++leading_decimal_point # leading decimal point may indicate a number or an object member ++legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax ++meaningless_block # meaningless block; curly braces have no impact ++mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence ++misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma ++missing_break # missing break statement ++missing_break_for_last_case # missing break statement for last case in switch ++missing_default_case # missing default case in switch statement ++missing_option_explicit # the "option explicit" control comment is missing ++missing_semicolon # missing semicolon ++missing_semicolon_for_lambda # missing semicolon for lambda assignment ++multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs ++nested_comment # nested comment +-no_return_value # function {name} does not always return a value +-octal_number # leading zeros make an octal number ++parseint_missing_radix # parseInt missing radix parameter ++partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag ++redeclared_var # redeclaration of {name} ++trailing_comma_in_array # extra comma is not recommended in array initializers ++trailing_decimal_point # trailing decimal point may indicate a number or an object member ++undeclared_identifier # undeclared identifier: {name} ++unreachable_code # unreachable code +-unreferenced_argument # argument declared but never referenced: {name} +-unreferenced_function # function is declared but never referenced: {name} ++unreferenced_variable # variable is declared but never referenced: {name} ++unsupported_version # JavaScript {version} is not supported ++use_of_label # use of label ++useless_assign # useless assignment ++useless_comparison # useless comparison; comparing identical expressions +-useless_quotes # the quotation marks are unnecessary ++useless_void # use of the void type may be unnecessary (void is always undefined) ++var_hides_arg # variable {name} hides argument ++want_assign_or_call # expected an assignment or function call ++with_statement # with statement hides undeclared variables; use temporary variable instead + + +### Output format +# Customize the format of the error message. +# __FILE__ indicates current file path +# __FILENAME__ indicates current file name +# __LINE__ indicates current line +# __COL__ indicates current column +# __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) +# __ERROR_NAME__ indicates error name (used in configuration file) +# __ERROR_PREFIX__ indicates error prefix +# __ERROR_MSG__ indicates error message +# +# For machine-friendly output, the output format can be prefixed with +# "encode:". If specified, all items will be encoded with C-slashes. +# +# Visual Studio syntax (default): ++output-format __FILE__(__LINE__): __ERROR__ +# Alternative syntax: +#+output-format __FILE__:__LINE__: __ERROR__ + + +### Context +# Show the in-line position of the error. +# Use "+context" to display or "-context" to suppress. +# ++context + + +### Control Comments +# Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for +# the /*@keyword@*/ control comments and JScript conditional comments. (The latter is +# enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, +# although legacy control comments are enabled by default for backward compatibility. +# +-legacy_control_comments + + +### Defining identifiers +# By default, "option explicit" is enabled on a per-file basis. +# To enable this for all files, use "+always_use_option_explicit" +-always_use_option_explicit + +# Define certain identifiers of which the lint is not aware. +# (Use this in conjunction with the "undeclared identifier" warning.) +# +# Common uses for webpages might be: ++define __dirname ++define __filename ++define clearInterval ++define clearTimeout ++define console ++define exports ++define global ++define module ++define process ++define require ++define setInterval ++define setTimeout ++define Buffer ++define JSON ++define Math + +### JavaScript Version +# To change the default JavaScript version: +#+default-type text/javascript;version=1.5 +#+default-type text/javascript;e4x=1 + +### Files +# Specify which files to lint +# Use "+recurse" to enable recursion (disabled by default). +# To add a set of files, use "+process FileName", "+process Folder\Path\*.js", +# or "+process Folder\Path\*.htm". +# + diff --git a/tools/jsstyle.conf b/tools/jsstyle.conf new file mode 100644 index 0000000..e52569e --- /dev/null +++ b/tools/jsstyle.conf @@ -0,0 +1,5 @@ +indent=4 +doxygen +unparenthesized-return=0 +blank-after-start-comment=0 +leading-right-paren-ok=1 diff --git a/tools/mk/Makefile.defs b/tools/mk/Makefile.defs new file mode 100644 index 0000000..50a13c5 --- /dev/null +++ b/tools/mk/Makefile.defs @@ -0,0 +1,43 @@ +# -*- mode: makefile -*- +# +# Copyright (c) 2012, Joyent, Inc. All rights reserved. +# +# Makefile.defs: common defines. +# +# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped +# into other repos as-is without requiring any modifications. If you find +# yourself changing this file, you should instead update the original copy in +# eng.git and then update your repo to use the new version. +# +# This makefile defines some useful defines. Include it at the top of +# your Makefile. +# +# Definitions in this Makefile: +# +# TOP The absolute path to the project directory. The top dir. +# BRANCH The current git branch. +# TIMESTAMP The timestamp for the build. This can be set via +# the TIMESTAMP envvar (used by MG-based builds). +# STAMP A build stamp to use in built package names. +# + +TOP := $(shell pwd) + +# +# Mountain Gorilla-spec'd versioning. +# See "Package Versioning" in MG's README.md: +# +# +# Need GNU awk for multi-char arg to "-F". +_AWK := $(shell (which gawk >/dev/null && echo gawk) \ + || (which nawk >/dev/null && echo nawk) \ + || echo awk) +BRANCH := $(shell git symbolic-ref HEAD | $(_AWK) -F/ '{print $$3}') +ifeq ($(TIMESTAMP),) + TIMESTAMP := $(shell date -u "+%Y%m%dT%H%M%SZ") +endif +_GITDESCRIBE := g$(shell git describe --all --long --dirty | $(_AWK) -F'-g' '{print $$NF}') +STAMP := $(BRANCH)-$(TIMESTAMP)-$(_GITDESCRIBE) + +# node-gyp will print build info useful for debugging with V=1 +export V=1 diff --git a/tools/mk/Makefile.deps b/tools/mk/Makefile.deps new file mode 100644 index 0000000..eeae27f --- /dev/null +++ b/tools/mk/Makefile.deps @@ -0,0 +1,44 @@ +# -*- mode: makefile -*- +# +# Copyright (c) 2012, Joyent, Inc. All rights reserved. +# +# Makefile.deps: Makefile for including common tools as dependencies +# +# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped +# into other repos as-is without requiring any modifications. If you find +# yourself changing this file, you should instead update the original copy in +# eng.git and then update your repo to use the new version. +# +# This file is separate from Makefile.targ so that teams can choose +# independently whether to use the common targets in Makefile.targ and the +# common tools here. +# + +# +# javascriptlint +# +JSL_EXEC ?= deps/javascriptlint/build/install/jsl +JSL ?= $(JSL_EXEC) + +$(JSL_EXEC): | deps/javascriptlint/.git + cd deps/javascriptlint && make install + +distclean:: + if [[ -f deps/javascriptlint/Makefile ]]; then \ + cd deps/javascriptlint && make clean; \ + fi + +# +# jsstyle +# +JSSTYLE_EXEC ?= deps/jsstyle/jsstyle +JSSTYLE ?= $(JSSTYLE_EXEC) + +$(JSSTYLE_EXEC): | deps/jsstyle/.git + +# +# restdown +# +RESTDOWN_EXEC ?= deps/restdown/bin/restdown +RESTDOWN ?= python $(RESTDOWN_EXEC) +$(RESTDOWN_EXEC): | deps/restdown/.git diff --git a/tools/mk/Makefile.targ b/tools/mk/Makefile.targ new file mode 100644 index 0000000..b6e642b --- /dev/null +++ b/tools/mk/Makefile.targ @@ -0,0 +1,309 @@ +# -*- mode: makefile -*- +# +# Copyright (c) 2012, Joyent, Inc. All rights reserved. +# +# Makefile.targ: common targets. +# +# NOTE: This makefile comes from the "eng" repo. It's designed to be dropped +# into other repos as-is without requiring any modifications. If you find +# yourself changing this file, you should instead update the original copy in +# eng.git and then update your repo to use the new version. +# +# This Makefile defines several useful targets and rules. You can use it by +# including it from a Makefile that specifies some of the variables below. +# +# Targets defined in this Makefile: +# +# check Checks JavaScript files for lint and style +# Checks bash scripts for syntax +# Checks SMF manifests for validity against the SMF DTD +# +# clean Removes built files +# +# docs Builds restdown documentation in docs/ +# +# prepush Depends on "check" and "test" +# +# test Does nothing (you should override this) +# +# xref Generates cscope (source cross-reference index) +# +# For details on what these targets are supposed to do, see the Joyent +# Engineering Guide. +# +# To make use of these targets, you'll need to set some of these variables. Any +# variables left unset will simply not be used. +# +# BASH_FILES Bash scripts to check for syntax +# (paths relative to top-level Makefile) +# +# CLEAN_FILES Files to remove as part of the "clean" target. Note +# that files generated by targets in this Makefile are +# automatically included in CLEAN_FILES. These include +# restdown-generated HTML and JSON files. +# +# DOC_FILES Restdown (documentation source) files. These are +# assumed to be contained in "docs/", and must NOT +# contain the "docs/" prefix. +# +# JSL_CONF_NODE Specify JavaScriptLint configuration files +# JSL_CONF_WEB (paths relative to top-level Makefile) +# +# Node.js and Web configuration files are separate +# because you'll usually want different global variable +# configurations. If no file is specified, none is given +# to jsl, which causes it to use a default configuration, +# which probably isn't what you want. +# +# JSL_FILES_NODE JavaScript files to check with Node config file. +# JSL_FILES_WEB JavaScript files to check with Web config file. +# +# JSON_FILES JSON files to be validated +# +# JSSTYLE_FILES JavaScript files to be style-checked +# +# You can also override these variables: +# +# BASH Path to bash (default: "bash") +# +# CSCOPE_DIRS Directories to search for source files for the cscope +# index. (default: ".") +# +# JSL Path to JavaScriptLint (default: "jsl") +# +# JSL_FLAGS_NODE Additional flags to pass through to JSL +# JSL_FLAGS_WEB +# JSL_FLAGS +# +# JSON Path to json tool (default: "json") +# +# JSSTYLE Path to jsstyle (default: "jsstyle") +# +# JSSTYLE_FLAGS Additional flags to pass through to jsstyle +# +# RESTDOWN_EXT By default '.restdown' is required for DOC_FILES +# (see above). If you want to use, say, '.md' instead, then +# set 'RESTDOWN_EXT=.md' in your Makefile. +# + +# +# Defaults for the various tools we use. +# +BASH ?= bash +BASHSTYLE ?= tools/bashstyle +CP ?= cp +CSCOPE ?= cscope +CSCOPE_DIRS ?= . +JSL ?= jsl +JSON ?= json +JSSTYLE ?= jsstyle +MKDIR ?= mkdir -p +MV ?= mv +RESTDOWN_FLAGS ?= +RESTDOWN_EXT ?= .restdown +RMTREE ?= rm -rf +JSL_FLAGS ?= --nologo --nosummary + +ifeq ($(shell uname -s),SunOS) + TAR ?= gtar +else + TAR ?= tar +endif + + +# +# Defaults for other fixed values. +# +BUILD = build +DISTCLEAN_FILES += $(BUILD) +DOC_BUILD = $(BUILD)/docs/public + +# +# Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. +# +ifneq ($(origin JSL_CONF_NODE), undefined) + JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) +endif + +ifneq ($(origin JSL_CONF_WEB), undefined) + JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) +endif + +# +# Targets. For descriptions on what these are supposed to do, see the +# Joyent Engineering Guide. +# + +# +# Instruct make to keep around temporary files. We have rules below that +# automatically update git submodules as needed, but they employ a deps/*/.git +# temporary file. Without this directive, make tries to remove these .git +# directories after the build has completed. +# +.SECONDARY: $($(wildcard deps/*):%=%/.git) + +# +# This rule enables other rules that use files from a git submodule to have +# those files depend on deps/module/.git and have "make" automatically check +# out the submodule as needed. +# +deps/%/.git: + git submodule update --init deps/$* + +# +# These recipes make heavy use of dynamically-created phony targets. The parent +# Makefile defines a list of input files like BASH_FILES. We then say that each +# of these files depends on a fake target called filename.bashchk, and then we +# define a pattern rule for those targets that runs bash in check-syntax-only +# mode. This mechanism has the nice properties that if you specify zero files, +# the rule becomes a noop (unlike a single rule to check all bash files, which +# would invoke bash with zero files), and you can check individual files from +# the command line with "make filename.bashchk". +# +.PHONY: check-bash +check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) + +%.bashchk: % + $(BASH) -n $^ + +%.bashstyle: % + $(BASHSTYLE) $^ + +.PHONY: check-json +check-json: $(JSON_FILES:%=%.jsonchk) + +%.jsonchk: % + $(JSON) --validate -f $^ + +# +# The above approach can be slow when there are many files to check because it +# requires that "make" invoke the check tool once for each file, rather than +# passing in several files at once. For the JavaScript check targets, we define +# a variable for the target itself *only if* the list of input files is +# non-empty. This avoids invoking the tool if there are no files to check. +# +JSL_NODE_TARGET = $(if $(JSL_FILES_NODE), check-jsl-node) +.PHONY: check-jsl-node +check-jsl-node: $(JSL_EXEC) + $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $(JSL_FILES_NODE) + +JSL_WEB_TARGET = $(if $(JSL_FILES_WEB), check-jsl-web) +.PHONY: check-jsl-web +check-jsl-web: $(JSL_EXEC) + $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $(JSL_FILES_WEB) + +.PHONY: check-jsl +check-jsl: $(JSL_NODE_TARGET) $(JSL_WEB_TARGET) + +JSSTYLE_TARGET = $(if $(JSSTYLE_FILES), check-jsstyle) +.PHONY: check-jsstyle +check-jsstyle: $(JSSTYLE_EXEC) + $(JSSTYLE) $(JSSTYLE_FLAGS) $(JSSTYLE_FILES) + +.PHONY: check +check: check-jsl check-json $(JSSTYLE_TARGET) check-bash + @echo check ok + +.PHONY: clean +clean:: + -$(RMTREE) $(CLEAN_FILES) + +.PHONY: distclean +distclean:: clean + -$(RMTREE) $(DISTCLEAN_FILES) + +CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out +CLEAN_FILES += $(CSCOPE_FILES) + +.PHONY: xref +xref: cscope.files + $(CSCOPE) -bqR + +.PHONY: cscope.files +cscope.files: + find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ + -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ + +# +# The "docs" target is complicated because we do several things here: +# +# (1) Use restdown to build HTML and JSON files from each of DOC_FILES. +# +# (2) Copy these files into $(DOC_BUILD) (build/docs/public), which +# functions as a complete copy of the documentation that could be +# mirrored or served over HTTP. +# +# (3) Then copy any directories and media from docs/media into +# $(DOC_BUILD)/media. This allows projects to include their own media, +# including files that will override same-named files provided by +# restdown. +# +# Step (3) is the surprisingly complex part: in order to do this, we need to +# identify the subdirectories in docs/media, recreate them in +# $(DOC_BUILD)/media, then do the same with the files. +# +DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") +DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) +DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) + +DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) +DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) +DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) + +# +# Like the other targets, "docs" just depends on the final files we want to +# create in $(DOC_BUILD), leveraging other targets and recipes to define how +# to get there. +# +.PHONY: docs +docs: \ + $(DOC_FILES:%$(RESTDOWN_EXT)=$(DOC_BUILD)/%.html) \ + $(DOC_FILES:%$(RESTDOWN_EXT)=$(DOC_BUILD)/%.json) \ + $(DOC_MEDIA_FILES_BUILD) + +# +# We keep the intermediate files so that the next build can see whether the +# files in DOC_BUILD are up to date. +# +.PRECIOUS: \ + $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.html) \ + $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%json) + +# +# We do clean those intermediate files, as well as all of DOC_BUILD. +# +CLEAN_FILES += \ + $(DOC_BUILD) \ + $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.html) \ + $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.json) + +# +# Before installing the files, we must make sure the directories exist. The | +# syntax tells make that the dependency need only exist, not be up to date. +# Otherwise, it might try to rebuild spuriously because the directory itself +# appears out of date. +# +$(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) + +$(DOC_BUILD)/%: docs/% | $(DOC_BUILD) + $(CP) $< $@ + +docs/%.json docs/%.html: docs/%$(RESTDOWN_EXT) | $(DOC_BUILD) $(RESTDOWN_EXEC) + $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< + +$(DOC_BUILD): + $(MKDIR) $@ + +$(DOC_MEDIA_DIRS_BUILD): + $(MKDIR) $@ + +# +# The default "test" target does nothing. This should usually be overridden by +# the parent Makefile. It's included here so we can define "prepush" without +# requiring the repo to define "test". +# +.PHONY: test +test: + +.PHONY: prepush +prepush: check test diff --git a/tools/rsync-to b/tools/rsync-to new file mode 100755 index 0000000..8828a27 --- /dev/null +++ b/tools/rsync-to @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Rsync the master in this working copy to the install on the given HN. +# + +#set -o xtrace +set -o errexit + +TOP=$(cd $(dirname $0)/../; pwd) +NODE=$1 +[[ -z "$NODE" ]] && echo 'rsync-to: error: no headnode given' && exit 1 +BASEDIR=/opt/smartdc/sdcadm + +extraOpts= +if [[ $(uname -s) != "SunOS" ]]; then + extraOpts="--exclude *.node --exclude build" +else + # Clean node_modules everytime. + ssh $NODE rm -rf $BASEDIR/node_modules +fi + +for f in bin lib node_modules package.json; do + rsync -av ${TOP}/$f $NODE:$BASEDIR/$f $extraOpts +done