merge with latest from upstream

This commit is contained in:
Marius Pana 2018-06-06 13:21:15 +03:00
commit 5438723d06
36 changed files with 2395 additions and 435 deletions

View File

@ -6,6 +6,66 @@ Known issues:
## not yet released
- [joyent/node-triton#245] `triton profile` now generates fresh new keys during
Docker setup and signs them with an account key, rather than copying (and
decrypting) the account key itself. This makes using Docker simpler with keys
in an SSH Agent.
## 6.0.0
This release containes some breaking changes with the --affinity flag to
`triton instance create`. It also does not work with cloudapi endpoints older
than 8.0.0 (mid 2016); for an older cloudapi endpoint, use node-triton 5.9.0.
- [TRITON-167, TRITON-168] update support for
`triton instance create --affinity=...`. It now fully supports regular
expressions, tags and globs, and works across a wider variety of situations.
Examples:
```
# regular expressions
triton instance create --affinity='instance!=/^production-db/' ...
# globs
triton instance create --affinity='instance!=production-db*' ...
# tags
triton instance create --affinity='role!=db'
```
See <https://apidocs.joyent.com/cloudapi/#affinity-rules> for more details
how affinities work.
However:
- Use of regular expressions requires a cloudapi version of 8.8.0 or later.
- 'inst' as a affinity shorthand no longer works. Use 'instance' instead.
E.g.: --affinity='instance==db1' instead of --affinity='inst==db1'
- The shorthand --affinity=<INST> no longer works. Use
--affinity='instance===<INST>' instead.
## 5.10.0
- [TRITON-19] add support for deletion protection on instances. An instance with
the deletion protection flag set true cannot be destroyed until the flag is
set false. It is exposed through
`triton instance create --deletion-protection ...`,
`triton instance enable-deletion-protection ...`, and
`triton instance disable-deletion-protection ...`. This flag is only supported
on cloudapi versions 8.7.0 or above.
- [TRITON-59] node-triton should support nic operations
`triton instance nic get ...`
`triton instance nic create ...`
`triton instance nic list ...`
`triton instance nic delete ...`
- [TRITON-42] node-triton should support nics when creating an instance, e.g.
`triton instance create --nic <Network Object> IMAGE PACKAGE`
## 5.9.0
- [TRITON-190] remove support for `triton instance create --brand=bhyve ...`.
The rest of bhyve support will remain, but selection of bhyve brand will
happen via images or packages that are bhyve-specific.
## 5.8.0
- [TRITON-124] add node-triton support for bhyve. This adds a `triton instance

View File

@ -154,7 +154,6 @@ function CloudApi(options) {
this.client = new SaferJsonClient(options);
}
CloudApi.prototype.close = function close(callback) {
this.log.trace({host: this.client.url && this.client.url.host},
'close cloudapi http client');
@ -1006,7 +1005,6 @@ function enableMachineFirewall(uuid, callback) {
return this._doMachine('enable_firewall', uuid, callback);
};
/**
* Disables machine firewall.
*
@ -1018,6 +1016,28 @@ function disableMachineFirewall(uuid, callback) {
return this._doMachine('disable_firewall', uuid, callback);
};
/**
* Enables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.enableMachineDeletionProtection =
function enableMachineDeletionProtection(uuid, callback) {
return this._doMachine('enable_deletion_protection', uuid, callback);
};
/**
* Disables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.disableMachineDeletionProtection =
function disableMachineDeletionProtection(uuid, callback) {
return this._doMachine('disable_deletion_protection', uuid, callback);
};
/**
* internal function for start/stop/reboot/enable_firewall/disable_firewall
*/
@ -1150,7 +1170,7 @@ CloudApi.prototype.createMachine = function createMachine(options, callback) {
assert.optionalString(options.name, 'options.name');
assert.uuid(options.image, 'options.image');
assert.uuid(options.package, 'options.package');
assert.optionalArrayOfUuid(options.networks, 'options.networks');
assert.optionalArray(options.networks, 'options.networks');
// TODO: assert the other fields
assert.func(callback, 'callback');
@ -1228,6 +1248,53 @@ function waitForMachineFirewallEnabled(opts, cb) {
};
/**
* Wait for a machine's `deletion_protection` field to go true or
* false/undefined.
*
* @param {Object} options
* - {String} id: Required. The machine UUID.
* - {Boolean} state: Required. The desired `deletion_protection` state.
* - {Number} interval: Optional. Time (in ms) to poll.
* @param {Function} callback of the form f(err, machine, res).
*/
CloudApi.prototype.waitForDeletionProtectionEnabled =
function waitForDeletionProtectionEnabled(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 getMachineCb(err, machine, res) {
if (err) {
cb(err, null, res);
return;
}
// !! converts an undefined to a false
if (opts.state === !!machine.deletion_protection) {
cb(null, machine, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- machine tags
/**
@ -1536,6 +1603,150 @@ function deleteMachineSnapshot(opts, cb) {
};
// --- NICs
/**
* Adds a NIC on a network to an instance.
*
* @param {Object} options object containing:
* - {String} id (required) the instance id.
* - {String|Object} (required) network uuid or network object.
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.addNic =
function addNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network');
var data = {
network: opts.network
};
this._request({
method: 'POST',
path: format('/%s/machines/%s/nics', this.account, opts.id),
data: data
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Lists all NICs on an instance.
*
* Returns an array of objects.
*
* @param opts {Object} Options
* - {String} id (required) the instance id.
* @param {Function} callback of the form f(err, nics, res).
*/
CloudApi.prototype.listNics =
function listNics(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s/nics', this.account, opts.id);
this._passThrough(endpoint, opts, cb);
};
/**
* Retrieves a NIC on an instance.
*
* @param {Object} options object containing:
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} callback of the form `function (err, nic, res)`
*/
CloudApi.prototype.getNic =
function getNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
var endpoint = format('/%s/machines/%s/nics/%s', this.account, opts.id,
mac);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Remove a NIC off an instance.
*
* @param {Object} opts (object)
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} cb of the form `function (err, res)`
*/
CloudApi.prototype.removeNic =
function removeNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
this._request({
method: 'DELETE',
path: format('/%s/machines/%s/nics/%s', this.account, opts.id, mac)
}, function (err, req, res) {
cb(err, res);
});
};
/**
* Wait for a machine's nic to go one of a set of specfic states.
*
* @param {Object} options
* - {String} id {required} machine id
* - {String} mac {required} mac for new nic
* - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.waitForNicStates =
function waitForNicStates(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
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.getNic({
id: opts.id,
mac: opts.mac
}, function onPoll(err, nic, res) {
if (err) {
cb(err, null, res);
return;
}
if (opts.states.indexOf(nic.state) !== -1) {
cb(null, nic, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- firewall rules
/**

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2017, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
var assert = require('assert-plus');
@ -24,7 +24,8 @@ var wordwrap = require('wordwrap');
var errors = require('./errors'),
InternalError = errors.InternalError;
var NETWORK_OBJECT_FIELDS =
require('./constants').NETWORK_OBJECT_FIELDS;
// ---- support stuff
@ -1412,6 +1413,55 @@ function ipv4ToLong(ip) {
return l;
}
/*
* Parse the input from the `--nics <nic>` CLI argument.
*
* @param a {Array} The array of strings formatted as key=value
* ex: ['ipv4_uuid=1234', 'ipv4_ips=1.2.3.4|5.6.7.8']
* @return {Object} A network object. From the example above:
* {
* "ipv4_uuid": 1234,
* "ipv4_ips": [
* "1.2.3.4",
* "5.6.7.8"
* ]
* }
* Note: "1234" is used as the UUID for this example, but would actually cause
* `parseNicStr` to throw as it is not a valid UUID.
*/
function parseNicStr(nic) {
assert.arrayOfString(nic);
var obj = objFromKeyValueArgs(nic, {
disableDotted: true,
typeHintFromKey: NETWORK_OBJECT_FIELDS,
validKeys: Object.keys(NETWORK_OBJECT_FIELDS)
});
if (!obj.ipv4_uuid) {
throw new errors.UsageError(
'ipv4_uuid must be specified in network object');
}
if (obj.ipv4_ips) {
obj.ipv4_ips = obj.ipv4_ips.split('|');
}
assert.uuid(obj.ipv4_uuid, 'obj.ipv4_uuid');
assert.optionalArrayOfString(obj.ipv4_ips, 'obj.ipv4_ips');
/*
* Only 1 IP address may be specified at this time. In the future, this
* limitation should be removed.
*/
if (obj.ipv4_ips && obj.ipv4_ips.length !== 1) {
throw new errors.UsageError('only 1 ipv4_ip may be specified');
}
return obj;
}
//---- exports
module.exports = {
@ -1451,6 +1501,7 @@ module.exports = {
monotonicTimeDiffMs: monotonicTimeDiffMs,
readStdin: readStdin,
validateObject: validateObject,
ipv4ToLong: ipv4ToLong
ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -46,11 +46,18 @@ if (process.env.SCTEST_CLI_CONFIG_DIR) {
CLI_CONFIG_DIR = mod_path.resolve(process.env.HOME, '.spearhead');
}
// <Network Object Key> -> <expected typeof>
var NETWORK_OBJECT_FIELDS = {
ipv4_uuid: 'string',
ipv4_ips: 'string'
};
// ---- exports
module.exports = {
CLI_CONFIG_DIR: CLI_CONFIG_DIR
CLI_CONFIG_DIR: CLI_CONFIG_DIR,
NETWORK_OBJECT_FIELDS: NETWORK_OBJECT_FIELDS
};

View File

@ -72,12 +72,8 @@ function do_update(subcmd, opts, args, callback) {
next();
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -92,36 +88,18 @@ function do_update(subcmd, opts, args, callback) {
},
function validateIt(ctx, next) {
var keys = Object.keys(ctx.data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_ACCOUNT_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_ACCOUNT_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;
}
try {
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS);
} catch (e) {
next(e);
return;
}
next();
},
function updateAway(ctx, next) {
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for account update');
next();
return;
}
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
if (err) {

View File

@ -115,6 +115,7 @@ function do_instances(subcmd, opts, args, cb) {
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined;
});
@ -164,6 +165,7 @@ do_instances.help = [
' "D" docker instance',
' "F" firewall is enabled',
' "K" the brand is "kvm"',
' "P" deletion protected',
' age* Approximate time since created, e.g. 1y, 2w.',
' img* The image "name@version", if available, else its',
' "shortid".'

View File

@ -84,14 +84,7 @@ function do_update(subcmd, opts, args, cb) {
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -107,33 +100,13 @@ function do_update(subcmd, opts, args, cb) {
},
function validateIt(ctx, next) {
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for firewall rule update');
next();
try {
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
} catch (e) {
next(e);
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();
},

View File

@ -19,6 +19,8 @@ var common = require('../common');
var distractions = require('../distractions');
var errors = require('../errors');
var mat = require('../metadataandtags');
var NETWORK_OBJECT_FIELDS =
require('../constants').NETWORK_OBJECT_FIELDS;
function parseVolMount(volume) {
var components;
@ -83,6 +85,9 @@ function do_create(subcmd, opts, args, cb) {
return;
} else if (args.length !== 2) {
return cb(new errors.UsageError('incorrect number of args'));
} else if (opts.nic && opts.network) {
return cb(new errors.UsageError(
'--network and --nic cannot be specified together'));
}
var log = this.top.log;
@ -90,103 +95,6 @@ function do_create(subcmd, opts, args, cb) {
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
/* BEGIN JSSTYLED */
/*
* Parse --affinity options for validity to `ctx.affinities`.
* Later (in `resolveLocality`) we'll translate this to locality hints
* that CloudAPI speaks.
*
* Some examples. Inspired by
* <https://docs.docker.com/swarm/scheduler/filter/#how-to-write-filter-expressions>
*
* instance==vm1
* container==vm1 # alternative to 'instance'
* inst==vm1 # alternative to 'instance'
* inst=vm1 # '=' is shortcut for '=='
* inst!=vm1 # '!='
* inst==~vm1 # '~' for soft/non-strict
* inst!=~vm1
*
* inst==vm* # globbing (not yet supported)
* inst!=/vm\d/ # regex (not yet supported)
*
* some-tag!=db # tags (not yet supported)
*
* Limitations:
* - no support for tags yet
* - no globbing or regex yet
* - we resolve name -> instance id *client-side* for now (until
* CloudAPI supports that)
* - Triton doesn't support mixed strict and non-strict, so we error
* out on that. We *could* just drop the non-strict, but that is
* slightly different.
*/
/* END JSSTYLED */
function parseAffinity(ctx, next) {
if (!opts.affinity) {
next();
return;
}
var affinities = [];
// TODO: stricter rules on the value part
// JSSTYLED
var affinityRe = /((instance|inst|container)(==~|!=~|==|!=|=~|=))?(.*?)$/;
for (var i = 0; i < opts.affinity.length; i++) {
var raw = opts.affinity[i];
var match = affinityRe.exec(raw);
if (!match) {
next(new errors.UsageError(format('invalid affinity: "%s"',
raw)));
return;
}
var key = match[2];
if ([undefined, 'inst', 'container'].indexOf(key) !== -1) {
key = 'instance';
}
assert.equal(key, 'instance');
var op = match[3];
if ([undefined, '='].indexOf(op) !== -1) {
op = '==';
}
var strict = true;
if (op[op.length - 1] === '~') {
strict = false;
op = op.slice(0, op.length - 1);
}
var val = match[4];
// Guard against mixed strictness (Triton can't handle those).
if (affinities.length > 0) {
var lastAff = affinities[affinities.length - 1];
if (strict !== lastAff.strict) {
next(new errors.TritonError(format('mixed strict and '
+ 'non-strict affinities are not supported: '
+ '%j (%s) and %j (%s)',
lastAff.raw,
(lastAff.strict ? 'strict' : 'non-strict'),
raw, (strict ? 'strict' : 'non-strict'))));
return;
}
}
affinities.push({
raw: raw,
key: key,
op: op,
strict: strict,
val: val
});
}
if (affinities.length) {
log.trace({affinities: affinities}, 'affinities');
ctx.affinities = affinities;
}
next();
},
/*
* Make sure if volumes were passed, they're in the correct form.
@ -225,61 +133,44 @@ function do_create(subcmd, opts, args, cb) {
},
/*
* Determine `ctx.locality` according to what CloudAPI supports
* based on `ctx.affinities` parsed earlier.
* Parse any nics given via `--nic`
*/
function resolveLocality(ctx, next) {
if (!ctx.affinities) {
function parseNics(ctx, next) {
if (!opts.nic) {
next();
return;
}
var strict;
var near = [];
var far = [];
ctx.nics = [];
var i;
var networksSeen = {};
var nic;
var nics = opts.nic;
vasync.forEachPipeline({
inputs: ctx.affinities,
func: function resolveAffinity(aff, nextAff) {
assert.ok(['==', '!='].indexOf(aff.op) !== -1,
'unexpected op: ' + aff.op);
var nearFar = (aff.op == '==' ? near : far);
log.trace({nics: nics}, 'parsing nics');
strict = aff.strict;
if (common.isUUID(aff.val)) {
nearFar.push(aff.val);
nextAff();
} else {
tritonapi.getInstance({
id: aff.val,
fields: ['id']
}, function (err, inst) {
if (err) {
nextAff(err);
} else {
log.trace({val: aff.val, inst: inst.id},
'resolveAffinity');
nearFar.push(inst.id);
nextAff();
}
});
for (i = 0; i < nics.length; i++) {
nic = nics[i].split(',');
try {
nic = common.parseNicStr(nic);
if (networksSeen[nic.ipv4_uuid]) {
throw new errors.UsageError(format(
'only 1 ip on a network allowed '
+ '(network %s specified multiple times)',
nic.ipv4_uuid));
}
}
}, function (err) {
if (err) {
networksSeen[nic.ipv4_uuid] = true;
ctx.nics.push(nic);
} catch (err) {
next(err);
return;
}
}
ctx.locality = {
strict: strict
};
if (near.length > 0) ctx.locality.near = near;
if (far.length > 0) ctx.locality.far = far;
log.trace({locality: ctx.locality}, 'resolveLocality');
log.trace({nics: ctx.nics}, 'parsed nics');
next();
});
next();
},
function loadMetadata(ctx, next) {
@ -371,19 +262,22 @@ function do_create(subcmd, opts, args, cb) {
var createOpts = {
name: opts.name,
image: ctx.img.id,
'package': ctx.pkg && ctx.pkg.id,
networks: ctx.nets && ctx.nets.map(
function (net) { return net.id; })
'package': ctx.pkg && ctx.pkg.id
};
if (opts.brand) {
createOpts.brand = opts.brand;
if (ctx.nets) {
createOpts.networks = ctx.nets.map(function (net) {
return net.id;
});
} else if (ctx.nics) {
createOpts.networks = ctx.nics;
}
if (ctx.volMounts) {
createOpts.volumes = ctx.volMounts;
}
if (ctx.locality) {
createOpts.locality = ctx.locality;
if (opts.affinity) {
createOpts.affinity = opts.affinity;
}
if (ctx.metadata) {
Object.keys(ctx.metadata).forEach(function (key) {
@ -400,6 +294,8 @@ function do_create(subcmd, opts, args, cb) {
var opt = opts._order[i];
if (opt.key === 'firewall') {
createOpts.firewall_enabled = opt.value;
} else if (opt.key === 'deletion_protection') {
createOpts.deletion_protection = opt.value;
}
}
@ -495,13 +391,6 @@ do_create.options = [
{
group: 'Create options'
},
{
names: ['brand'],
helpArg: 'BRAND',
type: 'string',
help: 'Override the default brand for this instance. Most users will ' +
'not need this option.'
},
{
names: ['name', 'n'],
helpArg: 'NAME',
@ -530,9 +419,7 @@ do_create.options = [
'INST), `instance==~INST` (*attempt* to place on the same server ' +
'as INST), or `instance!=~INST` (*attempt* to place on a server ' +
'other than INST\'s). `INST` is an existing instance name or ' +
'id. There are two shortcuts: `inst` may be used instead of ' +
'`instance` and `instance==INST` can be shortened to just ' +
'`INST`. Use this option more than once for multiple rules.',
'id. Use this option more than once for multiple rules.',
completionType: 'tritonaffinityrule'
},
@ -547,6 +434,15 @@ do_create.options = [
'This option can be used multiple times.',
completionType: 'tritonnetwork'
},
{
names: ['nic'],
type: 'arrayOfString',
helpArg: 'NICOPTS',
help: 'A network interface object containing comma separated ' +
'key=value pairs (Network object format). ' +
'This option can be used multiple times for multiple NICs. ' +
'Valid keys are: ' + Object.keys(NETWORK_OBJECT_FIELDS).join(', ')
},
{
// TODO: add boolNegationPrefix:'no-' when that cmdln pull is in
names: ['firewall'],
@ -554,6 +450,13 @@ do_create.options = [
help: 'Enable Cloud Firewall on this instance. See ' +
'<https://docs.spearhead.cloud/network/firewall>'
},
{
names: ['deletion-protection'],
type: 'bool',
help: 'Enable Deletion Protection on this instance. Such an instance ' +
'cannot be deleted until the protection is disabled. See ' +
'<https://apidocs.joyent.com/cloudapi/#deletion-protection>'
},
{
names: ['volume', 'v'],
type: 'arrayOfString',

View File

@ -0,0 +1,125 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance disable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_disable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
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(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: false
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(!inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Disabled deletion protection for instance "%s"', name);
next();
});
}
function disableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.disableInstanceDeletionProtection({
id: name
}, function disableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Disabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: disableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_disable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be removed.'
}
];
do_disable_deletion_protection.synopses = [
'{{name}} disable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_disable_deletion_protection.help = [
'Disable deletion protection on one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_disable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_disable_deletion_protection;

View File

@ -0,0 +1,125 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance enable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_enable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
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(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: true
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Enabled deletion protection for instance "%s"', name);
next();
});
}
function enableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.enableInstanceDeletionProtection({
id: name
}, function enableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Enabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: enableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_enable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be enabled.'
}
];
do_enable_deletion_protection.synopses = [
'{{name}} enable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_enable_deletion_protection.help = [
'Enable deletion protection for one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_enable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_enable_deletion_protection;

View File

@ -154,6 +154,7 @@ function do_list(subcmd, opts, args, callback) {
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined;
});
@ -213,6 +214,7 @@ do_list.help = [
' "D" docker instance',
' "F" firewall is enabled',
' "K" the brand is "kvm"',
' "P" deletion protected',
' age* Approximate time since created, e.g. 1y, 2w.',
' img* The image "name@version", if available, else its',
' "shortid".',

View File

@ -0,0 +1,211 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance nic create ...`
*/
var assert = require('assert-plus');
var common = require('../../common');
var errors = require('../../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalBool(opts.json, 'opts.json');
assert.optionalBool(opts.help, 'opts.help');
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 NETWORK or INST and' +
' NICOPT=VALUE arguments'));
return;
}
var cli = this.top;
var netObj;
var netObjArgs = [];
var regularArgs = [];
var createOpts = {};
args.forEach(function forEachArg(arg) {
if (arg.indexOf('=') !== -1) {
netObjArgs.push(arg);
return;
}
regularArgs.push(arg);
});
if (netObjArgs.length > 0) {
if (regularArgs.length > 1) {
cb(new errors.UsageError('cannot specify INST and NETWORK when'
+ ' passing in ipv4 arguments'));
return;
}
if (regularArgs.length !== 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
try {
netObj = common.parseNicStr(netObjArgs);
} catch (err) {
cb(err);
return;
}
}
if (netObj) {
assert.array(regularArgs, 'regularArgs');
assert.equal(regularArgs.length, 1, 'instance uuid');
createOpts.id = regularArgs[0];
createOpts.network = netObj;
} else {
assert.array(args, 'args');
assert.equal(args.length, 2, 'INST and NETWORK');
createOpts.id = args[0];
createOpts.network = args[1];
}
function wait(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
var waiter = cli.tritonapi.waitForNicStates.bind(cli.tritonapi);
/*
* We request state running|stopped because net-agent is doing work to
* keep a NICs state in sync with the VMs state. If a user adds a NIC
* to a stopped instance the final state of the NIC should also be
* stopped.
*/
waiter({
id: instId,
mac: mac,
states: ['running', 'stopped']
}, next);
}
// same signature as wait(), but is a nop
function waitNop(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
next();
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.addNic(createOpts, function onAddNic(err, nic) {
if (err) {
cb(err);
return;
}
// If a NIC exists on the network already we will receive a 302
if (!nic) {
var errMsg = 'Instance already has a NIC on that network';
cb(new errors.TritonError(errMsg));
return;
}
// either wait or invoke a nop stub
var func = opts.wait ? wait : waitNop;
if (opts.wait && !opts.json) {
console.log('Creating NIC %s', nic.mac);
}
func(createOpts.id, nic.mac, function onWait(err2, createdNic) {
if (err2) {
cb(err2);
return;
}
var nicInfo = createdNic || nic;
if (opts.json) {
console.log(JSON.stringify(nicInfo));
} else {
console.log('Created NIC %s', nic.mac);
}
cb();
});
});
});
}
do_create.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the creation to complete.'
}
];
do_create.synopses = [
'{{name}} {{cmd}} [OPTIONS] INST NETWORK',
'{{name}} {{cmd}} [OPTIONS] INST NICOPT=VALUE [NICOPT=VALUE ...]'
];
do_create.help = [
'Create a NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'INST is an instance id (full UUID), name, or short id,',
'and NETWORK is a network id (full UUID), name, or short id.',
'',
'NICOPTs are NIC options. The following NIC options are supported:',
'ipv4_uuid=<full network uuid> (required),' +
' and ipv4_ips=<a single IP string>.',
'',
'Be aware that adding NICs to an instance will cause that instance to',
'reboot.',
'',
'Example:',
' triton instance nic create --wait 22b75576 ca8aefb9',
' triton instance nic create 22b75576' +
' ipv4_uuid=651446a8-dab0-439e-a2c4-2c841ab07c51' +
' ipv4_ips=192.168.128.13'
].join('\n');
do_create.helpOpts = {
helpCol: 25
};
do_create.completionArgtypes = ['tritoninstance', 'tritonnic', 'none'];
module.exports = do_create;

