diff --git a/CHANGES.md b/CHANGES.md index 013a71f..5506504 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 4.7.1 (not yet released) - #97 `triton profile set -` to set the *last* profile as current. +- PUBAPI-1266 Added `instance enable-firewall` and `instance disable-firewall` ## 4.7.0 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 8ae9909..7364bfa 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -755,7 +755,7 @@ CloudApi.prototype.rebootMachine = function rebootMachine(uuid, callback) { * Enables machine firewall. * * @param {String} id (required) The machine id. - * @param {Function} callback of the form `function (err, machine, res)` + * @param {Function} callback of the form `function (err, null, res)` */ CloudApi.prototype.enableMachineFirewall = function enableMachineFirewall(uuid, callback) { @@ -767,7 +767,7 @@ function enableMachineFirewall(uuid, callback) { * Disables machine firewall. * * @param {String} id (required) The machine id. - * @param {Function} callback of the form `function (err, machine, res)` + * @param {Function} callback of the form `function (err, null, res)` */ CloudApi.prototype.disableMachineFirewall = function disableMachineFirewall(uuid, callback) { @@ -940,6 +940,50 @@ CloudApi.prototype.machineAudit = function machineAudit(id, cb) { }; +/** + * Wait for a machine's firewall to enable or disable. + * + * @param {Object} options + * - {String} id {required} machine id + * - {Boolean} state {require} - desired state + * - {Number} interval (optional) - time in ms to poll + * @param {Function} callback of the form f(err, machine, res). + */ +CloudApi.prototype.waitForMachineFirewallState = +function waitForMachineFirewallState(opts, cb) { + var self = this; + + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.bool(opts.state, 'opts.state'); + 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.getMachine({ + id: opts.id + }, function (err, machine, res) { + if (err) { + cb(err, null, res); + return; + } + + if (opts.state === machine.firewall_enabled) { + cb(null, machine, res); + return; + } + + setTimeout(poll, interval); + }); + } +}; + + // --- machine tags /** @@ -2120,4 +2164,4 @@ module.exports = { }, CloudApi: CloudApi -}; \ No newline at end of file +}; diff --git a/lib/do_instance/do_disable_firewall.js b/lib/do_instance/do_disable_firewall.js new file mode 100644 index 0000000..f649a9a --- /dev/null +++ b/lib/do_instance/do_disable_firewall.js @@ -0,0 +1,148 @@ +/* + * 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 disable-firewall ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_disable_firewall(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; + var insts = args; + + function wait(instId, startTime, next) { + var cloudapi = cli.tritonapi.cloudapi; + var waiter = cloudapi.waitForMachineFirewallState.bind(cloudapi); + + waiter({ + id: instId, + state: false + }, function (err, inst) { + if (err) { + return next(err); + } + + if (inst.firewall_enabled === false) { + var duration = Date.now() - startTime; + var durStr = common.humanDurationFromMs(duration); + console.log('Disabled firewall for instance "%s" in %s', instId, + durStr); + next(); + } else { + // shouldn't get here, but... + var msg = 'Failed to disable firewall for instance "%s"'; + next(new Error(format(msg, instId))); + } + }); + } + + vasync.pipeline({funcs: [ + function confirm(_, next) { + if (opts.force) { + return next(); + } + + var msg; + if (insts.length === 1) { + msg = 'Disable firewall for instance "' + insts[0] + + '"? [y/n] '; + } else { + msg = format('Disable firewalls for %d instances (%s)? [y/n] ', + insts.length, insts.join(', ')); + } + + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + function disableThem(_, next) { + var startTime = Date.now(); + + vasync.forEachParallel({ + inputs: insts, + func: function disableOne(instId, nextId) { + cli.tritonapi.disableInstanceFirewall({ + id: instId + }, function (err, __, res) { + if (err) { + nextId(err); + return; + } + + var msg = 'Disabling firewall for instance "%s"'; + console.log(msg, res.instId); + + if (opts.wait) { + wait(res.instId, startTime, nextId); + } else { + nextId(); + } + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +do_disable_firewall.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['force', 'f'], + type: 'bool', + help: 'Skip confirmation to enable.' + }, + { + names: ['wait', 'w'], + type: 'bool', + help: 'Wait for the firewall to be disabled.' + } +]; +do_disable_firewall.help = [ + 'Disable the firewall of one or more instances.', + '', + 'Usage:', + ' {{name}} disable-firewall [] [...]', + '', + '{{options}}' +].join('\n'); + +module.exports = do_disable_firewall; diff --git a/lib/do_instance/do_enable_firewall.js b/lib/do_instance/do_enable_firewall.js new file mode 100644 index 0000000..184fb3a --- /dev/null +++ b/lib/do_instance/do_enable_firewall.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 instance enable-firewall ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_enable_firewall(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; + var insts = args; + + function wait(instId, startTime, next) { + var cloudapi = cli.tritonapi.cloudapi; + var waiter = cloudapi.waitForMachineFirewallState.bind(cloudapi); + + waiter({ + id: instId, + state: true + }, function (err, inst) { + if (err) { + return next(err); + } + + if (inst.firewall_enabled === true) { + var duration = Date.now() - startTime; + var durStr = common.humanDurationFromMs(duration); + console.log('Enabled firewall for instance "%s" in %s', instId, + durStr); + next(); + } else { + // shouldn't get here, but... + var msg = 'Failed to enable firewall for instance "%s"'; + next(new Error(format(msg, instId))); + } + }); + } + + vasync.pipeline({funcs: [ + function confirm(_, next) { + if (opts.force) { + return next(); + } + + var msg; + if (insts.length === 1) { + msg = 'Enable firewall for instance "' + insts[0] + '"? [y/n] '; + } else { + msg = format('Enable firewalls for %d instances (%s)? [y/n] ', + insts.length, insts.join(', ')); + } + + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + function enableThem(_, next) { + var startTime = Date.now(); + + vasync.forEachParallel({ + inputs: insts, + func: function enableOne(instId, nextId) { + cli.tritonapi.enableInstanceFirewall({ + id: instId + }, function (err, __, res) { + if (err) { + nextId(err); + return; + } + + var msg = 'Enabling firewall for instance "%s"'; + console.log(msg, res.instId); + + if (opts.wait) { + wait(res.instId, startTime, nextId); + } else { + nextId(); + } + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +do_enable_firewall.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['force', 'f'], + type: 'bool', + help: 'Skip confirmation to enable.' + }, + { + names: ['wait', 'w'], + type: 'bool', + help: 'Wait for the firewall to be enabled.' + } +]; +do_enable_firewall.help = [ + 'Enable the firewall of one or more instances.', + '', + 'Usage:', + ' {{name}} enable-firewall [] [...]', + '', + '{{options}}' +].join('\n'); + +module.exports = do_enable_firewall; diff --git a/lib/do_instance/index.js b/lib/do_instance/index.js index 0484ebe..a4d9bf1 100644 --- a/lib/do_instance/index.js +++ b/lib/do_instance/index.js @@ -39,6 +39,9 @@ function InstanceCLI(top) { 'stop', 'reboot', { group: '' }, + 'enable-firewall', + 'disable-firewall', + { group: '' }, 'ssh', 'wait', 'audit', @@ -64,6 +67,9 @@ InstanceCLI.prototype.do_start = require('./do_start'); InstanceCLI.prototype.do_stop = require('./do_stop'); InstanceCLI.prototype.do_reboot = require('./do_reboot'); +InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall'); +InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall'); + InstanceCLI.prototype.do_ssh = require('./do_ssh'); InstanceCLI.prototype.do_wait = require('./do_wait'); InstanceCLI.prototype.do_audit = require('./do_audit'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 0de508d..f8cbc3e 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -769,6 +769,72 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { }; +// ---- instance firewall + +/** + * Enable the firewall on an instance. + * + * @param {Object} opts + * - {String} id: The instance ID, or short ID. Required. + * @param {Function} callback `function (err, null, res)` + */ +TritonApi.prototype.enableInstanceFirewall = +function enableInstanceFirewall(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: [ + _stepInstId, + + function enableFirewall(arg, next) { + self.cloudapi.enableMachineFirewall(arg.instId, + function (err, _, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + next(err); + }); + } + ]}, function (err) { + cb(err, null, res); + }); +}; + + +/** + * Disable the firewall on an instance. + * + * @param {Object} opts + * - {String} id: The instance ID, or short ID. Required. + * @param {Function} callback `function (err, null, res)` + */ +TritonApi.prototype.disableInstanceFirewall = +function disableInstanceFirewall(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: [ + _stepInstId, + + function disableFirewall(arg, next) { + self.cloudapi.disableMachineFirewall(arg.instId, + function (err, _, _res) { + res = _res; + res.instId = arg.instId; // gross hack, in case caller needs it + next(err); + }); + } + ]}, function (err) { + cb(err, null, res); + }); +}; + + // ---- instance snapshots /** diff --git a/test/integration/cli-fwrules.test.js b/test/integration/cli-fwrules.test.js index c1b2e0f..c1c46a1 100644 --- a/test/integration/cli-fwrules.test.js +++ b/test/integration/cli-fwrules.test.js @@ -248,6 +248,48 @@ test('triton fwrule', OPTS, function (tt) { }); }); + tt.test(' triton instance enable-firewall', function (t) { + var cmd = 'instance enable-firewall ' + INST + ' -fw'; + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance enable-firewall')) + return t.end(); + + t.ok(stdout.match('Enabled firewall for instance "' + INST + '"'), + 'firewall enabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.equal(inst.firewall_enabled, true); + + t.end(); + }); + }); + }); + + tt.test(' triton instance disable-firewall', function (t) { + var cmd = 'instance disable-firewall ' + INST + ' -fw'; + h.triton(cmd, function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance disable-firewall')) + return t.end(); + + t.ok(stdout.match('Disabled firewall for instance "' + INST + '"'), + 'firewall disabled'); + + h.triton('instance get -j ' + INST, function (err2, stdout2) { + if (h.ifErr(t, err2, 'triton instance get')) + return t.end(); + + var inst = JSON.parse(stdout2); + t.equal(inst.firewall_enabled, false); + + 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. diff --git a/test/integration/cli-subcommands.test.js b/test/integration/cli-subcommands.test.js index 37f167c..f9aab5f 100644 --- a/test/integration/cli-subcommands.test.js +++ b/test/integration/cli-subcommands.test.js @@ -41,6 +41,8 @@ var subs = [ ['instance stop', 'stop'], ['instance reboot', 'reboot'], ['instance delete', 'instance rm', 'delete', 'rm'], + ['instance enable-firewall'], + ['instance disable-firewall'], ['instance wait'], ['instance audit'], ['instance fwrules'],