PUBAPI-1233/PUBAPI-1234 Add firewalls and snapshots to node-triton

triton fwrule ...
triton instance snapshot ...

Reviewed-By: Trent Mick (with some contributions)
This commit is contained in:
Marsell Kukuljevic 2016-02-05 00:39:50 +11:00 committed by Trent Mick
parent a1df6e0fe7
commit 634350018a
34 changed files with 3032 additions and 65 deletions

View File

@ -1,7 +1,9 @@
# node-triton changelog # 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. - #52 Fix 'triton ssh ...' stdout/stderr to fully flush with node >= 4.x.

View File

@ -13,3 +13,10 @@ function complete_tritonupdateaccountfield {
candidates="{{UPDATE_ACCOUNT_FIELDS}}" candidates="{{UPDATE_ACCOUNT_FIELDS}}"
compgen $compgen_opts -W "$candidates" -- "$word" compgen $compgen_opts -W "$candidates" -- "$word"
} }
function complete_tritonupdatefwrulefield {
local word="$1"
local candidates
candidates="{{UPDATE_FWRULE_FIELDS}}"
compgen $compgen_opts -W "$candidates" -- "$word"
}

View File

@ -217,10 +217,11 @@ function CLI() {
'stop', 'stop',
'reboot', 'reboot',
'ssh', 'ssh',
{ group: 'Images, Packages, Networks' }, { group: 'Images, Packages, Networks, Firewall Rules' },
'image', 'image',
'package', 'package',
'network', 'network',
'fwrule',
{ group: 'Other Commands' }, { group: 'Other Commands' },
'info', 'info',
'account', 'account',
@ -354,6 +355,9 @@ CLI.prototype.do_info = require('./do_info');
CLI.prototype.do_key = require('./do_key'); CLI.prototype.do_key = require('./do_key');
CLI.prototype.do_keys = require('./do_keys'); CLI.prototype.do_keys = require('./do_keys');
// Firewall rules
CLI.prototype.do_fwrule = require('./do_fwrule');
// Images // Images
CLI.prototype.do_images = require('./do_images'); CLI.prototype.do_images = require('./do_images');
CLI.prototype.do_image = require('./do_image'); CLI.prototype.do_image = require('./do_image');

View File

@ -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) { CloudApi.prototype._doMachine = function _doMachine(action, uuid, callback) {
var self = this; var self = this;
@ -1010,6 +1033,26 @@ CloudApi.prototype.replaceMachineTags = function replaceMachineTags(opts, cb) {
}); });
}; };
/**
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTags>
*
* @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);
});
};
/** /**
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTag> * <http://apidocs.joyent.com/cloudapi/#DeleteMachineTag>
* *
@ -1034,27 +1077,399 @@ CloudApi.prototype.deleteMachineTag = function deleteMachineTag(opts, cb) {
}); });
}; };
// --- snapshots
/** /**
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTags> * Creates a new snapshot for a given machine.
* *
* @param {Object} opts: * The machine cannot be a KVM brand.
* - @param {UUID} id: The machine UUID. Required. *
* @param {Function} cb - `function (err, res)` * 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);
});
};
// <updatable account field> -> <expected typeof>
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.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
this._request({ this._request({
method: 'DELETE', 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) { }, function (err, req, res) {
cb(err, 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 // --- rbac
/** /**

View File

@ -314,8 +314,7 @@ function uuidToShortId(s) {
* *
* Short IDs: * Short IDs:
* - UUID prefix * - UUID prefix
* - allow '-' to be elided (to support using containers IDs from * - allow '-' to be elided (to support using containers IDs from docker)
* docker)
* - support docker ID *longer* than a UUID? The curr implementation does. * - support docker ID *longer* than a UUID? The curr implementation does.
*/ */
function normShortId(s) { function normShortId(s) {

View File

@ -13,8 +13,9 @@
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var UPDATE_ACCOUNT_FIELDS var CloudApi = require('./cloudapi2').CloudApi;
= require('./cloudapi2').CloudApi.prototype.UPDATE_ACCOUNT_FIELDS; 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`. // Replace {{variable}} in `s` with the template data in `d`.
@ -39,6 +40,8 @@ function do_completion(subcmd, opts, args, cb) {
'utf8'); 'utf8');
var specExtra = renderTemplate(specExtraIn, { var specExtra = renderTemplate(specExtraIn, {
UPDATE_ACCOUNT_FIELDS: Object.keys(UPDATE_ACCOUNT_FIELDS).sort() 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(' ') .map(function (field) { return field + '='; }).join(' ')
}); });
console.log(this.bashCompletion({specExtra: specExtra})); console.log(this.bashCompletion({specExtra: specExtra}));
@ -61,7 +64,7 @@ do_completion.options = [
} }
]; ];
do_completion.help = [ do_completion.help = [
'Output bash completion. See help output for installation.', 'Emit bash completion. See help for installation.',
'', '',
'Installation:', 'Installation:',
' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} # Mac', ' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} # Mac',

View File

@ -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 <fwrule> 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: '<desc>',
help: 'Description of the firewall rule.'
}
];
do_create.help = [
'Create a firewall rule.',
'',
'Usage:',
' {{name}} create [<options>] <fwrule>',
'',
'{{options}}'
].join('\n');
do_create.helpOpts = {
helpCol: 25
};
module.exports = do_create;

110
lib/do_fwrule/do_delete.js Normal file
View File

@ -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 <fwrule-id> argument(s)'));
return;
}
var cli = this.top;
var ruleIds = args;
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.force) {
return next();
}
var msg;
if (ruleIds.length === 1) {
msg = 'Delete firewall rule "' + ruleIds[0] + '"? [y/n] ';
} else {
msg = format('Delete %d firewall rules (%s)? [y/n] ',
ruleIds.length, ruleIds.join(', '));
}
common.promptYesNo({msg: msg}, function (answer) {
if (answer !== 'y') {
console.error('Aborting');
next(true); // early abort signal
} else {
next();
}
});
},
function deleteThem(_, next) {
vasync.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>] <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}'
].join('\n');
do_delete.aliases = ['rm'];
module.exports = do_delete;

View File

@ -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 <fwrule-id> 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 <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}'
].join('\n');
module.exports = do_disable;

View File

@ -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 <fwrule-id> 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 <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}'
].join('\n');
module.exports = do_enable;

76
lib/do_fwrule/do_get.js Normal file
View File

@ -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 <fwrule-id> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var id = args[0];
var cli = this.top;
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 <fwrule-id>',
'',
'{{options}}'
].join('\n');
module.exports = do_get;

View File

@ -0,0 +1,162 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2016 Joyent, Inc.
*
* `triton fwrule instances ...`
*/
var format = require('util').format;
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
var COLUMNS_DEFAULT = 'shortid,name,img,state,flags,age';
var COLUMNS_LONG = 'id,name,img,brand,package,state,flags,primaryIp,created';
var SORT_DEFAULT = 'created';
function do_instances(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing <fwrule-id> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var id = args[0];
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
var imgs;
var insts;
var tritonapi = this.top.tritonapi;
vasync.parallel({funcs: [
function getTheImages(next) {
tritonapi.listImages({useCache: true},
function (err, _imgs) {
if (err) {
next(err);
} else {
imgs = _imgs;
next();
}
});
},
function getTheMachines(next) {
tritonapi.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>] <fwrule-id>',
'',
'{{options}}',
'',
'Fields (most are self explanatory, "*" indicates a field added client-side',
'for convenience):',
' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:',
' "D" docker instance',
' "F" firewall is enabled',
' "K" the brand is "kvm"',
' age* Approximate time since created, e.g. 1y, 2w.',
' img* The image "name@version", if available, else its',
' "shortid".'
/* END JSSTYLED */
].join('\n');
do_instances.aliases = ['insts'];
module.exports = do_instances;

98
lib/do_fwrule/do_list.js Normal file
View File

@ -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>]',
'',
'{{options}}'
].join('\n');
do_list.aliases = ['ls'];
module.exports = do_list;

190
lib/do_fwrule/do_update.js Normal file
View File

@ -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 <fwrule-id> 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: '<json-file>',
help: 'A file holding a JSON file of updates, or "-" to read ' +
'JSON from stdin.'
}
];
do_update.help = [
'Update a firewall rule',
'',
'Usage:',
' {{name}} update <fwrule-id> [FIELD=VALUE ...]',
' {{name}} update -f <json-file> <fwrule-id>',
'',
'{{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;

59
lib/do_fwrule/index.js Normal file
View File

@ -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;

View File

@ -23,7 +23,7 @@ function ImageCLI(top) {
name: top.name + ' image', name: top.name + ' image',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List, get, create and manage Triton images.' 'List and manage Triton images.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -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 <inst> 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>] <inst>',
'',
'{{options}}'
].join('\n');
module.exports = do_fwrules;

View File

@ -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 <inst> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var cli = this.top;
var createOpts = {
userId: opts.userId,
id: inst
};
if (opts.name) {
createOpts.name = opts.name;
}
vasync.pipeline({arg: {}, funcs: [
function createSnapshot(ctx, next) {
ctx.start = Date.now();
cli.tritonapi.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: '<snapname>',
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>] <inst>',
'',
'{{options}}',
'Snapshot do not work for instances of type "kvm".'
].join('\n');
module.exports = do_create;

View File

@ -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 <inst> and <snapname> 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>] <inst> <snapname> [<snapname>...]',
'',
'{{options}}'
].join('\n');
do_delete.aliases = ['rm'];
module.exports = do_delete;

View File

@ -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 <inst> and/or <snapname> 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 <inst> <snapname>',
'',
'{{options}}'
].join('\n');
module.exports = do_get;

View File

@ -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 <inst> 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>] <inst>',
'',
'{{options}}'
].join('\n');
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -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=<snapname>`.'
});
}
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;