View File

@ -0,0 +1,126 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance nic delete ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
function do_delete(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.optionalBool(opts.force, 'opts.force');
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 MAC argument(s)'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
confirm({mac: mac, force: opts.force}, function onConfirm(confirmErr) {
if (confirmErr) {
console.error('Aborting');
cb();
return;
}
cli.tritonapi.removeNic({
id: inst,
mac: mac
}, function onRemove(err) {
if (err) {
cb(err);
return;
}
console.log('Deleted NIC %s', mac);
cb();
});
});
});
}
// Request confirmation before deleting, unless --force flag given.
// If user declines, terminate early.
function confirm(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
if (opts.force) {
cb();
return;
}
common.promptYesNo({
msg: 'Delete NIC "' + opts.mac + '"? [y/n] '
}, function (answer) {
if (answer !== 'y') {
cb(new Error('Aborted NIC deletion'));
} else {
cb();
}
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['force', 'f'],
type: 'bool',
help: 'Force removal.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} INST MAC'];
do_delete.help = [
'Remove a NIC from an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Be aware that removing NICs from an instance will cause that instance to',
'reboot.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_delete;

View File

@ -0,0 +1,89 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance nic 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 MAC arguments'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.getNic({id: inst, mac: mac}, function onNic(err, nic) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(nic));
} else {
console.log(JSON.stringify(nic, 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.synopses = ['{{name}} {{cmd}} INST MAC'];
do_get.help = [
'Show a specific NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.'
].join('\n');
do_get.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_get;

View File

@ -0,0 +1,154 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton instance nic list ...`
*/
var assert = require('assert-plus');
var tabula = require('tabula');
var common = require('../../common');
var errors = require('../../errors');
var VALID_FILTERS = ['ip', 'mac', 'state', 'network', 'primary', 'gateway'];
var COLUMNS_DEFAULT = 'ip,mac,state,network';
var COLUMNS_DEFAULT_LONG = 'ip,mac,state,network,primary,gateway';
var SORT_DEFAULT = 'ip';
function do_list(subcmd, opts, args, cb) {
assert.array(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
var inst = args.shift();
try {
var filters = common.objFromKeyValueArgs(args, {
validKeys: VALID_FILTERS,
disableDotted: true
});
} catch (e) {
cb(e);
return;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.listNics({id: inst}, function onNics(err, nics) {
if (err) {
cb(err);
return;
}
// do filtering
Object.keys(filters).forEach(function filterByKey(key) {
var val = filters[key];
nics = nics.filter(function filterByNic(nic) {
return nic[key] === val;
});
});
if (opts.json) {
common.jsonStream(nics);
} else {
nics.forEach(function onNic(nic) {
nic.network = nic.network.split('-')[0];
nic.ip = nic.ip + '/' + convertCidrSuffix(nic.netmask);
});
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_DEFAULT_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(nics, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
});
}
function convertCidrSuffix(netmask) {
var bitmask = netmask.split('.').map(function (octet) {
return (+octet).toString(2);
}).join('');
var i = 0;
for (i = 0; i < bitmask.length; i++) {
if (bitmask[i] === '0')
break;
}
return i;
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
do_list.help = [
'Show all NICs on an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Filters:',
' FIELD=<string> String filter. Supported fields: ip, mac, state,',
' network, netmask',
'',
'Filters are applied client-side (i.e. done by the triton command itself).'
].join('\n');
do_list.completionArgtypes = ['tritoninstance', 'none'];
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -0,0 +1,50 @@
/*
* 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 2018 Joyent, Inc.
*
* `triton inst nic ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function NicCLI(top) {
this.top = top.top;
Cmdln.call(this, {
name: top.name + ' nic',
desc: 'List and manage instance NICs.',
helpSubcmds: [
'help',
'list',
'get',
'create',
'delete'
],
helpOpts: {
minHelpCol: 23
}
});
}
util.inherits(NicCLI, Cmdln);
NicCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
NicCLI.prototype.do_list = require('./do_list');
NicCLI.prototype.do_create = require('./do_create');
NicCLI.prototype.do_get = require('./do_get');
NicCLI.prototype.do_delete = require('./do_delete');
module.exports = NicCLI;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2015 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*
* `triton instance ...`
*/
@ -45,10 +45,14 @@ function InstanceCLI(top) {
'enable-firewall',
'disable-firewall',
{ group: '' },
'enable-deletion-protection',
'disable-deletion-protection',
{ group: '' },
'ssh',
'ip',
'wait',
'audit',
'nic',
'snapshot',
'tag'
]
@ -77,10 +81,16 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall');
InstanceCLI.prototype.do_enable_deletion_protection =
require('./do_enable_deletion_protection');
InstanceCLI.prototype.do_disable_deletion_protection =
require('./do_disable_deletion_protection');
InstanceCLI.prototype.do_ssh = require('./do_ssh');
InstanceCLI.prototype.do_ip = require('./do_ip');
InstanceCLI.prototype.do_wait = require('./do_wait');
InstanceCLI.prototype.do_audit = require('./do_audit');
InstanceCLI.prototype.do_nic = require('./do_nic');
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
InstanceCLI.prototype.do_tag = require('./do_tag');

View File

@ -47,13 +47,7 @@ function do_add(subcmd, opts, args, cb) {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
ctx.data = stdin;
ctx.from = '<stdin>';
next();

View File

@ -93,12 +93,8 @@ function _createProfile(opts, cb) {
next();
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -23,7 +23,8 @@ function do_docker_setup(subcmd, opts, args, cb) {
cli: this.top,
name: profileName,
implicit: false,
yes: opts.yes
yes: opts.yes,
lifetime: opts.lifetime
}, cb);
}
@ -33,6 +34,11 @@ do_docker_setup.options = [
type: 'bool',
help: 'Show this help.'
},
{
names: ['lifetime', 't'],
type: 'number',
help: 'Lifetime of the generated docker certificate, in days'
},
{
names: ['yes', 'y'],
type: 'bool',

View File

@ -24,6 +24,7 @@ var rimraf = require('rimraf');
var semver = require('semver');
var sshpk = require('sshpk');
var mod_url = require('url');
var crypto = require('crypto');
var vasync = require('vasync');
var which = require('which');
var wordwrap = require('wordwrap')(78);
@ -128,7 +129,6 @@ function setCurrentProfile(opts, cb) {
});
}
/**
* Setup the given profile for Docker usage. This means checking the cloudapi
* has a Docker service (ListServices), finding the user's SSH *private* key,
@ -143,14 +143,21 @@ function setCurrentProfile(opts, cb) {
* implicit, we silently skip if ListServices shows no Docker service.
* - {Boolean} yes: Optional. Boolean indicating if confirmation prompts
* should be skipped, assuming a "yes" answer.
* - {Number} lifetime: Optional. Number of days to make the Docker
* certificate valid for. Defaults to 3650 (10 years).
*/
function profileDockerSetup(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.optionalBool(opts.implicit, 'opts.implicit');
assert.optionalBool(opts.yes, 'opts.yes');
assert.optionalNumber(opts.lifetime, 'opts.lifetime');
assert.func(cb, 'cb');
/* Default to a 10 year certificate. */
if (!opts.lifetime)
opts.lifetime = 3650;
var cli = opts.cli;
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
@ -165,13 +172,17 @@ function profileDockerSetup(opts, cb) {
function dockerKeyWarning(arg, next) {
console.log(wordwrap('WARNING: Docker uses authentication via ' +
'client TLS certificates that do not support encrypted ' +
'(passphrase protected) keys or SSH agents. If you continue, ' +
'this profile setup will attempt to write a copy of your ' +
'SSH private key formatted as an unencrypted TLS certificate ' +
'in "~/.spearhead/docker" for use by the Docker client.\n'));
'(passphrase protected) keys or SSH agents.\n'));
console.log(wordwrap('If you continue, this profile setup will ' +
'create a fresh private key to be written unencrypted to ' +
'disk in "~/.spearhead/docker" for use by the Docker client. ' +
'This key will be useable only for Docker.\n'));
if (yes) {
next();
return;
} else {
console.log(wordwrap('If you do not specifically want to use ' +
'Docker, you can answer "no" here.\n'));
}
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
if (answer !== 'y') {
@ -311,79 +322,143 @@ function profileDockerSetup(opts, cb) {
});
},
/*
* We need the private key to format as a client cert. If this profile's
* key was found in the SSH agent (and by default it prefers to take
* it from there), then we can't use `tritonapi.keyPair`, because
* the SSH agent protocol will not allow us access to the private key
* data (by design).
*
* As a fallback we'll look (via KeyRing) for a local copy of the
* private key to use, and then unlock it if necessary.
*/
function getPrivKey(arg, next) {
// If the key pair already works, then use that...
try {
arg.privKey = tritonapi.keyPair.getPrivateKey();
next();
return;
} catch (_) {
// ... else fall through.
}
function getSigningKey(arg, next) {
var kr = new auth.KeyRing();
var profileFp = sshpk.parseFingerprint(tritonapi.profile.keyId);
kr.find(profileFp, function (findErr, keyPairs) {
var profileFp = sshpk.parseFingerprint(profile.keyId);
kr.findSigningKeyPair(profileFp,
function unlockAndStash(findErr, keyPair) {
if (findErr) {
next(findErr);
return;
}
/*
* If our keyId was found, and with the 'homedir' plugin, then
* we should have access to the private key (modulo unlocking).
*/
var homedirKeyPair;
for (var i = 0; i < keyPairs.length; i++) {
if (keyPairs[i].plugin === 'homedir') {
homedirKeyPair = keyPairs[i];
break;
}
}
if (homedirKeyPair) {
common.promptPassphraseUnlockKey({
// Fake the `tritonapi` object, only `.keyPair` is used.
tritonapi: {keyPair: homedirKeyPair}
}, function (unlockErr) {
if (unlockErr) {
next(unlockErr);
return;
}
try {
arg.privKey = homedirKeyPair.getPrivateKey();
} catch (homedirErr) {
next(new errors.SetupError(homedirErr, format(
'could not obtain SSH private key for keyId ' +
'"%s" to create Docker certificate',
profile.keyId)));
return;
}
next();
});
} else {
next(new errors.SetupError(format('could not obtain SSH ' +
'private key for keyId "%s" to create Docker ' +
'certificate', profile.keyId)));
arg.signKeyPair = keyPair;
if (!keyPair.isLocked()) {
next();
return;
}
common.promptPassphraseUnlockKey({
/* Fake the `tritonapi` object, only `.keyPair` is used. */
tritonapi: { keyPair: keyPair }
}, next);
});
},
function generateAndSignCert(arg, next) {
var key = arg.signKeyPair;
var pubKey = key.getPublicKey();
function genClientCert_dir(arg, next) {
/*
* There isn't a particular reason this has to be ECDSA, but
* Docker supports it, and ECDSA keys are much easier to
* generate from inside node than RSA ones (since sshpk will
* do them for us instead of us shelling out and mucking with
* temporary files).
*/
arg.privKey = sshpk.generatePrivateKey('ecdsa');
var id = sshpk.identityFromDN('CN=' + profile.account);
var parentId = sshpk.identityFromDN('CN=' +
pubKey.fingerprint('md5').toString('base64'));
var serial = crypto.randomBytes(8);
/*
* Backdate the certificate by 5 minutes to account for clock
* sync -- we only allow 5 mins drift in cloudapi generally so
* using the same amount here seems fine.
*/
var validFrom = new Date();
validFrom.setTime(validFrom.getTime() - 300*1000);
var validUntil = new Date();
validUntil.setTime(validFrom.getTime() +
24*3600*1000*opts.lifetime);
/*
* Generate it self-signed for now -- we will clear this
* signature out and replace it with the real one below.
*/
var cert = sshpk.createCertificate(id, arg.privKey, parentId,
arg.privKey, { validFrom: validFrom, validUntil: validUntil,
purposes: ['clientAuth', 'joyentDocker'], serial: serial });
var algo = pubKey.type + '-' + pubKey.defaultHashAlgorithm();
/*
* This code is using private API in sshpk because there is
* no public API as of 1.14.x for async signing of certificates.
*
* If the sshpk version in package.json is updated (even a
* patch bump) this code could break! This will be fixed up
* eventually, but for now we just have to be careful.
*/
var x509 = require('sshpk/lib/formats/x509');
cert.signatures = {};
cert.signatures.x509 = {};
cert.signatures.x509.algo = algo;
var signer = key.createSign({
user: profile.account,
algorithm: algo
});
/*
* The smartdc-auth KeyPair signer produces an object with
* strings on it intended for http-signature instead of just a
* Signature instance (which is what the x509 format module
* expects). We wrap it up here to convert it.
*/
var signerConv = function (buf, ccb) {
signer(buf, function convertSignature(signErr, sigData) {
if (signErr) {
ccb(signErr);
return;
}
var algparts = sigData.algorithm.split('-');
var sig = sshpk.parseSignature(sigData.signature,
algparts[0], 'asn1');
sig.hashAlgorithm = algparts[1];
sig.curve = pubKey.curve;
ccb(null, sig);
});
};
/*
* Sign a "test" string first to double-check the hash algo
* it's going to use. The SSH agent may not support SHA256
* signatures, for example, and we will only find out by
* testing like this.
*/
signer('test', function afterTestSig(testErr, testSigData) {
if (testErr) {
next(new errors.SetupError(testErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.signatures.x509.algo = testSigData.algorithm;
x509.signAsync(cert, signerConv,
function afterCertSign(signErr) {
if (signErr) {
next(new errors.SetupError(signErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.issuerKey = undefined;
/* Double-check that it came out ok. */
assert.ok(cert.isSignedByKey(pubKey));
arg.cert = cert;
next();
});
});
},
function makeClientCertDir(arg, next) {
arg.dockerCertPath = path.resolve(cli.configDir,
'docker', common.profileSlug(profile));
mkdirp(arg.dockerCertPath, next);
},
function genClientCert_key(arg, next) {
function writeClientCertKey(arg, next) {
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
var data = arg.privKey.toBuffer('pkcs1');
fs.writeFile(arg.keyPath, data, function (err) {
@ -395,12 +470,9 @@ function profileDockerSetup(opts, cb) {
}
});
},
function genClientCert_cert(arg, next) {
function writeClientCert(arg, next) {
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
var id = sshpk.identityFromDN('CN=' + profile.account);
var cert = sshpk.createSelfSignedCertificate(id, arg.privKey);
var data = cert.toBuffer('pem');
var data = arg.cert.toBuffer('pem');
fs.writeFile(arg.certPath, data, function (err) {
if (err) {

View File

@ -125,12 +125,8 @@ function _addUserKey(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
ctx.data = stdin;
ctx.from = '<stdin>';
next();

View File

@ -291,12 +291,8 @@ function _addPolicy(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -287,12 +287,8 @@ function _addRole(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -282,12 +282,8 @@ function _addUser(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -133,7 +133,7 @@ var errors = require('./errors');
// ---- globals
var CLOUDAPI_ACCEPT_VERSION = '~8||~7';
var CLOUDAPI_ACCEPT_VERSION = '~8';
@ -1364,6 +1364,92 @@ function disableInstanceFirewall(opts, cb) {
};
// ---- instance enable/disable deletion protection
/**
* Enable deletion protection on an instance.
*
* @param {Object} opts
* - {String} id: Required. The instance ID, name, or short ID.
* @param {Function} callback `function (err, fauxInst, res)`
* On failure `err` is an error instance, else it is null.
* On success: `fauxInst` is an object with just the instance id,
* `{id: <instance UUID>}` and `res` is the CloudAPI
* `EnableMachineDeletionProtection` response.
* The API call does not return the instance/machine object, hence we
* are limited to just the id for `fauxInst`.
*/
TritonApi.prototype.enableInstanceDeletionProtection =
function enableInstanceDeletionProtection(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var res;
var fauxInst;
function enableDeletionProtection(arg, next) {
fauxInst = {id: arg.instId};
self.cloudapi.enableMachineDeletionProtection(arg.instId,
function enableCb(err, _, _res) {
res = _res;
next(err);
});
}
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
enableDeletionProtection
]}, function vasyncCb(err) {
cb(err, fauxInst, res);
});
};
/**
* Disable deletion protection on an instance.
*
* @param {Object} opts
* - {String} id: Required. The instance ID, name, or short ID.
* @param {Function} callback `function (err, fauxInst, res)`
* On failure `err` is an error instance, else it is null.
* On success: `fauxInst` is an object with just the instance id,
* `{id: <instance UUID>}` and `res` is the CloudAPI
* `DisableMachineDeletionProtectiomn` response.
* The API call does not return the instance/machine object, hence we
* are limited to just the id for `fauxInst`.
*/
TritonApi.prototype.disableInstanceDeletionProtection =
function disableInstanceDeletionProtection(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var res;
var fauxInst;
function disableDeletionProtection(arg, next) {
fauxInst = {id: arg.instId};
self.cloudapi.disableMachineDeletionProtection(arg.instId,
function (err, _, _res) {
res = _res;
next(err);
});
}
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
disableDeletionProtection
]}, function vasyncCb(err) {
cb(err, fauxInst, res);
});
};
// ---- instance snapshots
/**
@ -2029,6 +2115,230 @@ function deleteAllInstanceTags(opts, cb) {
};
// ---- nics
/**
* Add a NIC on a network to an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Object|String} network: The network object or ID, name, or short ID.
* Required.
* @param {Function} callback `function (err, nic, res)`
*/
TritonApi.prototype.addNic =
function addNic(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network');
assert.func(cb, 'cb');
var self = this;
var pipeline = [];
var res;
var nic;
switch (typeof (opts.network)) {
case 'string':
pipeline.push(_stepNetId);
break;
case 'object':
break;
default:
throw new Error('unexpected opts.network: ' + opts.network);
}
pipeline.push(_stepInstId);
pipeline.push(function createNic(arg, next) {
self.cloudapi.addNic({
id: arg.instId,
network: arg.netId || arg.network
}, function onCreateNic(err, _nic, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
res.netId = arg.netId; // ditto
nic = _nic;
next(err);
});
});
var pipelineArg = {
client: self,
id: opts.id,
network: opts.network
};
vasync.pipeline({
arg: pipelineArg,
funcs: pipeline
}, function (err) {
cb(err, nic, res);
});
};
/**
* List an instance's NICs.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* @param {Function} callback `function (err, nics, res)`
*/
TritonApi.prototype.listNics =
function listNics(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var res;
var nics;
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
function list(arg, next) {
self.cloudapi.listNics({
id: arg.instId
}, function (err, _nics, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
nics = _nics;
next(err);
});
}
]}, function (err) {
cb(err, nics, res);
});
};
/**
* Get a NIC belonging to an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {String} mac: The NIC's MAC address. Required.
* @param {Function} callback `function (err, nic, res)`
*/
TritonApi.prototype.getNic =
function getNic(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var self = this;
var res;
var nic;
vasync.pipeline({arg: {client: self, id: opts.id, mac: opts.mac}, funcs: [
_stepInstId,
function get(arg, next) {
self.cloudapi.getNic({
id: arg.instId,
mac: arg.mac
}, function (err, _nic, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
nic = _nic;
next(err);
});
}
]}, function (err) {
cb(err, nic, res);
});
};
/**
* Remove a NIC from an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {String} mac: The NIC's MAC address. Required.
* @param {Function} callback `function (err, res)`
*
*/
TritonApi.prototype.removeNic =
function removeNic(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var self = this;
var res;
vasync.pipeline({arg: {client: self, id: opts.id, mac: opts.mac}, funcs: [
_stepInstId,
function deleteNic(arg, next) {
self.cloudapi.removeNic({
id: arg.instId,
mac: arg.mac
}, function (err, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* Wrapper for cloudapi2's waitForNicStates that will first translate
* opts.id into the proper uuid from shortid/name.
*
* @param {Object} options
* - {String} id {required} machine id
* - {String} mac {required} mac for new nic
* - {Array of String} states - desired state
* @param {Function} callback of the form f(err, nic, res).
*/
TritonApi.prototype.waitForNicStates = function waitForNicStates(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.arrayOfString(opts.states, 'opts.states');
var self = this;
var nic, res;
function waitForNic(arg, next) {
var _opts = {
id: arg.instId,
mac: arg.mac,
states: arg.states
};
self.cloudapi.waitForNicStates(_opts,
function onWaitForNicState(err, _nic, _res) {
res = _res;
nic = _nic;
next(err);
});
}
var pipelineArgs = {
client: self,
id: opts.id,
mac: opts.mac,
states: opts.states
};
vasync.pipeline({
arg: pipelineArgs,
funcs: [
_stepInstId,
waitForNic
]
}, function onWaitForNicPipeline(err) {
cb(err, nic, res);
});
};
// ---- Firewall Rules
/**

View File

@ -1,7 +1,7 @@
{
"name": "spearhead",
"description": "Spearhead Cloud CLI and client (https://spearhead.cloud)",
"version": "5.8.0",
"version": "6.0.0",
"author": "Spearhead Systems (spearhead.systems)",
"homepage": "https://code.spearhead.cloud/Spearhead/node-spearhead",
"dependencies": {
@ -21,9 +21,9 @@
"restify-errors": "3.0.0",
"rimraf": "2.4.4",
"semver": "5.1.0",
"smartdc-auth": "2.5.6",
"sshpk": "1.10.2",
"sshpk-agent": "1.4.2",
"smartdc-auth": "2.5.7",
"sshpk": "1.14.1",
"sshpk-agent": "1.7.0",
"strsplit": "1.0.0",
"tabula": "1.10.0",
"vasync": "1.6.3",

View File

@ -26,11 +26,6 @@
// to true.
"skipAffinityTests": false,
// Optional. Set to 'true' to skip testing of bhyve things. Some DCs might
// not support bhyve (no packages or images available, and/or no CNs with
// bhyve compatible hardware).
"skipBhyveTests": false,
// Optional. Set to 'true' to skip testing of KVM things. Some DCs might
// not support KVM (no KVM packages or images available).
"skipKvmTests": false,
@ -41,12 +36,6 @@
"resizePackage": "<package name>",
"image": "<image uuid, name or name@version>"
// The params used for test *bhyve* provisions. By default the tests use:
// the smallest RAM package with "kvm" in the name, the latest
// ubuntu-certified image.
"bhyvePackage": "<package name or uuid>",
"bhyveImage": "<image uuid, name or name@version>",
// The params used for test *KVM* provisions. By default the tests use:
// the smallest RAM package with "kvm" in the name, the latest
// ubuntu-certified image.

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 2018 Joyent, Inc.
*/
/*
* Integration tests for using NIC-related APIs as a module.
*/
var h = require('./helpers');
var test = require('tape');
// --- Globals
var CLIENT;
var INST;
var NIC;
// --- Tests
test('TritonApi networks', function (tt) {
tt.test(' setup', function (t) {
h.createClient(function (err, client_) {
t.error(err);
CLIENT = client_;
t.end();
});
});
tt.test(' setup: inst', function (t) {
CLIENT.cloudapi.listMachines({}, function (err, vms) {
if (vms.length === 0)
return t.end();
t.ok(Array.isArray(vms), 'vms array');
INST = vms[0];
t.end();
});
});
tt.test(' TritonApi listNics', function (t) {
if (!INST)
return t.end();
function check(val, valName, next) {
CLIENT.listNics({id: val}, function (err, nics) {
if (h.ifErr(t, err, 'no err ' + valName))
return t.end();
t.ok(Array.isArray(nics), 'nics array');
NIC = nics[0];
next();
});
}
var shortId = INST.id.split('-')[0];
check(INST.id, 'id', function () {
check(INST.name, 'name', function () {
check(shortId, 'shortId', function () {
t.end();
});
});
});
});
tt.test(' TritonApi getNic', function (t) {
if (!NIC)
return t.end();
function check(inst, mac, instValName, next) {
CLIENT.getNic({id: inst, mac: mac}, function (err, nic) {
if (h.ifErr(t, err, 'no err for ' + instValName))
return t.end();
t.deepEqual(nic, NIC, instValName);
next();
});
}
var shortId = INST.id.split('-')[0];
check(INST.id, NIC.mac, 'id', function () {
check(INST.name, NIC.mac, 'name', function () {
check(shortId, NIC.mac, 'shortId', function () {
t.end();
});
});
});
});
tt.test(' teardown: client', function (t) {
CLIENT.close();
t.end();
});
});

View File

@ -81,7 +81,8 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
var db0Alias = ALIAS_PREFIX + '-db0';
var db0;
tt.test(' setup: triton create -n db0', function (t) {
var argv = ['create', '-wj', '-n', db0Alias, imgId, pkgId];
var argv = ['create', '-wj', '-n', db0Alias, '-t', 'role=database',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db0 = lines[1];
@ -92,9 +93,9 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
// Test db1 being put on same server as db0.
var db1Alias = ALIAS_PREFIX + '-db1';
var db1;
tt.test(' setup: triton create -n db1 -a db0', function (t) {
var argv = ['create', '-wj', '-n', db1Alias, '-a', db0Alias,
imgId, pkgId];
tt.test(' triton create -n db1 -a instance==db0', function (t) {
var argv = ['create', '-wj', '-n', db1Alias, '-a',
'instance==' + db0Alias, imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db1 = lines[1];
@ -105,11 +106,11 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
});
});
// Test db2 being put on server *other* than db0.
// Test db2 being put on a server without a db.
var db2Alias = ALIAS_PREFIX + '-db2';
var db2;
tt.test(' setup: triton create -n db2 -a \'inst!=db0\'', function (t) {
var argv = ['create', '-wj', '-n', db2Alias, '-a', 'inst!='+db0Alias,
tt.test(' triton create -n db2 -a \'instance!=db*\'', function (t) {
var argv = ['create', '-wj', '-n', db2Alias, '-a', 'instance!=db*',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
@ -121,11 +122,45 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
});
});
// Test db3 being put on server *other* than db0.
var db3Alias = ALIAS_PREFIX + '-db3';
var db3;
tt.test(' triton create -n db3 -a \'instance!=db0\'', function (t) {
var argv = ['create', '-wj', '-n', db3Alias, '-a',
'instance!='+db0Alias, imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db3 = lines[1];
t.notEqual(db0.compute_node, db3.compute_node,
format('inst %s landed on different CN (%s) as inst %s (%s)',
db3Alias, db3.compute_node, db0Alias, db0.compute_node));
t.end();
});
});
// Test db4 being put on server *other* than db0 (due ot db0's tag).
var db4Alias = ALIAS_PREFIX + '-db4';
var db4;
tt.test(' triton create -n db4 -a \'role!=database\'', function (t) {
var argv = ['create', '-wj', '-n', db4Alias, '-a', 'role!=database',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db4 = lines[1];
t.notEqual(db0.compute_node, db4.compute_node,
format('inst %s landed on different CN (%s) as inst %s (%s)',
db4Alias, db4.compute_node, db0Alias, db0.compute_node));
t.end();
});
});
// Remove instances. Add a test 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', {timeout: 10 * 60 * 1000}, function (t) {
h.safeTriton(t, ['rm', '-w', db0.id, db1.id, db2.id], function () {
h.safeTriton(t, ['rm', '-w', db0.id, db1.id, db2.id, db3.id, db4.id],
function () {
t.end();
});
});

View File

@ -0,0 +1,191 @@
/*
* 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) 2018, Joyent, Inc.
*/
/*
* Integration tests for `triton instance enable-deletion-protection ...` and
* `triton instance disable-deletion-protection ...`
*/
var h = require('./helpers');
var f = require('util').format;
var os = require('os');
var test = require('tape');
// --- Globals
var INST_ALIAS = f('nodetritontest-deletion-protection-%s', os.hostname());
var INST;
var OPTS = {
skip: !h.CONFIG.allowWriteActions
};
// --- Helpers
function cleanup(t) {
var cmd = 'instance disable-deletion-protection ' + INST_ALIAS + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (err)
return t.end();
h.deleteTestInst(t, INST_ALIAS, function (err2) {
t.ifErr(err2, 'delete inst err');
t.end();
});
});
}
// --- Tests
if (OPTS.skip) {
console.error('** skipping %s tests', __filename);
console.error('** set "allowWriteActions" in test config to enable');
}
test('triton instance', OPTS, function (tt) {
h.printConfig(tt);
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, cleanup);
tt.test(' triton create --deletion-protection', function (t) {
h.createTestInst(t, INST_ALIAS, {
extraFlags: ['--deletion-protection']
}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();
INST = instId;
h.triton('instance get -j ' + INST, function (err3, stdout) {
if (h.ifErr(t, err3, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout);
t.ok(inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' attempt to delete deletion-protected instance', function (t) {
var cmd = 'instance rm ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
t.ok(err, 'err expected');
/* JSSTYLED */
t.ok(stderr.match(/Instance has "deletion_protection" enabled/));
t.end();
});
});
tt.test(' triton instance disable-deletion-protection', function (t) {
var cmd = 'instance disable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance disable-deletion-protection'))
return t.end();
t.ok(stdout.match('Disabled deletion protection for instance "' +
INST + '"'), 'deletion protection 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.ok(!inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance disable-deletion-protection (already enabled)',
function (t) {
var cmd = 'instance disable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance disable-deletion-protection'))
return t.end();
t.ok(stdout.match('Disabled deletion protection for instance "' +
INST + '"'), 'deletion protection 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.ok(!inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance enable-deletion-protection', function (t) {
var cmd = 'instance enable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance enable-deletion-protection'))
return t.end();
t.ok(stdout.match('Enabled deletion protection for instance "' +
INST + '"'), 'deletion protection 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.ok(inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance enable-deletion-protection (already enabled)',
function (t) {
var cmd = 'instance enable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance enable-deletion-protection'))
return t.end();
t.ok(stdout.match('Enabled deletion protection for instance "' +
INST + '"'), 'deletion protection 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.ok(inst.deletion_protection, 'deletion_protection');
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}, cleanup);
});

View File

@ -47,7 +47,7 @@ test('triton fwrule', OPTS, function (tt) {
});
tt.test(' setup: triton create', function (t) {
h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();

View File

@ -0,0 +1,254 @@
/*
* 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) 2018, Joyent, Inc.
*/
/*
* Integration tests for `triton instance nics ...`
*/
var h = require('./helpers');
var f = require('util').format;
var os = require('os');
var test = require('tape');
// --- Globals
var INST_ALIAS = f('nodetritontest-nics-%s', os.hostname());
var NETWORK;
var INST;
var NIC;
var NIC2;
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 nics', 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(err, instId) {
if (h.ifErr(t, err, 'triton instance create'))
return t.end();
t.ok(instId, 'created instance ' + instId);
INST = instId;
t.end();
});
});
tt.test(' setup: find network for tests', function (t) {
h.triton('network list -j', function onNetworks(err, stdout) {
if (h.ifErr(t, err, 'triton network list'))
return t.end();
NETWORK = JSON.parse(stdout.trim().split('\n')[0]);
t.ok(NETWORK, 'NETWORK');
t.end();
});
});
tt.test(' triton instance nic create', function (t) {
var cmd = 'instance nic create -j -w ' + INST + ' ' + NETWORK.id;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create'))
return t.end();
NIC = JSON.parse(stdout);
t.ok(NIC, 'created NIC: ' + stdout.trim());
t.end();
});
});
tt.test(' triton instance nic get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get'))
return t.end();
var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC.mac, 'nic MAC is correct');
t.equal(obj.ip, NIC.ip, 'nic IP is correct');
t.equal(obj.network, NIC.network, 'nic network is correct');
t.end();
});
});
tt.test(' triton instance nic list', function (t) {
var cmd = 'instance nic list ' + INST;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list'))
return t.end();
var nics = stdout.trim().split('\n');
t.ok(nics[0].match(/IP\s+MAC\s+STATE\s+NETWORK/), 'nic list' +
' header correct');
nics.shift();
t.ok(nics.length >= 1, 'triton nic list expected nic num');
var testNics = nics.filter(function (nic) {
return nic.match(NIC.mac);
});
t.equal(testNics.length, 1, 'triton nic list test nic found');
t.end();
});
});
tt.test(' triton instance nic list -j', function (t) {
var cmd = 'instance nic list -j ' + INST;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list'))
return t.end();
var nics = stdout.trim().split('\n').map(function (line) {
return JSON.parse(line);
});
t.ok(nics.length >= 1, 'triton nic list expected nic num');
var testNics = nics.filter(function (nic) {
return nic.mac === NIC.mac;
});
t.equal(testNics.length, 1, 'triton nic list test nic found');
t.end();
});
});
tt.test(' triton instance nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
var nics = stdout.trim().split('\n').map(function (str) {
return JSON.parse(str);
});
t.equal(nics.length, 1);
t.equal(nics[0].ip, NIC.ip);
t.equal(nics[0].network, NIC.network);
t.end();
});
});
tt.test(' triton nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
var nics = stdout.trim().split('\n').map(function (str) {
return JSON.parse(str);
});
t.equal(nics.length, 1);
t.equal(nics[0].ip, NIC.ip);
t.equal(nics[0].network, NIC.network);
t.end();
});
});
tt.test(' triton instance nic delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic delete'))
return t.end();
t.ok(stdout.match('Deleted NIC ' + NIC.mac, 'deleted nic'));
t.end();
});
});
tt.test(' triton instance nic create (with NICOPTS)', function (t) {
var cmd = 'instance nic create -j -w ' + INST + ' ipv4_uuid=' +
NETWORK.id;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create'))
return t.end();
NIC2 = JSON.parse(stdout);
t.end();
});
});
tt.test(' triton instance nic with ip get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get'))
return t.end();
var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC2.mac, 'nic MAC is correct');
t.equal(obj.ip, NIC2.ip, 'nic IP is correct');
t.equal(obj.network, NIC2.network, 'nic network is correct');
t.end();
});
});
tt.test(' triton instance nic with ip delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic with ip delete'))
return t.end();
t.ok(stdout.match('Deleted NIC ' + NIC2.mac, 'deleted nic'));
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

