merge with latest from upstream
This commit is contained in:
commit
5438723d06
60
CHANGES.md
60
CHANGES.md
@ -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
|
||||
|
217
lib/cloudapi2.js
217
lib/cloudapi2.js
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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".'
|
||||
|
@ -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();
|
||||
},
|
||||
|
||||
|
@ -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',
|
||||
|
125
lib/do_instance/do_disable_deletion_protection.js
Normal file
125
lib/do_instance/do_disable_deletion_protection.js
Normal 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;
|
125
lib/do_instance/do_enable_deletion_protection.js
Normal file
125
lib/do_instance/do_enable_deletion_protection.js
Normal 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;
|
@ -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".',
|
||||
|
211
lib/do_instance/do_nic/do_create.js
Normal file
211
lib/do_instance/do_nic/do_create.js
Normal 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;
|
126
lib/do_instance/do_nic/do_delete.js
Normal file
126
lib/do_instance/do_nic/do_delete.js
Normal 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;
|
89
lib/do_instance/do_nic/do_get.js
Normal file
89
lib/do_instance/do_nic/do_get.js
Normal 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;
|
154
lib/do_instance/do_nic/do_list.js
Normal file
154
lib/do_instance/do_nic/do_list.js
Normal 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;
|
50
lib/do_instance/do_nic/index.js
Normal file
50
lib/do_instance/do_nic/index.js
Normal 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;
|
@ -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');
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
312
lib/tritonapi.js
312
lib/tritonapi.js
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
|
110
test/integration/api-nics.test.js
Normal file
110
test/integration/api-nics.test.js
Normal 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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
191
test/integration/cli-deletion-protection.test.js
Normal file
191
test/integration/cli-deletion-protection.test.js
Normal 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);
|
||||
});
|
@ -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();
|
||||
|
||||
|
254
test/integration/cli-nics.test.js
Normal file
254
test/integration/cli-nics.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
|
||||
|
@ -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'],
|
||||
|
@ -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,
|
||||
|
||||
|
Reference in New Issue
Block a user