View File

@ -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;

View File

@ -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; return do_ACTION;
} }
@ -77,7 +85,8 @@ function _doTheAction(action, subcmd, opts, args, callback) {
var command, state; var command, state;
switch (action) { switch (action) {
case 'start': case 'start':
command = 'startMachine'; command = opts.snapshot ? 'startMachineFromSnapshot' :
'startMachine';
state = 'running'; state = 'running';
break; break;
case 'stop': case 'stop':
@ -126,7 +135,12 @@ function _doTheAction(action, subcmd, opts, args, callback) {
// called when "uuid" is set // called when "uuid" is set
function done() { 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) { function (err, body, res) {
if (err) { if (err) {

View File

@ -22,7 +22,7 @@ function InstanceCLI(top) {
name: top.name + ' instance', name: top.name + ' instance',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List, get, create and manage Triton instances.' 'List and manage Triton instances.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {
@ -42,6 +42,8 @@ function InstanceCLI(top) {
'ssh', 'ssh',
'wait', 'wait',
'audit', 'audit',
'fwrules',
'snapshot',
'tag' 'tag'
] ]
}); });
@ -65,6 +67,9 @@ InstanceCLI.prototype.do_reboot = require('./do_reboot');
InstanceCLI.prototype.do_ssh = require('./do_ssh'); InstanceCLI.prototype.do_ssh = require('./do_ssh');
InstanceCLI.prototype.do_wait = require('./do_wait'); InstanceCLI.prototype.do_wait = require('./do_wait');
InstanceCLI.prototype.do_audit = require('./do_audit'); 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_tag = require('./do_tag');
InstanceCLI.prototype.do_tags = require('./do_tags'); InstanceCLI.prototype.do_tags = require('./do_tags');

View File

@ -23,7 +23,7 @@ function NetworkCLI(top) {
name: top.name + ' network', name: top.name + ' network',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List, get, create and update Triton networks.' 'List and manage Triton networks.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -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 //---- TritonApi class
@ -256,7 +281,7 @@ TritonApi.prototype._cacheGetJson = function _cacheGetJson(key, cb) {
* *
* @param opts {Object} Optional. * @param opts {Object} Optional.
* - useCache {Boolean} Default false. Whether to use Triton's local cache. * - 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. * when there are no filter options.
* - ... all other cloudapi ListImages options per * - ... all other cloudapi ListImages options per
* <https://apidocs.joyent.com/cloudapi/#ListImages> * <https://apidocs.joyent.com/cloudapi/#ListImages>
@ -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 // ---- 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 // ---- RBAC
/** /**

View File

@ -1,7 +1,7 @@
{ {
"name": "triton", "name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "4.5.3", "version": "4.6.0",
"author": "Joyent (joyent.com)", "author": "Joyent (joyent.com)",
"dependencies": { "dependencies": {
"assert-plus": "0.2.0", "assert-plus": "0.2.0",

View File

@ -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();
});
});
});

View File

@ -39,32 +39,14 @@ if (opts.skip) {
console.error('** set "allowWriteActions" in test config to enable'); console.error('** set "allowWriteActions" in test config to enable');
} }
test('triton inst tag ...', opts, function (tt) { test('triton inst tag ...', opts, function (tt) {
tt.comment('Test config:'); h.printConfig(tt);
Object.keys(h.CONFIG).forEach(function (key) {
var value = h.CONFIG[key];
tt.comment(f('- %s: %j', key, value));
});
var inst; var inst;
tt.test(' cleanup: rm inst ' + INST_ALIAS + ' if exists', function (t) { tt.test(' cleanup: rm inst ' + INST_ALIAS + ' if exists', function (t) {
h.triton(['inst', 'get', '-j', INST_ALIAS], h.deleteTestInst(t, INST_ALIAS, function (err) {
function (err, stdout, stderr) { t.ifErr(err);
if (err) { t.end();
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();
});
}
}); });
}); });

View File

@ -40,30 +40,12 @@ if (opts.skip) {
console.error('** set "allowWriteActions" in test config to enable'); console.error('** set "allowWriteActions" in test config to enable');
} }
test('triton manage workflow', opts, function (tt) { test('triton manage workflow', opts, function (tt) {
tt.comment('Test config:'); h.printConfig(tt);
Object.keys(h.CONFIG).forEach(function (key) {
var value = h.CONFIG[key];
tt.comment(f('- %s: %j', key, value));
});
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) { tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) {
h.triton(['inst', 'get', '-j', INST_ALIAS], h.deleteTestInst(t, INST_ALIAS, function (err) {
function (err, stdout, stderr) { t.ifErr(err);
if (err) { t.end();
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();
});
}
}); });
}); });

View File

@ -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();
});
});
});

View File

@ -43,6 +43,12 @@ var subs = [
['instance delete', 'instance rm', 'delete', 'rm'], ['instance delete', 'instance rm', 'delete', 'rm'],
['instance wait'], ['instance wait'],
['instance audit'], ['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'], ['ssh'],
['network'], ['network'],
['network list', 'networks'], ['network list', 'networks'],
@ -58,6 +64,15 @@ var subs = [
['package', 'pkg'], ['package', 'pkg'],
['package get'], ['package get'],
['package list', 'packages', 'pkgs'], ['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'],
['rbac info'], ['rbac info'],
['rbac apply'], ['rbac apply'],

View File

@ -122,6 +122,8 @@ function triton(args, opts, cb) {
}, cb); }, cb);
} }
/* /*
* `triton ...` wrapper that: * `triton ...` wrapper that:
* - tests non-error exit * - 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 // --- exports
module.exports = { module.exports = {
@ -262,9 +326,12 @@ module.exports = {
triton: triton, triton: triton,
safeTriton: safeTriton, safeTriton: safeTriton,
createClient: createClient, createClient: createClient,
createTestInst: createTestInst,
deleteTestInst: deleteTestInst,
getTestImg: getTestImg, getTestImg: getTestImg,
getTestPkg: getTestPkg, getTestPkg: getTestPkg,
jsonStreamParse: jsonStreamParse, jsonStreamParse: jsonStreamParse,
printConfig: printConfig,
ifErr: testcommon.ifErr ifErr: testcommon.ifErr
}; };