From 634350018af646b8d37d39925793b94b95be50f8 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Fri, 5 Feb 2016 00:39:50 +1100 Subject: [PATCH] PUBAPI-1233/PUBAPI-1234 Add firewalls and snapshots to node-triton triton fwrule ... triton instance snapshot ... Reviewed-By: Trent Mick (with some contributions) --- CHANGES.md | 4 +- etc/triton-bash-completion-types.sh | 7 + lib/cli.js | 6 +- lib/cloudapi2.js | 429 ++++++++++++++++- lib/common.js | 3 +- lib/do_completion.js | 9 +- lib/do_fwrule/do_create.js | 99 ++++ lib/do_fwrule/do_delete.js | 110 +++++ lib/do_fwrule/do_disable.js | 68 +++ lib/do_fwrule/do_enable.js | 68 +++ lib/do_fwrule/do_get.js | 76 +++ lib/do_fwrule/do_instances.js | 162 +++++++ lib/do_fwrule/do_list.js | 98 ++++ lib/do_fwrule/do_update.js | 190 ++++++++ lib/do_fwrule/index.js | 59 +++ lib/do_image/index.js | 2 +- lib/do_instance/do_fwrules.js | 103 ++++ lib/do_instance/do_snapshot/do_create.js | 149 ++++++ lib/do_instance/do_snapshot/do_delete.js | 164 +++++++ lib/do_instance/do_snapshot/do_get.js | 80 ++++ lib/do_instance/do_snapshot/do_list.js | 98 ++++ lib/do_instance/do_snapshot/index.js | 49 ++ lib/do_instance/do_snapshots.js | 26 ++ lib/do_instance/gen_do_ACTION.js | 18 +- lib/do_instance/index.js | 7 +- lib/do_network/index.js | 2 +- lib/tritonapi.js | 465 ++++++++++++++++++- package.json | 2 +- test/integration/cli-fwrules.test.js | 261 +++++++++++ test/integration/cli-instance-tag.test.js | 26 +- test/integration/cli-manage-workflow.test.js | 26 +- test/integration/cli-snapshots.test.js | 149 ++++++ test/integration/cli-subcommands.test.js | 15 + test/integration/helpers.js | 67 +++ 34 files changed, 3032 insertions(+), 65 deletions(-) create mode 100644 lib/do_fwrule/do_create.js create mode 100644 lib/do_fwrule/do_delete.js create mode 100644 lib/do_fwrule/do_disable.js create mode 100644 lib/do_fwrule/do_enable.js create mode 100644 lib/do_fwrule/do_get.js create mode 100644 lib/do_fwrule/do_instances.js create mode 100644 lib/do_fwrule/do_list.js create mode 100644 lib/do_fwrule/do_update.js create mode 100644 lib/do_fwrule/index.js create mode 100644 lib/do_instance/do_fwrules.js create mode 100644 lib/do_instance/do_snapshot/do_create.js create mode 100644 lib/do_instance/do_snapshot/do_delete.js create mode 100644 lib/do_instance/do_snapshot/do_get.js create mode 100644 lib/do_instance/do_snapshot/do_list.js create mode 100644 lib/do_instance/do_snapshot/index.js create mode 100644 lib/do_instance/do_snapshots.js create mode 100644 test/integration/cli-fwrules.test.js create mode 100644 test/integration/cli-snapshots.test.js diff --git a/CHANGES.md b/CHANGES.md index 1025c03..687f089 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,9 @@ # node-triton changelog -## 4.5.3 (not yet released) +## 4.6.0 (not yet released) +- PUBAPI-1233 firewalls: `triton fwrule ...` +- PUBAPI-1234 instance snapshots: `triton inst snapshot ...` - #52 Fix 'triton ssh ...' stdout/stderr to fully flush with node >= 4.x. diff --git a/etc/triton-bash-completion-types.sh b/etc/triton-bash-completion-types.sh index 2ae1e7f..4cc99fd 100644 --- a/etc/triton-bash-completion-types.sh +++ b/etc/triton-bash-completion-types.sh @@ -12,4 +12,11 @@ function complete_tritonupdateaccountfield { local candidates candidates="{{UPDATE_ACCOUNT_FIELDS}}" compgen $compgen_opts -W "$candidates" -- "$word" +} + +function complete_tritonupdatefwrulefield { + local word="$1" + local candidates + candidates="{{UPDATE_FWRULE_FIELDS}}" + compgen $compgen_opts -W "$candidates" -- "$word" } \ No newline at end of file diff --git a/lib/cli.js b/lib/cli.js index 49a1409..25124b6 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -217,10 +217,11 @@ function CLI() { 'stop', 'reboot', 'ssh', - { group: 'Images, Packages, Networks' }, + { group: 'Images, Packages, Networks, Firewall Rules' }, 'image', 'package', 'network', + 'fwrule', { group: 'Other Commands' }, 'info', 'account', @@ -354,6 +355,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'); diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index e7f98f0..8ae9909 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.enableMachineFirewall = +function enableMachineFirewall(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.disableMachineFirewall = +function disableMachineFirewall(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; @@ -1010,6 +1033,26 @@ CloudApi.prototype.replaceMachineTags = function replaceMachineTags(opts, cb) { }); }; +/** + * + * + * @param {Object} opts: + * - @param {UUID} id: The machine UUID. Required. + * @param {Function} cb - `function (err, res)` + */ +CloudApi.prototype.deleteMachineTags = function deleteMachineTags(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/machines/%s/tags', this.account, opts.id) + }, function (err, req, res) { + cb(err, res); + }); +}; + /** * * @@ -1034,27 +1077,399 @@ CloudApi.prototype.deleteMachineTag = function deleteMachineTag(opts, cb) { }); }; + +// --- snapshots + /** - * + * Creates a new snapshot for a given machine. * - * @param {Object} opts: - * - @param {UUID} id: The machine UUID. Required. - * @param {Function} cb - `function (err, res)` + * 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.deleteMachineTags = function deleteMachineTags(opts, cb) { +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'); + + var data = {}; + if (opts.name) + data.name = opts.name; + + this._request({ + method: 'POST', + path: format('/%s/machines/%s/snapshots', this.account, opts.id), + data: data + }, 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, + encodeURIComponent(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). + */ +CloudApi.prototype.startMachineFromSnapshot = +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/machines/%s/snapshots/%s', this.account, opts.id, + encodeURIComponent(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, + encodeURIComponent(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. + * - {String} description (optional) + * @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'); + + var data = {}; + Object.keys(this.UPDATE_FWRULE_FIELDS).forEach(function (attr) { + if (opts[attr] !== undefined) + data[attr] = opts[attr]; + }); + + this._request({ + method: 'POST', + path: format('/%s/fwrules', this.account), + data: data + }, 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 {UUID} id: The firewall rule 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_FWRULE_FIELDS = { + enabled: 'boolean', + rule: 'string', + description: 'string' +}; + + +/** + * Updates a Firewall Rule. + * + * Dev Note: That 'rule' is *required* here is lame. Hoping to change that + * in cloudapi. + * + * @param {Object} opts object containing: + * - {UUID} id: The fwrule id. Required. + * - {String} rule: The fwrule text. Required. + * - {Boolean} enabled: Optional. + * - {String} description: Description of the rule. Optional. + * @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.optionalString(opts.description, 'opts.description'); + assert.func(cb, 'cb'); + + var data = {}; + Object.keys(this.UPDATE_FWRULE_FIELDS).forEach(function (attr) { + if (opts[attr] !== undefined) + data[attr] = opts[attr]; + }); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s', this.account, opts.id), + data: data + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Enable a Firewall Rule. + * + * @param {Object} opts + * - {UUID} id: The firewall id. Required. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.enableFirewallRule = +function enableFirewallRule(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s/enable', this.account, opts.id), + data: {} + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Disable a Firewall Rule. + * + * @param {Object} opts + * - {UUID} id: The firewall id. Required. + * @param {Function} callback of the form `function (err, fwrule, res)` + */ +CloudApi.prototype.disableFirewallRule = +function disableFirewallRule(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/fwrules/%s/disable', this.account, opts.id), + data: {} + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * Remove a Firewall Rule. + * + * @param {Object} opts (object) + * - {UUID} id: The firewall id. Required. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deleteFirewallRule = +function deleteFirewallRule(opts, cb) { assert.object(opts, 'opts'); assert.uuid(opts.id, 'opts.id'); assert.func(cb, 'cb'); this._request({ method: 'DELETE', - path: format('/%s/machines/%s/tags', this.account, opts.id) + 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/common.js b/lib/common.js index b5af9c7..54a53fc 100644 --- a/lib/common.js +++ b/lib/common.js @@ -314,8 +314,7 @@ function uuidToShortId(s) { * * Short IDs: * - UUID prefix - * - allow '-' to be elided (to support using containers IDs from - * docker) + * - allow '-' to be elided (to support using containers IDs from docker) * - support docker ID *longer* than a UUID? The curr implementation does. */ function normShortId(s) { diff --git a/lib/do_completion.js b/lib/do_completion.js index f6426da..9655292 100644 --- a/lib/do_completion.js +++ b/lib/do_completion.js @@ -13,8 +13,9 @@ var fs = require('fs'); var path = require('path'); -var UPDATE_ACCOUNT_FIELDS - = require('./cloudapi2').CloudApi.prototype.UPDATE_ACCOUNT_FIELDS; +var CloudApi = require('./cloudapi2').CloudApi; +var UPDATE_ACCOUNT_FIELDS = CloudApi.prototype.UPDATE_ACCOUNT_FIELDS; +var UPDATE_FWRULE_FIELDS = CloudApi.prototype.UPDATE_FWRULE_FIELDS; // Replace {{variable}} in `s` with the template data in `d`. @@ -39,6 +40,8 @@ function do_completion(subcmd, opts, args, cb) { 'utf8'); var specExtra = renderTemplate(specExtraIn, { UPDATE_ACCOUNT_FIELDS: Object.keys(UPDATE_ACCOUNT_FIELDS).sort() + .map(function (field) { return field + '='; }).join(' '), + UPDATE_FWRULE_FIELDS: Object.keys(UPDATE_FWRULE_FIELDS).sort() .map(function (field) { return field + '='; }).join(' ') }); console.log(this.bashCompletion({specExtra: specExtra})); @@ -61,7 +64,7 @@ do_completion.options = [ } ]; do_completion.help = [ - 'Output bash completion. See help output for installation.', + 'Emit bash completion. See help for installation.', '', 'Installation:', ' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} # Mac', diff --git a/lib/do_fwrule/do_create.js b/lib/do_fwrule/do_create.js new file mode 100644 index 0000000..cc24858 --- /dev/null +++ b/lib/do_fwrule/do_create.js @@ -0,0 +1,99 @@ +/* + * 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 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.disabled, 'opts.disabled'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + if (args.length === 0) { + cb(new errors.UsageError('missing argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var createOpts = { + rule: args[0] + }; + if (!opts.disabled) { + createOpts.enabled = true; + } + if (opts.description) { + createOpts.description = opts.description; + } + + this.top.tritonapi.cloudapi.createFirewallRule(createOpts, + function (err, fwrule) { + if (err) { + cb(err); + return; + } + console.log('Created firewall rule %s%s', fwrule.id, + (!fwrule.enabled ? ' (disabled)' : '')); + cb(); + }); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['disabled', 'd'], + type: 'bool', + help: 'Disable the created firewall rule. By default a created ' + + 'firewall rule is enabled. Use "triton fwrule enable" ' + + 'to enable it later.' + }, + { + names: ['description', 'D'], + type: 'string', + helpArg: '', + help: 'Description of the firewall rule.' + } +]; +do_create.help = [ + 'Create a firewall rule.', + '', + 'Usage:', + ' {{name}} create [] ', + '', + '{{options}}' +].join('\n'); + +do_create.helpOpts = { + helpCol: 25 +}; + +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..29814ec --- /dev/null +++ b/lib/do_fwrule/do_delete.js @@ -0,0 +1,110 @@ +/* + * 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 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 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.forEachParallel({ + inputs: ruleIds, + func: function deleteOne(id, nextId) { + cli.tritonapi.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 [] [...]', + '', + '{{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..9afa8de --- /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 vasync = require('vasync'); + +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 argument(s)')); + return; + } + + var cli = this.top; + + vasync.forEachParallel({ + inputs: args, + func: function disableOne(id, nextId) { + cli.tritonapi.disableFirewallRule({ id: id }, function (err) { + if (err) { + nextId(err); + return; + } + + console.log('Disabled firewall rule %s', id); + nextId(); + }); + } + }, cb); +} + + +do_disable.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; +do_disable.help = [ + 'Disable a specific firewall rule.', + '', + 'Usage:', + ' {{name}} disable [...]', + '', + '{{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..0cd7306 --- /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 vasync = require('vasync'); + +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 argument(s)')); + return; + } + + var cli = this.top; + + vasync.forEachParallel({ + inputs: args, + func: function enableOne(id, nextId) { + cli.tritonapi.enableFirewallRule({ id: id }, function (err) { + if (err) { + nextId(err); + return; + } + + console.log('Enabled firewall rule %s', id); + nextId(); + }); + } + }, cb); +} + + +do_enable.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; +do_enable.help = [ + 'Enable a specific firewall rule.', + '', + 'Usage:', + ' {{name}} enable [...]', + '', + '{{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..ac0db39 --- /dev/null +++ b/lib/do_fwrule/do_get.js @@ -0,0 +1,76 @@ +/* + * 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) { + cb(new errors.UsageError('missing argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var id = args[0]; + var cli = this.top; + + cli.tritonapi.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 ', + '', + '{{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..627a481 --- /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 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.listFirewallRuleInstances({ + 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 [] ', + '', + '{{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..daec3e4 --- /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.uuidToShortId(rule.id); + }); + } + + tabula(rules, { + skipHeader: opts.H, + 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..e9c62e0 --- /dev/null +++ b/lib/do_fwrule/do_update.js @@ -0,0 +1,190 @@ +/* + * 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_FWRULE_FIELDS + = require('../cloudapi2').CloudApi.prototype.UPDATE_FWRULE_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 argument')); + return; + } + + var id = args.shift(); + + vasync.pipeline({arg: {}, funcs: [ + function gatherDataArgs(ctx, next) { + if (opts.file) { + next(); + return; + } + + try { + ctx.data = common.objFromKeyValueArgs(args, { + disableDotted: true, + typeHintFromKey: UPDATE_FWRULE_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); + + if (keys.length === 0) { + console.log('No fields given for firewall rule update'); + next(); + return; + } + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = ctx.data[key]; + var type = UPDATE_FWRULE_FIELDS[key]; + if (!type) { + next(new errors.UsageError(format('unknown or ' + + 'unupdateable field: %s (updateable fields are: %s)', + key, + Object.keys(UPDATE_FWRULE_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 data = ctx.data; + data.id = id; + + tritonapi.updateFirewallRule(data, function (err) { + if (err) { + next(err); + return; + } + + delete data.id; + console.log('Updated firewall rule %s (fields: %s)', id, + Object.keys(data).join(', ')); + + next(); + }); + } + ]}, cb); +} + +do_update.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['file', 'f'], + type: 'string', + helpArg: '', + 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 ...]', + ' {{name}} update -f ', + '', + '{{options}}', + + 'Updateable fields:', + ' ' + Object.keys(UPDATE_FWRULE_FIELDS).sort().map(function (f) { + return f + ' (' + UPDATE_FWRULE_FIELDS[f] + ')'; + }).join('\n '), + '' +].join('\n'); + +do_update.completionArgtypes = ['tritonfwrule', '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..2b722d9 --- /dev/null +++ b/lib/do_fwrule/index.js @@ -0,0 +1,59 @@ +/* + * 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 ...` + */ + +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: 'List and manage Triton firewall rules.', + helpSubcmds: [ + 'help', + 'list', + 'get', + 'create', + 'update', + 'delete', + { group: '' }, + 'enable', + 'disable', + 'instances' + ], + helpOpts: { + minHelpCol: 23 + } + }); +} +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_image/index.js b/lib/do_image/index.js index 40d0807..35fb759 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -23,7 +23,7 @@ function ImageCLI(top) { name: top.name + ' image', /* BEGIN JSSTYLED */ desc: [ - 'List, get, create and manage Triton images.' + 'List and manage Triton images.' ].join('\n'), /* END JSSTYLED */ helpOpts: { diff --git a/lib/do_instance/do_fwrules.js b/lib/do_instance/do_fwrules.js new file mode 100644 index 0000000..327bcae --- /dev/null +++ b/lib/do_instance/do_fwrules.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 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 argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var id = args[0]; + + var cli = this.top; + cli.tritonapi.listInstanceFirewallRules({ + id: id + }, 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: opts.H, + 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 [] ', + '', + '{{options}}' +].join('\n'); + +module.exports = do_fwrules; diff --git a/lib/do_instance/do_snapshot/do_create.js b/lib/do_instance/do_snapshot/do_create.js new file mode 100644 index 0000000..469e6b9 --- /dev/null +++ b/lib/do_instance/do_snapshot/do_create.js @@ -0,0 +1,149 @@ +/* + * 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 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.createInstanceSnapshot(createOpts, + function (err, snapshot, res) { + if (err) { + next(err); + return; + } + + console.log('Creating snapshot %s of instance %s', + snapshot.name, createOpts.id); + ctx.name = snapshot.name; + ctx.instId = res.instId; + + 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: ctx.instId, + 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: '', + 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 an instance.', + '', + 'Usage:', + ' {{name}} create [] ', + '', + '{{options}}', + 'Snapshot do not work for instances of type "kvm".' +].join('\n'); + +module.exports = do_create; diff --git a/lib/do_instance/do_snapshot/do_delete.js b/lib/do_instance/do_snapshot/do_delete.js new file mode 100644 index 0000000..e0f0453 --- /dev/null +++ b/lib/do_instance/do_snapshot/do_delete.js @@ -0,0 +1,164 @@ +/* + * 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 and argument(s)')); + return; + } + + var cli = this.top; + var inst = args[0]; + var names = args.slice(1, args.length); + + function wait(instId, 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: instId, + 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.deleteInstanceSnapshot({ + id: inst, + name: name + }, function (err, res) { + if (err) { + nextName(err); + return; + } + + var instId = res.instId; + + var msg = 'Deleting snapshot "%s" of instance "%s"'; + console.log(msg, name, instId); + + if (opts.wait) { + wait(instId, 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 an instance.', + '', + 'Usage:', + ' {{name}} delete [] [...]', + '', + '{{options}}' +].join('\n'); + +do_delete.aliases = ['rm']; + +module.exports = do_delete; diff --git a/lib/do_instance/do_snapshot/do_get.js b/lib/do_instance/do_snapshot/do_get.js new file mode 100644 index 0000000..102f060 --- /dev/null +++ b/lib/do_instance/do_snapshot/do_get.js @@ -0,0 +1,80 @@ +/* + * 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) { + cb(new errors.UsageError('missing and/or arguments')); + 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.getInstanceSnapshot({ + 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 an instance.', + '', + 'Usage:', + ' {{name}} get ', + '', + '{{options}}' +].join('\n'); + +module.exports = do_get; diff --git a/lib/do_instance/do_snapshot/do_list.js b/lib/do_instance/do_snapshot/do_list.js new file mode 100644 index 0000000..e25adc2 --- /dev/null +++ b/lib/do_instance/do_snapshot/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 snapshot list ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../../common'); +var errors = require('../../errors'); + + +var COLUMNS_DEFAULT = 'name,state,created'; +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 === 0) { + cb(new errors.UsageError('missing argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var cli = this.top; + var machineId = args[0]; + + cli.tritonapi.listInstanceSnapshots({ + 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: opts.H, + 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 an instance\'s snapshots.', + '', + 'Usage:', + ' {{name}} list [] ', + '', + '{{options}}' +].join('\n'); + +do_list.aliases = ['ls']; + +module.exports = do_list; diff --git a/lib/do_instance/do_snapshot/index.js b/lib/do_instance/do_snapshot/index.js new file mode 100644 index 0000000..471dc6f --- /dev/null +++ b/lib/do_instance/do_snapshot/index.js @@ -0,0 +1,49 @@ +/* + * 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 SnapshotCLI(top) { + this.top = top.top; + + Cmdln.call(this, { + name: top.name + ' snapshot', + desc: 'List, get, create and delete Triton instance snapshots.', + helpSubcmds: [ + 'help', + 'create', + 'list', + 'get', + 'delete' + ], + helpBody: 'Instances can be rolled back to a snapshot using\n' + + '`triton instance start --snapshot=`.' + }); +} +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/lib/do_instance/do_snapshots.js b/lib/do_instance/do_snapshots.js new file mode 100644 index 0000000..021b081 --- /dev/null +++ b/lib/do_instance/do_snapshots.js @@ -0,0 +1,26 @@ +/* + * 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 snapshots ...` shortcut for + * `triton instance snapshot list ...`. + */ + +function do_snapshots(subcmd, opts, args, callback) { + this.handlerFromSubcmd('snapshot').dispatch({ + subcmd: 'list', + opts: opts, + args: args + }, callback); +} + +do_snapshots.help = 'A shortcut for "triton instance snapshot list".'; +do_snapshots.options = require('./do_snapshot/do_list').options; +do_snapshots.hidden = true; + +module.exports = do_snapshots; diff --git a/lib/do_instance/gen_do_ACTION.js b/lib/do_instance/gen_do_ACTION.js index f813e6d..b418be0 100644 --- a/lib/do_instance/gen_do_ACTION.js +++ b/lib/do_instance/gen_do_ACTION.js @@ -66,6 +66,14 @@ function gen_do_ACTION(opts) { } ]; + if (action === 'start') { + do_ACTION.options.push({ + names: ['snapshot'], + type: 'string', + help: 'Name of snapshot to start machine with.' + }); + } + return do_ACTION; } @@ -77,7 +85,8 @@ function _doTheAction(action, subcmd, opts, args, callback) { var command, state; switch (action) { case 'start': - command = 'startMachine'; + command = opts.snapshot ? 'startMachineFromSnapshot' : + 'startMachine'; state = 'running'; break; case 'stop': @@ -126,7 +135,12 @@ function _doTheAction(action, subcmd, opts, args, callback) { // called when "uuid" is set function done() { - self.top.tritonapi.cloudapi[command](uuid, + var cOpts = uuid; + if (command === 'startMachineFromSnapshot') { + cOpts = { id: uuid, name: opts.snapshot }; + } + + self.top.tritonapi.cloudapi[command](cOpts, function (err, body, res) { if (err) { diff --git a/lib/do_instance/index.js b/lib/do_instance/index.js index d5a2193..0484ebe 100644 --- a/lib/do_instance/index.js +++ b/lib/do_instance/index.js @@ -22,7 +22,7 @@ function InstanceCLI(top) { name: top.name + ' instance', /* BEGIN JSSTYLED */ desc: [ - 'List, get, create and manage Triton instances.' + 'List and manage Triton instances.' ].join('\n'), /* END JSSTYLED */ helpOpts: { @@ -42,6 +42,8 @@ function InstanceCLI(top) { 'ssh', 'wait', 'audit', + 'fwrules', + 'snapshot', 'tag' ] }); @@ -65,6 +67,9 @@ 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.prototype.do_snapshot = require('./do_snapshot'); +InstanceCLI.prototype.do_snapshots = require('./do_snapshots'); InstanceCLI.prototype.do_tag = require('./do_tag'); InstanceCLI.prototype.do_tags = require('./do_tags'); diff --git a/lib/do_network/index.js b/lib/do_network/index.js index 5d01012..e22c576 100644 --- a/lib/do_network/index.js +++ b/lib/do_network/index.js @@ -23,7 +23,7 @@ function NetworkCLI(top) { name: top.name + ' network', /* BEGIN JSSTYLED */ desc: [ - 'List, get, create and update Triton networks.' + 'List and manage Triton networks.' ].join('\n'), /* END JSSTYLED */ helpOpts: { diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 9712caa..3021934 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -86,6 +86,31 @@ function _stepInstId(arg, next) { } } +/** + * A function appropriate for `vasync.pipeline` funcs that takes a `arg.id` + * fwrule shortid or uuid, and determines the fwrule id (setting it + * as `arg.fwruleId`). + * + * If the fwrule *was* retrieved, that is set as `arg.fwrule`. + */ +function _stepFwRuleId(arg, next) { + assert.object(arg.client, 'arg.client'); + assert.string(arg.id, 'arg.id'); + + if (common.isUUID(arg.id)) { + arg.fwruleId = arg.id; + next(); + } else { + arg.client.getFirewallRule(arg.id, function (err, fwrule) { + if (err) { + next(err); + } else { + arg.fwruleId = fwrule.id; + next(); + } + }); + } +} //---- TritonApi class @@ -256,7 +281,7 @@ TritonApi.prototype._cacheGetJson = function _cacheGetJson(key, cb) { * * @param opts {Object} Optional. * - useCache {Boolean} Default false. Whether to use Triton's local cache. - * Note that the *currently* implementation will only use the cache + * Note that the *current* implementation will only use the cache * when there are no filter options. * - ... all other cloudapi ListImages options per * @@ -735,6 +760,157 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { }; +// ---- instance snapshots + +/** + * Create a snapshot of an instance. + * + * @param {Object} opts + * - {String} id: The instance ID, name, or short ID. Required. + * - {String} name: The name for new snapshot. Optional. + * @param {Function} callback `function (err, snapshots, res)` + */ +TritonApi.prototype.createInstanceSnapshot = +function createInstanceSnapshot(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.optionalString(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var snapshot; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + + function createSnapshot(arg, next) { + self.cloudapi.createMachineSnapshot({ + id: arg.instId, + name: opts.name + }, function (err, snap, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + snapshot = snap; + next(err); + }); + } + ]}, function (err) { + cb(err, snapshot, res); + }); +}; + + +/** + * List an instance's snapshots. + * + * @param {Object} opts + * - {String} id: The instance ID, name, or short ID. Required. + * @param {Function} callback `function (err, snapshots, res)` + */ +TritonApi.prototype.listInstanceSnapshots = +function listInstanceSnapshots(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var snapshots; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + + function listSnapshots(arg, next) { + self.cloudapi.listMachineSnapshots({ + id: arg.instId, + name: opts.name + }, function (err, snaps, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + snapshots = snaps; + next(err); + }); + } + ]}, function (err) { + cb(err, snapshots, res); + }); +}; + + +/** + * Get an instance's snapshot. + * + * @param {Object} opts + * - {String} id: The instance ID, name, or short ID. Required. + * - {String} name: The name of the snapshot. Required. + * @param {Function} callback `function (err, snapshot, res)` + */ +TritonApi.prototype.getInstanceSnapshot = +function getInstanceSnapshot(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var snapshot; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + + function getSnapshot(arg, next) { + self.cloudapi.getMachineSnapshot({ + id: arg.instId, + name: opts.name + }, function (err, _snap, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + snapshot = _snap; + next(err); + }); + } + ]}, function (err) { + cb(err, snapshot, res); + }); +}; + + +/** + * Delete an instance's snapshot. + * + * @param {Object} opts + * - {String} id: The instance ID, name, or short ID. Required. + * - {String} name: The name of the snapshot. Required. + * @param {Function} callback `function (err, res)` + * + */ +TritonApi.prototype.deleteInstanceSnapshot = +function deleteInstanceSnapshot(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.string(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + var self = this; + var res; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + + function deleteSnapshot(arg, next) { + self.cloudapi.deleteMachineSnapshot({ + id: arg.instId, + name: opts.name + }, function (err, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + next(err); + }); + } + ]}, function (err) { + cb(err, res); + }); +}; + + // ---- instance tags /** @@ -1249,6 +1425,293 @@ function deleteAllInstanceTags(opts, cb) { }; +// ---- Firewall Rules + +/** + * Get a firewall rule by ID, or short ID, in that order. + * + * If there is more than one firewall rule with that short ID, then this errors + * out. + */ +TritonApi.prototype.getFirewallRule = function getFirewallRule(id, cb) { + assert.string(id, 'id'); + assert.func(cb, 'cb'); + + if (common.isUUID(id)) { + this.cloudapi.getFirewallRule(id, function (err, fwrule) { + if (err) { + if (err.restCode === 'ResourceNotFound') { + err = new errors.ResourceNotFoundError(err, + format('firewall rule with id %s was not found', id)); + } + cb(err); + } else { + cb(null, fwrule); + } + }); + } else { + this.cloudapi.listFirewallRules({}, function (err, fwrules) { + if (err) { + return cb(err); + } + + var shortIdMatches = fwrules.filter(function (fwrule) { + return fwrule.id.slice(0, 8) === id; + }); + + if (shortIdMatches.length === 1) { + cb(null, shortIdMatches[0]); + } else if (shortIdMatches.length === 0) { + cb(new errors.ResourceNotFoundError(format( + 'no firewall rule with short id "%s" was found', id))); + } else { + cb(new errors.ResourceNotFoundError( + format('"%s" is an ambiguous short id, with multiple ' + + 'matching firewall rules', id))); + } + }); + } +}; + + +/** + * List all firewall rules affecting an instance. + * + * @param {Object} opts + * - {String} id: The instance ID, name, or short ID. Required. + * @param {Function} callback `function (err, instances, res)` + */ +TritonApi.prototype.listInstanceFirewallRules = +function listInstanceFirewallRules(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var fwrules; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepInstId, + + function listRules(arg, next) { + self.cloudapi.listMachineFirewallRules({ + id: arg.instId + }, function (err, rules, _res) { + res = _res; + fwrules = rules; + next(err); + }); + } + ]}, function (err) { + cb(err, fwrules, res); + }); +}; + + +/** + * List all instances affected by a firewall rule. + * + * @param {Object} opts + * - {String} id: The fwrule ID, or short ID. Required. + * @param {Function} callback `function (err, instances, res)` + */ +TritonApi.prototype.listFirewallRuleInstances = +function listFirewallRuleInstances(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var instances; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepFwRuleId, + + function listInsts(arg, next) { + self.cloudapi.listFirewallRuleMachines({ + id: arg.fwruleId + }, function (err, machines, _res) { + res = _res; + instances = machines; + next(err); + }); + } + ]}, function (err) { + cb(err, instances, res); + }); +}; + + +/** + * Update a firewall rule. + * + * Dev Note: Currently cloudapi UpdateFirewallRule *requires* the 'rule' field, + * which is overkill. `TritonApi.updateFirewallRule` adds sugar by making + * 'rule' optional. + * + * @param {Object} opts + * - {String} id: The fwrule ID, or short ID. Required. + * - {String} rule: The fwrule text. Optional. + * - {Boolean} enabled: Default to false. Optional. + * - {String} description: Description of the rule. Optional. + * At least one of the fields must be provided. + * @param {Function} callback `function (err, fwrule, res)` + */ +TritonApi.prototype.updateFirewallRule = function updateFirewallRule(opts, cb) { + // TODO: strict opts field validation + assert.string(opts.id, 'opts.id'); + assert.optionalString(opts.rule, 'opts.rule'); + assert.optionalBool(opts.enabled, 'opts.enabled'); + assert.optionalString(opts.description, 'opts.description'); + assert.ok(opts.rule !== undefined || opts.enabled !== undefined || + opts.description !== undefined, 'at least one of opts.rule, ' + + 'opts.enabled, or opts.description is required'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var updatedFwrule; + var updateOpts = common.objCopy(opts); + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepFwRuleId, + + /* + * CloudAPI currently requires the 'rule' field. We provide sugar here + * and fill it in for you. + */ + function sugarFillRuleField(arg, next) { + if (updateOpts.rule) { + next(); + } else if (arg.fwrule) { + updateOpts.rule = arg.fwrule.rule; + next(); + } else { + self.getFirewallRule(arg.fwruleId, function (err, fwrule) { + if (err) { + next(err); + } else { + updateOpts.rule = fwrule.rule; + next(); + } + }); + } + }, + + function updateRule(arg, next) { + updateOpts.id = arg.fwruleId; + self.cloudapi.updateFirewallRule(updateOpts, + function (err, fwrule, res_) { + res = res_; + updatedFwrule = fwrule; + next(err); + }); + } + ]}, function (err) { + cb(err, updatedFwrule, res); + }); +}; + + +/** + * Enable a firewall rule. + * + * @param {Object} opts + * - {String} id: The fwrule ID, or short ID. Required. + * @param {Function} callback `function (err, fwrule, res)` + */ +TritonApi.prototype.enableFirewallRule = function enableFirewallRule(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var fwrule; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepFwRuleId, + + function enableRule(arg, next) { + self.cloudapi.enableFirewallRule({ + id: arg.fwruleId + }, function (err, rule, _res) { + res = _res; + fwrule = rule; + next(err); + }); + } + ]}, function (err) { + cb(err, fwrule, res); + }); +}; + + +/** + * Disable a firewall rule. + * + * @param {Object} opts + * - {String} id: The fwrule ID, or short ID. Required. + * @param {Function} callback `function (err, fwrule, res)` + */ +TritonApi.prototype.disableFirewallRule = +function disableFirewallRule(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var fwrule; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepFwRuleId, + + function disableRule(arg, next) { + self.cloudapi.disableFirewallRule({ + id: arg.fwruleId + }, function (err, rule, _res) { + res = _res; + fwrule = rule; + next(err); + }); + } + ]}, function (err) { + cb(err, fwrule, res); + }); +}; + + +/** + * Delete a firewall rule. + * + * @param {Object} opts + * - {String} id: The fwrule ID, or short ID. Required. + * @param {Function} callback `function (err, res)` + * + */ +TritonApi.prototype.deleteFirewallRule = function deleteFirewallRule(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var res; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepFwRuleId, + + function deleteRule(arg, next) { + self.cloudapi.deleteFirewallRule({ + id: arg.fwruleId + }, function (err, _res) { + res = _res; + next(err); + }); + } + ]}, function (err) { + cb(err, res); + }); +}; + + // ---- RBAC /** diff --git a/package.json b/package.json index c16377d..6f6bac7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "4.5.3", + "version": "4.6.0", "author": "Joyent (joyent.com)", "dependencies": { "assert-plus": "0.2.0", diff --git a/test/integration/cli-fwrules.test.js b/test/integration/cli-fwrules.test.js new file mode 100644 index 0000000..c1b2e0f --- /dev/null +++ b/test/integration/cli-fwrules.test.js @@ -0,0 +1,261 @@ +/* + * 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 f = require('util').format; +var os = require('os'); +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; +var INST_ALIAS = f('nodetritontest-fwrules-%s', os.hostname()); +var OPTS = { + skip: !h.CONFIG.allowWriteActions +}; + +// --- Tests + +if (OPTS.skip) { + console.error('** skipping %s tests', __filename); + console.error('** set "allowWriteActions" in test config to enable'); +} + +test('triton fwrule', OPTS, function (tt) { + h.printConfig(tt); + + tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) { + h.deleteTestInst(t, INST_ALIAS, function (err) { + t.ifErr(err); + t.end(); + }); + }); + + tt.test(' setup: triton create', function (t) { + h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) { + if (h.ifErr(t, err2, 'triton instance create')) + return t.end(); + + INST = instId; + RULE = RULE.replace('$id', INST); + RULE2 = RULE2.replace('$id', INST); + + t.end(); + }); + }); + + tt.test(' triton fwrule create --disabled', function (t) { + var cmd = f('fwrule create -d "%s"', RULE); + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule create --disabled')) + return t.end(); + /* JSSTYLED */ + var expected = /^Created firewall rule ([a-f0-9-]{36}) \(disabled\)$/m; + var match = expected.exec(stdout); + t.ok(match, f('stdout matches %s: %j', expected, stdout)); + + var id = match[1]; + t.ok(id); + ID = id.match(/^(.+?)-/)[1]; // convert to short ID + + t.end(); + }); + }); + + tt.test(' triton fwrule get (disabled)', 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.enabled, false, 'fwrule is disabled'); + t.end(); + }); + }); + + tt.test(' triton fwrule create', function (t) { + var cmd = f('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(); + + /* JSSTYLED */ + var expected = /^Created firewall rule ([a-f0-9-]{36})$/m; + var match = expected.exec(stdout); + t.ok(match, f('stdout matches %s: %j', expected, stdout)); + + var id = match[1]; + t.ok(id); + ID = id.match(/^(.+?)-/)[1]; // convert to short 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, true, 'fwrule enabled defaults to true'); + 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 ' + ID + ' rule="' + RULE2 + '"'; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton fwrule update')) + 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(); + + if (!INST) + return t.end(); + + 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 ' + INST, 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(); + }); + }); + + /* + * Use a timeout, because '-w' on delete doesn't have a way to know if the + * attempt failed or if it is just taking a really long time. + */ + tt.test(' cleanup: triton rm INST', {timeout: 10 * 60 * 1000}, + function (t) { + h.deleteTestInst(t, INST_ALIAS, function () { + t.end(); + }); + }); +}); diff --git a/test/integration/cli-instance-tag.test.js b/test/integration/cli-instance-tag.test.js index 659468d..283e023 100644 --- a/test/integration/cli-instance-tag.test.js +++ b/test/integration/cli-instance-tag.test.js @@ -39,32 +39,14 @@ if (opts.skip) { console.error('** set "allowWriteActions" in test config to enable'); } test('triton inst tag ...', opts, function (tt) { - tt.comment('Test config:'); - Object.keys(h.CONFIG).forEach(function (key) { - var value = h.CONFIG[key]; - tt.comment(f('- %s: %j', key, value)); - }); + h.printConfig(tt); var inst; tt.test(' cleanup: rm inst ' + INST_ALIAS + ' if exists', function (t) { - h.triton(['inst', 'get', '-j', INST_ALIAS], - function (err, stdout, stderr) { - if (err) { - if (err.code === 3) { // `triton` code for ResourceNotFound - t.ok(true, 'no pre-existing alias in the way'); - t.end(); - } else { - t.ifErr(err); - t.end(); - } - } else { - var oldInst = JSON.parse(stdout); - h.safeTriton(t, ['delete', '-w', oldInst.id], function (dErr) { - t.ifError(dErr, 'deleted old inst ' + oldInst.id); - t.end(); - }); - } + h.deleteTestInst(t, INST_ALIAS, function (err) { + t.ifErr(err); + t.end(); }); }); diff --git a/test/integration/cli-manage-workflow.test.js b/test/integration/cli-manage-workflow.test.js index f617743..cfa25d9 100644 --- a/test/integration/cli-manage-workflow.test.js +++ b/test/integration/cli-manage-workflow.test.js @@ -40,30 +40,12 @@ if (opts.skip) { console.error('** set "allowWriteActions" in test config to enable'); } test('triton manage workflow', opts, function (tt) { - tt.comment('Test config:'); - Object.keys(h.CONFIG).forEach(function (key) { - var value = h.CONFIG[key]; - tt.comment(f('- %s: %j', key, value)); - }); + h.printConfig(tt); tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) { - h.triton(['inst', 'get', '-j', INST_ALIAS], - function (err, stdout, stderr) { - if (err) { - if (err.code === 3) { // `triton` code for ResourceNotFound - t.ok(true, 'no pre-existing alias in the way'); - t.end(); - } else { - t.ifErr(err, err); - t.end(); - } - } else { - var inst = JSON.parse(stdout); - h.safeTriton(t, ['inst', 'rm', '-w', inst.id], function () { - t.ok(true, 'deleted inst ' + inst.id); - t.end(); - }); - } + h.deleteTestInst(t, INST_ALIAS, function (err) { + t.ifErr(err); + t.end(); }); }); diff --git a/test/integration/cli-snapshots.test.js b/test/integration/cli-snapshots.test.js new file mode 100644 index 0000000..47ed0e3 --- /dev/null +++ b/test/integration/cli-snapshots.test.js @@ -0,0 +1,149 @@ +/* + * 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 instance snapshot ...` + */ + +var h = require('./helpers'); +var f = require('util').format; +var os = require('os'); +var test = require('tape'); + +// --- Globals + +var SNAP_NAME = 'test-snapshot'; +var INST_ALIAS = f('nodetritontest-snapshots-%s', os.hostname()); +var INST; +var OPTS = { + skip: !h.CONFIG.allowWriteActions +}; + +// --- Tests + +if (OPTS.skip) { + console.error('** skipping %s tests', __filename); + console.error('** set "allowWriteActions" in test config to enable'); +} + +test('triton instance snapshot', OPTS, function (tt) { + h.printConfig(tt); + + tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) { + h.deleteTestInst(t, INST_ALIAS, function (err) { + t.ifErr(err); + t.end(); + }); + }); + + tt.test(' setup: triton instance create', function (t) { + h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) { + if (h.ifErr(t, err2, 'triton instance create')) + return t.end(); + + INST = instId.match(/^(.+?)-/)[1]; // convert to short ID + + t.end(); + }); + }); + + tt.test(' triton instance snapshot create', function (t) { + var cmd = 'instance snapshot create -w -n ' + SNAP_NAME + ' ' + INST; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance snapshot create')) + return t.end(); + + t.ok(stdout.match('Created snapshot "' + SNAP_NAME + '" in \\d+'), + 'snapshot made'); + + t.end(); + }); + }); + + tt.test(' triton instance snapshot get', function (t) { + var cmd = 'instance snapshot get ' + INST + ' ' + SNAP_NAME; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance 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 instance snapshot list', function (t) { + var cmd = 'instance snapshot list ' + INST; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance snapshot list')) + return t.end(); + + var snaps = stdout.split('\n'); + t.ok(snaps[0].match(/NAME\s+STATE\s+CREATED/)); + 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 instance start --snapshot', function (t) { + var cmd = 'instance start ' + INST + ' -w --snapshot=' + SNAP_NAME; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance start --snapshot')) + return t.end(); + + t.ok(stdout.match('Start instance ' + INST)); + + t.end(); + }); + }); + + tt.test(' triton instance snapshot delete', function (t) { + var cmd = 'instance snapshot delete -w --force ' + INST + ' ' + + SNAP_NAME; + + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance 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(); + }); + }); + + /* + * Use a timeout, because '-w' on delete doesn't have a way to know if the + * attempt failed or if it is just taking a really long time. + */ + tt.test(' cleanup: triton instance rm INST', {timeout: 10 * 60 * 1000}, + function (t) { + h.deleteTestInst(t, INST_ALIAS, function () { + t.end(); + }); + }); +}); diff --git a/test/integration/cli-subcommands.test.js b/test/integration/cli-subcommands.test.js index 189b485..37f167c 100644 --- a/test/integration/cli-subcommands.test.js +++ b/test/integration/cli-subcommands.test.js @@ -43,6 +43,12 @@ var subs = [ ['instance delete', 'instance rm', 'delete', 'rm'], ['instance wait'], ['instance audit'], + ['instance fwrules'], + ['instance snapshot'], + ['instance snapshot create'], + ['instance snapshot list', 'instance snapshot ls', 'instance snapshots'], + ['instance snapshot get'], + ['instance snapshot delete', 'instance snapshot rm'], ['ssh'], ['network'], ['network list', 'networks'], @@ -58,6 +64,15 @@ var subs = [ ['package', 'pkg'], ['package get'], ['package list', 'packages', 'pkgs'], + ['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'], diff --git a/test/integration/helpers.js b/test/integration/helpers.js index fa6f8d4..7218c7d 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -122,6 +122,8 @@ function triton(args, opts, cb) { }, cb); } + + /* * `triton ...` wrapper that: * - tests non-error exit @@ -255,6 +257,68 @@ function createClient() { } +/* + * Create a small test instance. + */ +function createTestInst(t, name, cb) { + getTestPkg(t, function (err, pkgId) { + t.ifErr(err); + + getTestImg(t, function (err2, imgId) { + t.ifErr(err2); + + var cmd = f('instance create -w -n %s %s %s', name, imgId, pkgId); + triton(cmd, function (err3, stdout) { + t.ifErr(err3, 'create test instance'); + + var match = stdout.match(/Created .+? \((.+)\)/); + var inst = match[1]; + + cb(null, inst); + }); + }); + }); +} + + +/* + * Remove test instance, if exists. + */ +function deleteTestInst(t, name, cb) { + triton(['inst', 'get', '-j', name], function (err, stdout, stderr) { + if (err) { + if (err.code === 3) { // `triton` code for ResourceNotFound + t.ok(true, 'no pre-existing alias in the way'); + } else { + t.ifErr(err); + } + + return cb(); + } + + var oldInst = JSON.parse(stdout); + + safeTriton(t, ['delete', '-w', oldInst.id], function (dErr) { + t.ifError(dErr, 'deleted old inst ' + oldInst.id); + cb(); + }); + }); +} + + +/* + * Print out a listing of the test config.json values. + */ +function printConfig(t) { + t.comment('Test config:'); + + Object.keys(CONFIG).forEach(function (key) { + var value = CONFIG[key]; + t.comment(f('- %s: %j', key, value)); + }); +} + + // --- exports module.exports = { @@ -262,9 +326,12 @@ module.exports = { triton: triton, safeTriton: safeTriton, createClient: createClient, + createTestInst: createTestInst, + deleteTestInst: deleteTestInst, getTestImg: getTestImg, getTestPkg: getTestPkg, jsonStreamParse: jsonStreamParse, + printConfig: printConfig, ifErr: testcommon.ifErr };