@ -44,7 +44,7 @@ test('triton instance snapshot', OPTS, function (tt) {
});
tt.test(' setup: triton instance create', function (t) {
h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();

View File

@ -45,6 +45,8 @@ var subs = [
['instance delete', 'instance rm', 'delete', 'rm'],
['instance enable-firewall'],
['instance disable-firewall'],
['instance enable-deletion-protection'],
['instance disable-deletion-protection'],
['instance rename'],
['instance ssh'],
['instance ip'],
@ -56,6 +58,10 @@ var subs = [
['instance snapshot list', 'instance snapshot ls', 'instance snapshots'],
['instance snapshot get'],
['instance snapshot delete', 'instance snapshot rm'],
['instance nic create'],
['instance nic list', 'instance nic ls'],
['instance nic get'],
['instance nic delete', 'instance nic rm'],
['ip'],
['ssh'],
['network'],

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2018 Joyent, Inc.
* Copyright 2017 Joyent, Inc.
*/
/*
@ -211,46 +211,6 @@ function getTestImg(t, cb) {
}
/*
* Find and return an image that can be used for test *bhyve* provisions.
*
* @param {Tape} t - tape test object
* @param {Function} cb - `function (err, imgId)`
* where `imgId` is an image identifier (an image name, shortid, or id).
*/
function getTestBhyveImg(t, cb) {
if (CONFIG.bhyveImage) {
assert.string(CONFIG.bhyvePackage, 'CONFIG.bhyvePackage');
t.ok(CONFIG.bhyveImage, 'bhyveImage from config: ' + CONFIG.bhyveImage);
cb(null, CONFIG.bhyveImage);
return;
}
var candidateImageNames = {
'ubuntu-certified-16.04': true
};
safeTriton(t, ['img', 'ls', '-j'], function (err, stdout) {
var imgId;
var imgs = jsonStreamParse(stdout);
// Newest images first.
tabula.sortArrayOfObjects(imgs, ['-published_at']);
var imgRepr;
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
if (candidateImageNames[img.name]) {
imgId = img.id;
imgRepr = f('%s@%s', img.name, img.version);
break;
}
}
t.ok(imgId,
f('latest bhyve image (using subset of supported names): %s (%s)',
imgId, imgRepr));
cb(err, imgId);
});
}
/*
* Find and return an image that can be used for test *KVM* provisions.
*
@ -321,38 +281,6 @@ function getTestPkg(t, cb) {
});
}
/*
* Find and return an package that can be used for *bhyve* test provisions.
*
* @param {Tape} t - tape test object
* @param {Function} cb - `function (err, pkgId)`
* where `pkgId` is an package identifier (a name, shortid, or id).
*/
function getTestBhyvePkg(t, cb) {
if (CONFIG.bhyvePackage) {
assert.string(CONFIG.bhyvePackage, 'CONFIG.bhyvePackage');
t.ok(CONFIG.bhyvePackage, 'bhyvePackage from config: ' +
CONFIG.bhyvePackage);
cb(null, CONFIG.bhyvePackage);
return;
}
// bhyve uses the same packages as kvm
safeTriton(t, ['pkg', 'ls', '-j'], function (err, stdout) {
var pkgs = jsonStreamParse(stdout);
// Filter on those with 'kvm' in the name.
pkgs = pkgs.filter(function (pkg) {
return pkg.name.indexOf('kvm') !== -1;
});
// Smallest RAM first.
tabula.sortArrayOfObjects(pkgs, ['memory']);
var pkgId = pkgs[0].id;
t.ok(pkgId, f('smallest (RAM) available kvm package: %s (%s)',
pkgId, pkgs[0].name));
cb(null, pkgId);
});
}
/*
* Find and return an package that can be used for *KVM* test provisions.
*
@ -441,7 +369,13 @@ function createClient(cb) {
/*
* Create a small test instance.
*/
function createTestInst(t, name, cb) {
function createTestInst(t, name, opts, cb) {
assert.object(t, 't');
assert.string(name, 'name');
assert.object(opts, 'opts');
assert.optionalArrayOfString(opts.extraFlags, 'opts.extraFlags');
assert.func(cb, 'cb');
getTestPkg(t, function (err, pkgId) {
t.ifErr(err);
if (err) {
@ -457,6 +391,10 @@ function createTestInst(t, name, cb) {
}
var cmd = f('instance create -w -n %s %s %s', name, imgId, pkgId);
if (opts.extraFlags) {
cmd += ' ' + opts.extraFlags.join(' ');
}
triton(cmd, function (err3, stdout) {
t.ifErr(err3, 'create test instance');
if (err3) {
@ -583,10 +521,8 @@ module.exports = {
deleteTestImg: deleteTestImg,
getTestImg: getTestImg,
getTestBhyveImg: getTestBhyveImg,
getTestKvmImg: getTestKvmImg,
getTestPkg: getTestPkg,
getTestBhyvePkg: getTestBhyvePkg,
getTestKvmPkg: getTestKvmPkg,
getResizeTestPkg: getResizeTestPkg,