diff --git a/lib/cli.js b/lib/cli.js index a9cecfa..2fd6562 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -217,10 +217,12 @@ function CLI() { 'stop', 'reboot', 'ssh', + 'snapshot', { group: 'Images, Packages, Networks' }, 'image', 'package', 'network', + 'fwrule', { group: 'Other Commands' }, 'info', 'account', @@ -354,6 +356,9 @@ CLI.prototype.do_info = require('./do_info'); CLI.prototype.do_key = require('./do_key'); CLI.prototype.do_keys = require('./do_keys'); +// Firewall rules +CLI.prototype.do_fwrule = require('./do_fwrule'); + // Images CLI.prototype.do_images = require('./do_images'); CLI.prototype.do_image = require('./do_image'); @@ -376,6 +381,9 @@ CLI.prototype.do_package = require('./do_package'); CLI.prototype.do_networks = require('./do_networks'); CLI.prototype.do_network = require('./do_network'); +// Snapshots +CLI.prototype.do_snapshot = require('./do_snapshot'); + // Hidden commands CLI.prototype.do_cloudapi = require('./do_cloudapi'); CLI.prototype.do_badger = require('./do_badger'); diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 9b5a6a7..e6c7f05 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -752,7 +752,30 @@ CloudApi.prototype.rebootMachine = function rebootMachine(uuid, callback) { }; /** - * internal function for start/stop/reboot + * Enables machine firewall. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, machine, res)` + */ +CloudApi.prototype.enableFirewall = +function enableFirewall(uuid, callback) { + return this._doMachine('enable_firewall', uuid, callback); +}; + + +/** + * Disables machine firewall. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, machine, res)` + */ +CloudApi.prototype.disableFirewall = +function disableFirewall(uuid, callback) { + return this._doMachine('disable_firewall', uuid, callback); +}; + +/** + * internal function for start/stop/reboot/enable_firewall/disable_firewall */ CloudApi.prototype._doMachine = function _doMachine(action, uuid, callback) { var self = this; @@ -917,6 +940,378 @@ CloudApi.prototype.machineAudit = function machineAudit(id, cb) { }; +// --- snapshots + +/** + * Creates a new snapshot for a given machine. + * + * The machine cannot be a KVM brand. + * + * Returns a snapshot object. + * + * @param {Object} options object containing: + * - {String} id (required) the machine's id. + * - {String} name (optional) name for new snapshot + * @param {Function} callback of the form f(err, snapshot, res). + */ +CloudApi.prototype.createMachineSnapshot = +function createMachineSnapshot(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.optionalString(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/machines/%s/snapshots', this.account, opts.id), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Wait for a machine's snapshot to go one of a set of specfic states. + * + * @param {Object} options + * - {String} id {required} machine id + * - {String} name (optional) name for new snapshot + * - {Array of String} states - desired state + * - {Number} interval (optional) - time in ms to poll + * @param {Function} callback of the form f(err, snapshot, res). + */ +CloudApi.prototype.waitForSnapshotStates = +function waitForSnapshotStates(opts, cb) { + var self = this; + + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.arrayOfString(opts.states, 'opts.states'); + assert.optionalNumber(opts.interval, 'opts.interval'); + assert.func(cb, 'cb'); + + var interval = opts.interval || 1000; + assert.ok(interval > 0, 'interval must be a positive number'); + + poll(); + + function poll() { + self.getMachineSnapshot({ + id: opts.id, + name: opts.name + }, function (err, snapshot, res) { + if (err) { + cb(err, null, res); + return; + } + if (opts.states.indexOf(snapshot.state) !== -1) { + cb(null, snapshot, res); + return; + } + setTimeout(poll, interval); + }); + } +}; + + +/** + * Lists all snapshots for a given machine. + * + * Returns a list of snapshot objects. + * + * @param {Object} options object containing: + * - {String} id (required) the machine's id. + * @param {Function} callback of the form f(err, snapshot, res). + */ +CloudApi.prototype.listMachineSnapshots = +function listMachineSnapshots(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/machines/%s/snapshots', this.account, opts.id); + this._passThrough(endpoint, opts, cb); +}; + + +/** + * Get a single snapshot for a given machine. + * + * Returns a snapshot object. + * + * @param {Object} options object containing: + * - {String} id (required) the machine's id. + * - {String} name (required) the snapshot's name. + * @param {Function} callback of the form f(err, snapshot, res). + */ +CloudApi.prototype.getMachineSnapshot = +function getMachineSnapshot(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/machines/%s/snapshots/%s', this.account, opts.id, + opts.name); + this._passThrough(endpoint, opts, cb); +}; + + +/** + * Re/boots a machine from a snapshot. + * + * @param {Object} options object containing: + * - {String} id (required) the machine's id. + * - {String} name (required) the snapshot's name. + * @param {Function} callback of the form f(err, res). + */ +function startMachineFromSnapshot(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/machine/%s/snapshots/%s', this.account, opts.id, + opts.name), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +} + + +/** + * Deletes a machine snapshot. + * + * @param {Object} options object containing: + * - {String} id (required) the machine's id. + * - {String} name (required) the snapshot's name. + * @param {Function} callback of the form f(err, res). + */ +CloudApi.prototype.deleteMachineSnapshot = +function deleteMachineSnapshot(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/machines/%s/snapshots/%s', this.account, opts.id, + opts.name) + }, function (err, req, res) { + cb(err, res); + }); +}; + + +// --- firewall rules + +/** + * Creates a Firewall Rule. + * + * @param {Object} options object containing: + * - {String} rule (required) the fwrule text. + * - {Boolean} enabled (optional) default to false. + * @param {Function} callback of the form f(err, fwrule, res). + */ +CloudApi.prototype.createFirewallRule = +function createFirewallRule(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.rule, 'opts.rule'); + assert.optionalString(opts.description, 'opts.description'); + assert.optionalBool(opts.enabled, 'opts.enabled'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules', this.account), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Lists all your Firewall Rules. + * + * Returns an array of objects. + * + * @param opts {Object} Options + * @param {Function} callback of the form f(err, fwrules, res). + */ +CloudApi.prototype.listFirewallRules = +function listFirewallRules(opts, cb) { + assert.object(opts, 'opts'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fwrules', this.account); + this._passThrough(endpoint, opts, cb); +}; + + +/** + * Retrieves a Firewall Rule. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.getFirewallRule = +function getFirewallRule(id, cb) { + assert.uuid(id, 'id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fwrules/%s', this.account, id); + this._request(endpoint, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +// -> +CloudApi.prototype.UPDATE_FIREWALL_RULE_FIELDS = { + enabled: 'boolean', + rule: 'string', + description: 'string' +}; + + +/** + * Updates a Firewall Rule. + * + * @param {Object} opts object containing: + * - {String} id (required) The fwrule id. + * - {String} rule (required) the fwrule text. + * - {Boolean} enabled (optional) default to false. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.updateFirewallRule = +function updateFirewallRule(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.rule, 'opts.rule'); + assert.optionalBool(opts.enabled, 'opts.enabled'); + assert.optionalBool(opts.description, 'opts.description'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s', this.account, opts.id), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Enable a Firewall Rule. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.enableFirewallRule = +function enableFirewallRule(id, cb) { + assert.uuid(id, 'id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s/enable', this.account, id), + data: {} + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Disable a Firewall Rule. + * + * @param {String} id (required) The machine id. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.disableFirewallRule = +function disableFirewallRule(id, cb) { + assert.uuid(id, 'id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s/disable', this.account, id), + data: {} + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * + * + * @param {Object} opts (object) + * - {String} id (required) for your firewall. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deleteFirewallRule = +function deleteFirewallRule(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/fwrules/%s', this.account, opts.id) + }, function (err, req, res) { + cb(err, res); + }); +}; + + +/** + * Lists all the Firewall Rules affecting a given machine. + * + * Returns an array of firewall objects. + * + * @param opts {Object} Options + * - {String} id (required) machine id. + * @param {Function} callback of the form f(err, fwrules, res). + */ +CloudApi.prototype.listMachineFirewallRules = +function listMachineFirewallRules(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/machines/%s/fwrules', this.account, opts.id); + this._passThrough(endpoint, opts, cb); +}; + + +/** + * Lists all the Machines affected by the given firewall rule. + * + * Returns an array of machine objects. + * + * @param opts {Object} Options + * - {String} id (required) firewall rule. + * @param {Function} callback of the form f(err, machines, res). + */ +CloudApi.prototype.listFirewallRuleMachines = +function listFirewallRuleMachines(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fwrules/%s/machines', this.account, opts.id); + this._passThrough(endpoint, opts, cb); +}; + + // --- rbac /** diff --git a/lib/do_fwrule/do_create.js b/lib/do_fwrule/do_create.js new file mode 100644 index 0000000..df34c80 --- /dev/null +++ b/lib/do_fwrule/do_create.js @@ -0,0 +1,86 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2015 Joyent, Inc. + * + * `triton fwrule create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_create(subcmd, opts, args, cb) { + assert.optionalString(opts.description, 'opts.description'); + assert.optionalBool(opts.enabled, 'opts.enabled'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('Missing RULE argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + opts.rule = args[0]; + + var cli = this.top; + cli.tritonapi.cloudapi.createFirewallRule(opts, function (err, fwrule) { + if (err) { + cb(err); + return; + } + + console.log('Created firewall rule %s', fwrule.id); + + cb(); + }); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['enabled', 'e'], + type: 'bool', + help: 'If the firewall rule should be enabled upon creation.' + }, + { + names: ['description', 'd'], + type: 'string', + help: 'Description of the firewall rule.' + } +]; +do_create.help = [ + 'Create a firewall rule.', + '', + 'Usage:', + ' {{name}} create [] RULE', + '', + '{{options}}' +].join('\n'); + +module.exports = do_create; diff --git a/lib/do_fwrule/do_delete.js b/lib/do_fwrule/do_delete.js new file mode 100644 index 0000000..2e612c2 --- /dev/null +++ b/lib/do_fwrule/do_delete.js @@ -0,0 +1,112 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2015 Joyent, Inc. + * + * `triton snapshot delete ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var fs = require('fs'); +var sshpk = require('sshpk'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_delete(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length < 1) { + cb(new errors.UsageError('missing FWRULE-ID argument(s)')); + return; + } + + var cli = this.top; + var ruleIds = args; + + vasync.pipeline({funcs: [ + function confirm(_, next) { + if (opts.force) { + return next(); + } + + var msg; + if (ruleIds.length === 1) { + msg = 'Delete firewall rule "' + ruleIds[0] + '"? [y/n] '; + } else { + msg = format('Delete %d firewall rules (%s)? [y/n] ', + ruleIds.length, ruleIds.join(', ')); + } + + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + function deleteThem(_, next) { + vasync.forEachPipeline({ + inputs: ruleIds, + func: function deleteOne(id, nextId) { + cli.tritonapi.cloudapi.deleteFirewallRule({ + id: id + }, function (err) { + if (err) { + nextId(err); + return; + } + + console.log('Deleted rule %s', id); + nextId(); + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['force', 'f'], + type: 'bool', + help: 'Skip confirmation of delete.' + } +]; +do_delete.help = [ + 'Remove a firewall rule.', + '', + 'Usage:', + ' {{name}} delete [] FWRULE-ID [FWRULE-ID...]', + '', + '{{options}}' +].join('\n'); + +do_delete.aliases = ['rm']; + +module.exports = do_delete; diff --git a/lib/do_fwrule/do_disable.js b/lib/do_fwrule/do_disable.js new file mode 100644 index 0000000..3a132de --- /dev/null +++ b/lib/do_fwrule/do_disable.js @@ -0,0 +1,68 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule disable ...` + */ + +var assert = require('assert-plus'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_disable(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('Missing FWRULE-ID argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var id = args[0]; + var cli = this.top; + + // XXX add support for shortId + cli.tritonapi.cloudapi.disableFirewallRule(id, function onRule(err) { + if (err) { + cb(err); + return; + } + + console.log('Disabled firewall rule %s', id); + + cb(); + }); +} + + +do_disable.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; +do_disable.help = [ + 'Disable a specific firewall rule.', + '', + 'Usage:', + ' {{name}} disable FWRULE-ID', + '', + '{{options}}' +].join('\n'); + +module.exports = do_disable; diff --git a/lib/do_fwrule/do_enable.js b/lib/do_fwrule/do_enable.js new file mode 100644 index 0000000..383fbbb --- /dev/null +++ b/lib/do_fwrule/do_enable.js @@ -0,0 +1,68 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule enable ...` + */ + +var assert = require('assert-plus'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_enable(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('Missing FWRULE-ID argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var id = args[0]; + var cli = this.top; + + // XXX add support for shortId + cli.tritonapi.cloudapi.enableFirewallRule(id, function onRule(err) { + if (err) { + cb(err); + return; + } + + console.log('Enabled firewall rule %s', id); + + cb(); + }); +} + + +do_enable.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; +do_enable.help = [ + 'Enable a specific firewall rule.', + '', + 'Usage:', + ' {{name}} enable FWRULE-ID', + '', + '{{options}}' +].join('\n'); + +module.exports = do_enable; diff --git a/lib/do_fwrule/do_get.js b/lib/do_fwrule/do_get.js new file mode 100644 index 0000000..6c0b736 --- /dev/null +++ b/lib/do_fwrule/do_get.js @@ -0,0 +1,78 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule get ...` + */ + +var assert = require('assert-plus'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_get(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + var errMsg = 'missing FWRULE-ID argument'; + cb(new errors.UsageError(errMsg)); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var id = args[0]; + var cli = this.top; + + // XXX add support for shortId + cli.tritonapi.cloudapi.getFirewallRule(id, function onRule(err, fwrule) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(fwrule)); + } else { + console.log(JSON.stringify(fwrule, null, 4)); + } + + cb(); + }); +} + + +do_get.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; +do_get.help = [ + 'Show a specific firewall rule.', + '', + 'Usage:', + ' {{name}} get FWRULE-ID', + '', + '{{options}}' +].join('\n'); + +module.exports = do_get; diff --git a/lib/do_fwrule/do_instances.js b/lib/do_fwrule/do_instances.js new file mode 100644 index 0000000..2c8803e --- /dev/null +++ b/lib/do_fwrule/do_instances.js @@ -0,0 +1,162 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule instances ...` + */ + +var format = require('util').format; +var tabula = require('tabula'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'shortid,name,img,state,flags,age'; +var COLUMNS_LONG = 'id,name,img,brand,package,state,flags,primaryIp,created'; +var SORT_DEFAULT = 'created'; + + +function do_instances(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('Missing FWRULE-ID argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var id = args[0]; + + var columns = COLUMNS_DEFAULT; + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + columns = columns.split(','); + + var sort = opts.s.split(','); + + var imgs; + var insts; + + var tritonapi = this.top.tritonapi; + + vasync.parallel({funcs: [ + function getTheImages(next) { + tritonapi.listImages({useCache: true}, + function (err, _imgs) { + if (err) { + next(err); + } else { + imgs = _imgs; + next(); + } + }); + }, + function getTheMachines(next) { + tritonapi.cloudapi.listFirewallRuleMachines({ + id: id + }, function (err, _insts) { + if (err) { + next(err); + } else { + insts = _insts; + next(); + } + }); + } + ]}, function (err, results) { + /* + * Error handling: vasync.parallel's `err` is always a MultiError. We + * want to prefer the `getTheMachines` err, e.g. if both get a + * self-signed cert error. + */ + if (err) { + err = results.operations[1].err || err; + return cb(err); + } + + // map "uuid" => "image_name" + var imgmap = {}; + imgs.forEach(function (img) { + imgmap[img.id] = format('%s@%s', img.name, img.version); + }); + + // Add extra fields for nice output. + var now = new Date(); + insts.forEach(function (inst) { + var created = new Date(inst.created); + inst.age = common.longAgo(created, now); + inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image); + inst.shortid = inst.id.split('-', 1)[0]; + var flags = []; + if (inst.docker) flags.push('D'); + if (inst.firewall_enabled) flags.push('F'); + if (inst.brand === 'kvm') flags.push('K'); + inst.flags = flags.length ? flags.join('') : undefined; + }); + + if (opts.json) { + common.jsonStream(insts); + } else { + tabula(insts, { + skipHeader: opts.H, + columns: columns, + sort: sort, + dottedLookup: true + }); + } + + cb(); + }); +} + +do_instances.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: SORT_DEFAULT +})); + +do_instances.help = [ + /* BEGIN JSSTYLED */ + 'List instances a firewall rule is applied to.', + '', + 'Usage:', + ' {{name}} instances [] FWRULE-ID', + '', + '{{options}}', + '', + 'Fields (most are self explanatory, "*" indicates a field added client-side', + 'for convenience):', + ' shortid* A short ID prefix.', + ' flags* Single letter flags summarizing some fields:', + ' "D" docker instance', + ' "F" firewall is enabled', + ' "K" the brand is "kvm"', + ' age* Approximate time since created, e.g. 1y, 2w.', + ' img* The image "name@version", if available, else its', + ' "shortid".' + /* END JSSTYLED */ +].join('\n'); + +do_instances.aliases = ['insts']; + +module.exports = do_instances; diff --git a/lib/do_fwrule/do_list.js b/lib/do_fwrule/do_list.js new file mode 100644 index 0000000..5321793 --- /dev/null +++ b/lib/do_fwrule/do_list.js @@ -0,0 +1,98 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule list ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'shortid,enabled,global,rule'; +var COLUMNS_LONG = 'id,enabled,global,rule,description'; +var SORT_DEFAULT = 'rule'; + + +function do_list(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length > 0) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var cli = this.top; + cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + common.jsonStream(rules); + } else { + var columns = COLUMNS_DEFAULT; + + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + + columns = columns.toLowerCase().split(','); + var sort = opts.s.toLowerCase().split(','); + + if (columns.indexOf('shortid') !== -1) { + rules.forEach(function (rule) { + rule.shortid = common.normShortId(rule.id); + }); + } + + tabula(rules, { + skipHeader: false, + columns: columns, + sort: sort + }); + } + cb(); + }); +} + + +do_list.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: SORT_DEFAULT +})); + +do_list.help = [ + 'Show all firewall rules.', + '', + 'Usage:', + ' {{name}} list []', + '', + '{{options}}' +].join('\n'); + +do_list.aliases = ['ls']; + +module.exports = do_list; diff --git a/lib/do_fwrule/do_update.js b/lib/do_fwrule/do_update.js new file mode 100644 index 0000000..b81f3de --- /dev/null +++ b/lib/do_fwrule/do_update.js @@ -0,0 +1,189 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton fwrule update ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var fs = require('fs'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); +var UPDATE_FIREWALL_RULE_FIELDS + = require('../cloudapi2').CloudApi.prototype.UPDATE_FIREWALL_RULE_FIELDS; + + +function do_update(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + var log = this.log; + var tritonapi = this.top.tritonapi; + + if (args.length === 0) { + cb(new errors.UsageError('Missing FWRULE-ID argument')); + return; + } + + var id = args.pop(); + + vasync.pipeline({arg: {}, funcs: [ + function gatherDataArgs(ctx, next) { + if (opts.file) { + next(); + return; + } + + try { + ctx.data = common.objFromKeyValueArgs(args, { + disableDotted: true, + typeHintFromKey: UPDATE_FIREWALL_RULE_FIELDS + }); + } catch (err) { + next(err); + return; + } + + next(); + }, + + function gatherDataFile(ctx, next) { + if (!opts.file || opts.file === '-') { + next(); + return; + } + + var input = fs.readFileSync(opts.file, 'utf8'); + + try { + ctx.data = JSON.parse(input); + } catch (err) { + next(new errors.TritonError(format( + 'invalid JSON for firewall rule update in "%s": %s', + opts.file, err))); + return; + } + next(); + }, + + function gatherDataStdin(ctx, next) { + if (opts.file !== '-') { + next(); + return; + } + + var stdin = ''; + + process.stdin.resume(); + process.stdin.on('data', function (chunk) { + stdin += chunk; + }); + + process.stdin.on('end', function () { + try { + ctx.data = JSON.parse(stdin); + } catch (err) { + log.trace({stdin: stdin}, + 'invalid firewall rule update JSON on stdin'); + next(new errors.TritonError(format( + 'invalid JSON for firewall rule update on stdin: %s', + err))); + return; + } + next(); + }); + }, + + function validateIt(ctx, next) { + var keys = Object.keys(ctx.data); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = ctx.data[key]; + var type = UPDATE_FIREWALL_RULE_FIELDS[key]; + if (!type) { + next(new errors.UsageError(format('unknown or ' + + 'unupdateable field: %s (updateable fields are: %s)', + key, + Object.keys(UPDATE_FIREWALL_RULE_FIELDS).sort().join( + ', ')))); + return; + } + + if (typeof (value) !== type) { + next(new errors.UsageError(format('field "%s" must be ' + + 'of type "%s", but got a value of type "%s"', key, + type, typeof (value)))); + return; + } + } + next(); + }, + + function updateAway(ctx, next) { + var keys = Object.keys(ctx.data); + if (keys.length === 0) { + console.log('No fields given for firewall rule update'); + next(); + return; + } + + ctx.data.id = id; + + tritonapi.cloudapi.updateFirewallRule(ctx.data, function (err) { + if (err) { + next(err); + return; + } + + console.log('Updated firewall rule %s (fields: %s)', id, + keys.join(', ')); + + next(); + }); + } + ]}, cb); +} + +do_update.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['file', 'f'], + type: 'string', + helpArg: 'FILE', + help: 'A file holding a JSON file of updates, or "-" to read ' + + 'JSON from stdin.' + } +]; +do_update.help = [ + 'Update a firewall rule', + '', + 'Usage:', + ' {{name}} update [FIELD=VALUE ...] FWRULE-ID', + ' {{name}} update -f JSON-FILE FWRULE-ID', + '', + '{{options}}', + + 'Updateable fields:', + ' ' + Object.keys(UPDATE_FIREWALL_RULE_FIELDS).sort().map(function (f) { + return f + ' (' + UPDATE_FIREWALL_RULE_FIELDS[f] + ')'; + }).join('\n '), + '' +].join('\n'); + +do_update.completionArgtypes = ['tritonupdatefwrulefield']; + +module.exports = do_update; diff --git a/lib/do_fwrule/index.js b/lib/do_fwrule/index.js new file mode 100644 index 0000000..4ce8e14 --- /dev/null +++ b/lib/do_fwrule/index.js @@ -0,0 +1,56 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton snapshot ...` + */ + +var Cmdln = require('cmdln').Cmdln; +var util = require('util'); + + + +// ---- CLI class + +function FirewallRuleCLI(top) { + this.top = top; + + Cmdln.call(this, { + name: top.name + ' fwrule', + desc: 'Firewall rule commands', + helpSubcmds: [ + 'help', + { group: 'Key Resources' }, + 'create', + 'list', + 'get', + 'update', + 'delete', + 'enable', + 'disable', + 'instances' + ] + }); +} +util.inherits(FirewallRuleCLI, Cmdln); + +FirewallRuleCLI.prototype.init = function init(opts, args, cb) { + this.log = this.top.log; + Cmdln.prototype.init.apply(this, arguments); +}; + +FirewallRuleCLI.prototype.do_list = require('./do_list'); +FirewallRuleCLI.prototype.do_create = require('./do_create'); +FirewallRuleCLI.prototype.do_get = require('./do_get'); +FirewallRuleCLI.prototype.do_update = require('./do_update'); +FirewallRuleCLI.prototype.do_delete = require('./do_delete'); +FirewallRuleCLI.prototype.do_enable = require('./do_enable'); +FirewallRuleCLI.prototype.do_disable = require('./do_disable'); +FirewallRuleCLI.prototype.do_instances = require('./do_instances'); + +module.exports = FirewallRuleCLI; diff --git a/lib/do_instance/do_fwrules.js b/lib/do_instance/do_fwrules.js new file mode 100644 index 0000000..42ca476 --- /dev/null +++ b/lib/do_instance/do_fwrules.js @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton instance fwrules ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'shortid,enabled,global,rule'; +var COLUMNS_LONG = 'id,enabled,global,rule,description'; +var SORT_DEFAULT = 'rule'; + + +function do_fwrules(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('Missing INST argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('Incorrect number of arguments')); + return; + } + + var cli = this.top; + cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + common.jsonStream(rules); + } else { + var columns = COLUMNS_DEFAULT; + + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + + columns = columns.toLowerCase().split(','); + var sort = opts.s.toLowerCase().split(','); + + if (columns.indexOf('shortid') !== -1) { + rules.forEach(function (rule) { + rule.shortid = common.normShortId(rule.id); + }); + } + + tabula(rules, { + skipHeader: false, + columns: columns, + sort: sort + }); + } + cb(); + }); +} + + +do_fwrules.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: SORT_DEFAULT +})); + +do_fwrules.help = [ + 'Show firewall rules applied to an instance.', + '', + 'Usage:', + ' {{name}} fwrules [] INST', + '', + '{{options}}' +].join('\n'); + +//do_fwrules.aliases = ['fwrules']; + +module.exports = do_fwrules; diff --git a/lib/do_instance/index.js b/lib/do_instance/index.js index 9c43dab..491e5a5 100644 --- a/lib/do_instance/index.js +++ b/lib/do_instance/index.js @@ -41,7 +41,8 @@ function InstanceCLI(top) { { group: '' }, 'ssh', 'wait', - 'audit' + 'audit', + 'fwrules' ] }); } @@ -64,6 +65,7 @@ InstanceCLI.prototype.do_reboot = require('./do_reboot'); InstanceCLI.prototype.do_ssh = require('./do_ssh'); InstanceCLI.prototype.do_wait = require('./do_wait'); InstanceCLI.prototype.do_audit = require('./do_audit'); +InstanceCLI.prototype.do_fwrules = require('./do_fwrules'); InstanceCLI.aliases = ['inst']; diff --git a/lib/do_snapshot/do_create.js b/lib/do_snapshot/do_create.js new file mode 100644 index 0000000..7473bcb --- /dev/null +++ b/lib/do_snapshot/do_create.js @@ -0,0 +1,147 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton snapshot create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var distractions = require('../distractions'); +var errors = require('../errors'); + + +function do_create(subcmd, opts, args, cb) { + assert.optionalString(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing INST argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var inst = args[0]; + var cli = this.top; + + var createOpts = { + userId: opts.userId, + id: inst + }; + + if (opts.name) { + createOpts.name = opts.name; + } + + vasync.pipeline({arg: {}, funcs: [ + function createSnapshot(ctx, next) { + ctx.start = Date.now(); + + cli.tritonapi.cloudapi.createMachineSnapshot(createOpts, + function (err, snapshot) { + if (err) { + next(err); + return; + } + + console.log('Creating snapshot %s', snapshot.name); + ctx.name = snapshot.name; + + next(); + }); + }, + function maybeWait(ctx, next) { + if (!opts.wait) { + return next(); + } + + // 1 'wait': no distraction. + // >1 'wait': distraction, pass in the N. + var distraction; + if (process.stderr.isTTY && opts.wait.length > 1) { + distraction = distractions.createDistraction(opts.wait.length); + } + + var cloudapi = cli.tritonapi.cloudapi; + var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi); + + waiter({ + id: inst, + name: ctx.name, + states: ['created', 'failed'] + }, function (err, snap) { + if (distraction) { + distraction.destroy(); + } + + if (err) { + return next(err); + } + + if (opts.json) { + console.log(JSON.stringify(snap)); + } else if (snap.state === 'created') { + var duration = Date.now() - ctx.start; + console.log('Created snapshot "%s" in %s', snap.name, + common.humanDurationFromMs(duration)); + next(); + } else { + next(new Error(format('Failed to create snapshot "%s"', + snap.name))); + } + }); + } + ]}, cb); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['name', 'n'], + type: 'string', + helpArg: 'SNAPSHOT-NAME', + help: 'An optional name for a snapshot.' + }, + { + names: ['wait', 'w'], + type: 'arrayOfBool', + help: 'Wait for the creation to complete. Use multiple times for a ' + + 'spinner.' + } +]; +do_create.help = [ + 'Create a snapshot of a machine.', + '', + 'Usage:', + ' {{name}} create [] INST', + '', + '{{options}}', + 'Snapshot do not work for instances of type "kvm".' +].join('\n'); + +module.exports = do_create; diff --git a/lib/do_snapshot/do_delete.js b/lib/do_snapshot/do_delete.js new file mode 100644 index 0000000..f374a6a --- /dev/null +++ b/lib/do_snapshot/do_delete.js @@ -0,0 +1,161 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton snapshot delete ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var distractions = require('../distractions'); +var errors = require('../errors'); + + +function do_delete(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length < 2) { + cb(new errors.UsageError('missing INST and SNAPSHOT-NAME argument(s)')); + return; + } + + var cli = this.top; + var inst = args[0]; + var names = args.slice(1, args.length); + + function wait(name, startTime, next) { + // 1 'wait': no distraction. + // >1 'wait': distraction, pass in the N. + var distraction; + if (process.stderr.isTTY && opts.wait.length > 1) { + distraction = distractions.createDistraction(opts.wait.length); + } + + var cloudapi = cli.tritonapi.cloudapi; + var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi); + + waiter({ + id: inst, + name: name, + states: ['deleted'] + }, function (err, snap) { + if (distraction) { + distraction.destroy(); + } + if (err) { + return next(err); + } + if (snap.state === 'deleted') { + var duration = Date.now() - startTime; + var durStr = common.humanDurationFromMs(duration); + console.log('Deleted snapshot "%s" in %s', name, durStr); + + next(); + } else { + // shouldn't get here, but... + next(new Error(format('Failed to delete snapshot "%s"', name))); + } + }); + } + + vasync.pipeline({funcs: [ + function confirm(_, next) { + if (opts.force) { + return next(); + } + + var msg; + if (names.length === 1) { + msg = 'Delete snapshot "' + names[0] + '"? [y/n] '; + } else { + msg = format('Delete %d snapshots (%s)? [y/n] ', + names.length, names.join(', ')); + } + + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + function deleteThem(_, next) { + var startTime = Date.now(); + + vasync.forEachParallel({ + inputs: names, + func: function deleteOne(name, nextName) { + cli.tritonapi.cloudapi.deleteMachineSnapshot({ + id: inst, + name: name + }, function (err) { + if (err) { + nextName(err); + return; + } + + console.log('Deleting snapshot "%s"', name); + + if (opts.wait) { + wait(name, startTime, nextName); + } else { + nextName(); + } + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['force', 'f'], + type: 'bool', + help: 'Skip confirmation of delete.' + }, + { + names: ['wait', 'w'], + type: 'arrayOfBool', + help: 'Wait for the deletion to complete. Use multiple times for a ' + + 'spinner.' + } +]; +do_delete.help = [ + 'Remove a snapshot from a machine.', + '', + 'Usage:', + ' {{name}} delete [] SNAPSHOT-NAME [SNAPSHOT-NAME...]', + '', + '{{options}}' +].join('\n'); + +do_delete.aliases = ['rm']; + +module.exports = do_delete; diff --git a/lib/do_snapshot/do_get.js b/lib/do_snapshot/do_get.js new file mode 100644 index 0000000..623d3ad --- /dev/null +++ b/lib/do_snapshot/do_get.js @@ -0,0 +1,81 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton snapshot get ...` + */ + +var assert = require('assert-plus'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_get(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length < 2) { + var errMsg = 'missing INST and/or SNAPSHOT-NAME arguments'; + cb(new errors.UsageError(errMsg)); + return; + } else if (args.length > 2) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var id = args[0]; + var name = args[1]; + var cli = this.top; + + cli.tritonapi.cloudapi.getMachineSnapshot({ + id: id, + name: name + }, function onSnapshot(err, snapshot) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(snapshot)); + } else { + console.log(JSON.stringify(snapshot, null, 4)); + } + + cb(); + }); +} + + +do_get.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; +do_get.help = [ + 'Show a specific snapshot of a machine.', + '', + 'Usage:', + ' {{name}} get INST SNAPSHOT-NAME', + '', + '{{options}}' +].join('\n'); + +module.exports = do_get; diff --git a/lib/do_snapshot/do_list.js b/lib/do_snapshot/do_list.js new file mode 100644 index 0000000..ba16ce3 --- /dev/null +++ b/lib/do_snapshot/do_list.js @@ -0,0 +1,95 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2016 Joyent, Inc. + * + * `triton snapshot list ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'name,state'; +var SORT_DEFAULT = 'name'; + + +function do_list(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length !== 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var cli = this.top; + var machineId = args[0]; + + cli.tritonapi.cloudapi.listMachineSnapshots({ + id: machineId + }, function onSnapshots(err, snapshots) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + common.jsonStream(snapshots); + } else { + var columns = COLUMNS_DEFAULT; + + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_DEFAULT; + } + + columns = columns.split(','); + var sort = opts.s.split(','); + + tabula(snapshots, { + skipHeader: false, + columns: columns, + sort: sort + }); + } + cb(); + }); +} + + +do_list.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: SORT_DEFAULT +})); + +do_list.help = [ + 'Show all of a machines\'s snapshots.', + '', + 'Usage:', + ' {{name}} list []', + '', + '{{options}}' +].join('\n'); + +do_list.aliases = ['ls']; + +module.exports = do_list; diff --git a/lib/do_snapshot/index.js b/lib/do_snapshot/index.js new file mode 100644 index 0000000..42ac4a5 --- /dev/null +++ b/lib/do_snapshot/index.js @@ -0,0 +1,48 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2015 Joyent, Inc. + * + * `triton snapshot ...` + */ + +var Cmdln = require('cmdln').Cmdln; +var util = require('util'); + + + +// ---- CLI class + +function SnapshotCLI(top) { + this.top = top; + + Cmdln.call(this, { + name: top.name + ' snapshot', + desc: 'Machine snapshot commands', + helpSubcmds: [ + 'help', + { group: 'Key Resources' }, + 'create', + 'list', + 'get', + 'delete' + ] + }); +} +util.inherits(SnapshotCLI, Cmdln); + +SnapshotCLI.prototype.init = function init(opts, args, cb) { + this.log = this.top.log; + Cmdln.prototype.init.apply(this, arguments); +}; + +SnapshotCLI.prototype.do_create = require('./do_create'); +SnapshotCLI.prototype.do_get = require('./do_get'); +SnapshotCLI.prototype.do_list = require('./do_list'); +SnapshotCLI.prototype.do_delete = require('./do_delete'); + +module.exports = SnapshotCLI; diff --git a/test/integration/cli-fwrules.test.js b/test/integration/cli-fwrules.test.js new file mode 100644 index 0000000..9ece721 --- /dev/null +++ b/test/integration/cli-fwrules.test.js @@ -0,0 +1,197 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2016, Joyent, Inc. + */ + +/* + * Integration tests for `triton fwrules ...` + */ + +var h = require('./helpers'); +var format = require('util').format; +var test = require('tape'); + +// --- Globals + +var DESC = 'This rule was created by node-triton tests'; +var RULE = 'FROM any TO vm $id ALLOW tcp PORT 80'; +var RULE2 = 'FROM any TO vm $id BLOCK tcp port 25'; +var INST; +var ID; + +// --- Tests + +test('triton fwrule', function (tt) { + tt.test('setup', function (t) { + h.triton('insts -j', function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton insts')) + return t.end(); + + var rows = stdout.split('\n'); + INST = JSON.parse(rows[0]).id; + t.ok(INST); + + RULE = RULE.replace('$id', INST); + RULE2 = RULE2.replace('$id', INST); + + t.end(); + }); + }); + + tt.test(' triton fwrule create', function (t) { + var cmd = format('fwrule create -d "%s" "%s"', DESC, RULE); + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule create')) + return t.end(); + + var match = stdout.match('Created firewall rule (.+)'); + t.ok(match, 'fwrule made'); + + ID = match[1]; + t.ok(ID); + + t.end(); + }); + }); + + tt.test(' triton fwrule get', function (t) { + var cmd = 'fwrule get ' + ID; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule get')) + return t.end(); + + var obj = JSON.parse(stdout); + t.equal(obj.rule, RULE, 'fwrule rule is correct'); + t.equal(obj.description, DESC, 'fwrule was properly created'); + t.equal(obj.enabled, false, 'fwrule enabled defaults to false'); + + t.end(); + }); + }); + + tt.test(' triton fwrule enable', function (t) { + var cmd = 'fwrule enable ' + ID; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule enable')) + return t.end(); + + t.ok(stdout.match('Enabled firewall rule ' + ID)); + + t.end(); + }); + }); + + tt.test(' triton fwrule disable', function (t) { + var cmd = 'fwrule disable ' + ID; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule disable')) + return t.end(); + + t.ok(stdout.match('Disabled firewall rule ' + ID)); + + t.end(); + }); + }); + + tt.test(' triton fwrule update', function (t) { + var cmd = 'fwrule update rule="' + RULE2 + '" ' + ID; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule disable')) + return t.end(); + + t.ok(stdout.match('Updated firewall rule ' + ID + + ' \\(fields: rule\\)')); + + t.end(); + }); + }); + + tt.test(' triton fwrule list', function (t) { + h.triton('fwrule list -l', function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule list')) + return t.end(); + + var rules = stdout.split('\n'); + t.ok(rules[0].match(/ID\s+ENABLED\s+GLOBAL\s+RULE\s+DESCRIPTION/)); + rules.shift(); + + t.ok(rules.length >= 1, 'triton fwrule list expected fwrule num'); + + var testRules = rules.filter(function (rule) { + return rule.match(ID); + }); + + t.equal(testRules.length, 1, 'triton fwrule list test rule found'); + + t.end(); + }); + }); + + tt.test(' triton fwrule instances', function (t) { + h.triton('fwrule instances -l ' + ID, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule instances')) + return t.end(); + + var machines = stdout.split('\n').filter(function (machine) { + return machine !== ''; + }); + t.ok(machines[0].match(/ID\s+NAME\s+IMG\s+BRAND/)); + machines.shift(); + + t.equal(machines.length, 1, 'triton fwrule instances expected ' + + 'num machines'); + + var testMachines = machines.filter(function (machine) { + return machine.match(INST); + }); + + t.equal(testMachines.length, 1, 'triton fwrule instances test ' + + 'machine found'); + + t.end(); + }); + }); + + tt.test(' triton instance fwrules', function (t) { + h.triton('instance fwrules -l ' + ID, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule list')) + return t.end(); + + var rules = stdout.split('\n'); + t.ok(rules[0].match(/ID\s+ENABLED\s+GLOBAL\s+RULE\s+DESCRIPTION/)); + rules.shift(); + + t.ok(rules.length >= 1, 'triton fwrule list expected fwrule num'); + + var testRules = rules.filter(function (rule) { + return rule.match(ID); + }); + + t.equal(testRules.length, 1, 'triton fwrule list test rule found'); + + t.end(); + }); + }); + + tt.test(' triton fwrule delete', function (t) { + var cmd = 'fwrule delete ' + ID + ' --force'; + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule delete')) + return t.end(); + + t.ok(stdout.match('Deleted rule ' + ID + ''), 'rule deleted'); + + t.end(); + }); + }); +}); diff --git a/test/integration/cli-snapshots.test.js b/test/integration/cli-snapshots.test.js new file mode 100644 index 0000000..05be1fc --- /dev/null +++ b/test/integration/cli-snapshots.test.js @@ -0,0 +1,103 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2016, Joyent, Inc. + */ + +/* + * Integration tests for `triton snapshot ...` + */ + +var h = require('./helpers'); +var test = require('tape'); + +// --- Globals + +var SNAP_NAME = 'test-snapshot'; +var INST; + +// --- Tests + +test('triton snapshot', function (tt) { + tt.test('setup', function (t) { + h.triton('insts -j', function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton insts')) + return t.end(); + + var rows = stdout.split('\n'); + INST = JSON.parse(rows[0]).id; + t.ok(INST); + + t.end(); + }); + }); + + tt.test(' triton snapshot create', function (t) { + var cmd = 'snapshot create -w -n ' + SNAP_NAME + ' ' + INST; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton snapshot create')) + return t.end(); + + t.ok(stdout.match('Created snapshot "' + SNAP_NAME + '" in \\d+'), + 'snapshot made'); + + t.end(); + }); + }); + + tt.test(' triton snapshot get', function (t) { + var cmd = 'snapshot get ' + INST + ' ' + SNAP_NAME; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton snapshot get')) + return t.end(); + + var obj = JSON.parse(stdout); + t.equal(obj.name, SNAP_NAME, 'snapshot name is correct'); + t.equal(obj.state, 'created', 'snapshot was properly created'); + + t.end(); + }); + }); + + tt.test(' triton snapshot list', function (t) { + h.triton('snapshot list ' + INST, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton snapshot list')) + return t.end(); + + var snaps = stdout.split('\n'); + t.ok(snaps[0].match(/NAME\s+STATE/)); + snaps.shift(); + + t.ok(snaps.length >= 1, 'triton snap list expected snap num'); + + var testSnaps = snaps.filter(function (snap) { + return snap.match(SNAP_NAME); + }); + + t.equal(testSnaps.length, 1, 'triton snap list test snap found'); + + t.end(); + }); + }); + + tt.test(' triton snapshot delete', function (t) { + var cmd = 'snapshot delete ' + INST + ' ' + SNAP_NAME + ' -w --force'; + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton snapshot delete')) + return t.end(); + + t.ok(stdout.match('Deleting snapshot "' + SNAP_NAME + '"', + 'deleting snapshot')); + t.ok(stdout.match('Deleted snapshot "' + SNAP_NAME + '" in \\d+s', + 'deleted snapshot')); + + t.end(); + }); + }); +}); diff --git a/test/integration/cli-subcommands.test.js b/test/integration/cli-subcommands.test.js index 189b485..44f0e59 100644 --- a/test/integration/cli-subcommands.test.js +++ b/test/integration/cli-subcommands.test.js @@ -43,6 +43,7 @@ var subs = [ ['instance delete', 'instance rm', 'delete', 'rm'], ['instance wait'], ['instance audit'], + ['instance fwrules'], ['ssh'], ['network'], ['network list', 'networks'], @@ -58,6 +59,20 @@ var subs = [ ['package', 'pkg'], ['package get'], ['package list', 'packages', 'pkgs'], + ['snapshot'], + ['snapshot create'], + ['snapshot list', 'snapshot ls'], + ['snapshot get'], + ['snapshot delete', 'snapshot rm'], + ['fwrule'], + ['fwrule create'], + ['fwrule list', 'fwrule ls'], + ['fwrule get'], + ['fwrule update'], + ['fwrule delete', 'fwrule rm'], + ['fwrule enable'], + ['fwrule disable'], + ['fwrule instances', 'fwrule insts'], ['rbac'], ['rbac info'], ['rbac apply'],