From f100c4dbb59bfbff5b6e381f8648133a73499e0d Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 19 Jun 2018 14:42:10 -0700 Subject: [PATCH 1/8] TRITON-524 'triton inst get' should support '--credentials' Reviewed by: Marsell Kukuljevic Approved by: Marsell Kukuljevic --- CHANGES.md | 3 +++ lib/cloudapi2.js | 16 ++++++++++------ lib/do_instance/do_get.js | 13 +++++++++++-- lib/tritonapi.js | 13 +++++++++++-- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 51acbd5..0d2c70d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ Known issues: ## not yet released +- [TRITON-524] Add `triton inst get --credentials ...` option to match + `triton inst list --credentials ...` for including generated credentials + in instance metadata. - [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 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 94c57eb..26f80fd 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -872,13 +872,12 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) { /** * Get a machine by id. * - * XXX add getCredentials equivalent - * XXX cloudapi docs don't doc the credentials=true option - * * For backwards compat, calling with `getMachine(id, cb)` is allowed. * * @param {Object} opts - * - id {UUID} Required. The machine id. + * - {UUID} id - Required. The machine id. + * - {Boolean} credentials - Optional. Set to true to include generated + * credentials for this machine in `machine.metadata.credentials`. * @param {Function} cb of the form `function (err, machine, res)` */ CloudApi.prototype.getMachine = function getMachine(opts, cb) { @@ -887,9 +886,14 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) { } assert.object(opts, 'opts'); assert.uuid(opts.id, 'opts.id'); + assert.optionalBool(opts.credentials, 'opts.credentials'); - var endpoint = format('/%s/machines/%s', this.account, opts.id); - this._request(endpoint, function (err, req, res, body) { + var query = {}; + if (opts.credentials) { + query.credentials = 'true'; + } + var p = this._path(format('/%s/machines/%s', this.account, opts.id), query); + this._request(p, function (err, req, res, body) { cb(err, body, res); }); }; diff --git a/lib/do_instance/do_get.js b/lib/do_instance/do_get.js index c7f3c4e..ce22914 100644 --- a/lib/do_instance/do_get.js +++ b/lib/do_instance/do_get.js @@ -25,7 +25,10 @@ function do_get(subcmd, opts, args, cb) { cb(setupErr); return; } - tritonapi.getInstance(args[0], function (err, inst) { + tritonapi.getInstance({ + id: args[0], + credentials: opts.credentials + }, function onInst(err, inst) { if (inst) { if (opts.json) { console.log(JSON.stringify(inst)); @@ -44,6 +47,13 @@ do_get.options = [ type: 'bool', help: 'Show this help.' }, + { + names: ['credentials'], + type: 'bool', + help: 'Include generated credentials, in the "metadata.credentials" ' + + 'field, if any. Typically used with "-j", though one can show ' + + 'values with "-o metadata.credentials".' + }, { names: ['json', 'j'], type: 'bool', @@ -58,7 +68,6 @@ do_get.help = [ '{{usage}}', '', '{{options}}', - '', 'Where "INST" is an instance name, id, or short id.', '', 'A *deleted* instance may still respond with the instance object. In that', diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 7bc46df..92882cc 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -1123,6 +1123,8 @@ TritonApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) { * - {Array} fields: Optional. An array of instance field names that are * wanted by the caller. This *can* allow the implementation to avoid * extra API calls. E.g. `['id', 'name']`. + * - {Boolean} credentials: Optional. Set to true to include generated + * credentials for this instance in `inst.metadata.credentials`. * @param {Function} cb `function (err, inst, res)` * Note that deleted instances will result in `err` being a * `InstanceDeletedError` and `inst` being defined. On success, `res` is @@ -1137,6 +1139,7 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { assert.object(opts, 'opts'); assert.string(opts.id, 'opts.id'); assert.optionalArrayOfString(opts.fields, 'opts.fields'); + assert.optionalBool(opts.credentials, 'opts.credentials'); assert.func(cb, 'cb'); /* @@ -1176,7 +1179,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { return next(); } } - self.cloudapi.getMachine(uuid, function (err, inst_, res_) { + self.cloudapi.getMachine({ + id: uuid, + credentials: opts.credentials + }, function onMachine(err, inst_, res_) { res = res_; inst = inst_; err = errFromGetMachineErr(err); @@ -1264,7 +1270,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { } var uuid = instFromList.id; - self.cloudapi.getMachine(uuid, function (err, inst_, res_) { + self.cloudapi.getMachine({ + id: uuid, + credentials: opts.credentials + }, function onMachine(err, inst_, res_) { res = res_; inst = inst_; err = errFromGetMachineErr(err); From 0bc11c1e335fc06f9439e904add6491b555e5fe3 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Tue, 5 Jun 2018 06:32:58 +0200 Subject: [PATCH 2/8] TRITON-401 Add support for fabric vlans and networks to node-triton Reviewed by: Trent Mick Reviewed by: Julien Gilli Reviewed by: Pedro P. Candel Approved by: Pedro P. Candel --- .gitignore | 1 + CHANGES.md | 2 + lib/cli.js | 4 + lib/cloudapi2.js | 264 ++++++++++++++ lib/do_network/do_create.js | 187 ++++++++++ lib/do_network/do_delete.js | 85 +++++ lib/do_network/do_get_default.js | 78 +++++ lib/do_network/do_list.js | 13 +- lib/do_network/do_set_default.js | 92 +++++ lib/do_network/index.js | 10 +- lib/do_vlan/do_create.js | 123 +++++++ lib/do_vlan/do_delete.js | 85 +++++ lib/do_vlan/do_get.js | 88 +++++ lib/do_vlan/do_list.js | 123 +++++++ lib/do_vlan/do_networks.js | 52 +++ lib/do_vlan/do_update.js | 201 +++++++++++ lib/do_vlan/index.js | 55 +++ lib/tritonapi.js | 264 +++++++++++++- test/integration/api-networks.test.js | 70 +++- test/integration/api-nics.test.js | 52 +-- test/integration/api-vlans.test.js | 120 +++++++ test/integration/cli-networks.test.js | 161 ++++++++- test/integration/cli-nics.test.js | 104 +++--- test/integration/cli-subcommands.test.js | 14 +- test/integration/cli-vlans.test.js | 418 +++++++++++++++++++++++ test/integration/helpers.js | 1 + 26 files changed, 2586 insertions(+), 81 deletions(-) create mode 100644 lib/do_network/do_create.js create mode 100644 lib/do_network/do_delete.js create mode 100644 lib/do_network/do_get_default.js create mode 100644 lib/do_network/do_set_default.js create mode 100644 lib/do_vlan/do_create.js create mode 100644 lib/do_vlan/do_delete.js create mode 100644 lib/do_vlan/do_get.js create mode 100644 lib/do_vlan/do_list.js create mode 100644 lib/do_vlan/do_networks.js create mode 100644 lib/do_vlan/do_update.js create mode 100644 lib/do_vlan/index.js create mode 100644 test/integration/api-vlans.test.js create mode 100644 test/integration/cli-vlans.test.js diff --git a/.gitignore b/.gitignore index 9950c21..cd0b884 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /test/*.json /npm-debug.log /triton-*.tgz +*.swp diff --git a/CHANGES.md b/CHANGES.md index 0d2c70d..36548df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ Known issues: ## not yet released +- [TRITON-401] Add `triton network` and `triton vlan` commands, for + creating/changing/removing network fabrics and VLANs. - [TRITON-524] Add `triton inst get --credentials ...` option to match `triton inst list --credentials ...` for including generated credentials in instance metadata. diff --git a/lib/cli.js b/lib/cli.js index 2b868e5..ca6acd0 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -210,6 +210,7 @@ function CLI() { 'package', 'network', 'fwrule', + 'vlan', { group: 'Other Commands' }, 'info', 'account', @@ -705,6 +706,9 @@ CLI.prototype.do_package = require('./do_package'); CLI.prototype.do_networks = require('./do_networks'); CLI.prototype.do_network = require('./do_network'); +// VLANs +CLI.prototype.do_vlan = require('./do_vlan'); + // Hidden commands CLI.prototype.do_cloudapi = require('./do_cloudapi'); CLI.prototype.do_badger = require('./do_badger'); diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 26f80fd..3deeefd 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -357,6 +357,48 @@ CloudApi.prototype.ping = function ping(opts, cb) { }; +// ---- config + +/** + * Get config object for the current user. + * + * @param {Object} opts + * @param {Function} cb of the form `function (err, config, res)` + */ +CloudApi.prototype.getConfig = function getConfig(opts, cb) { + assert.object(opts, 'opts'); + assert.func(cb, 'cb'); + + var endpoint = this._path(format('/%s/config', this.account)); + this._request(endpoint, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Set config object for the current user. + * + * @param {Object} opts + * - {String} default_network: network fabric docker containers are + * provisioned on. Optional. + * @param {Function} cb of the form `function (err, config, res)` + */ +CloudApi.prototype.updateConfig = function updateConfig(opts, cb) { + assert.object(opts, 'opts'); + assert.optionalUuid(opts.default_network, 'opts.default_network'); + assert.func(cb, 'cb'); + + this._request({ + method: 'PUT', + path: format('/%s/config', this.account), + data: opts + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + // ---- networks /** @@ -458,6 +500,228 @@ CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = { reserved: 'boolean' }; + +// --- Fabric VLANs + +/** + * Creates a network on a fabric (specifically: a fabric VLAN). + * + * @param {Object} options object containing: + * - {Integer} vlan_id (required) VLAN's id, between 0-4095. + * - {String} name (required) A name to identify the network. + * - {String} subnet (required) CIDR description of the network. + * - {String} provision_start_ip (required) First assignable IP addr. + * - {String} provision_end_ip (required) Last assignable IP addr. + * - {String} gateway (optional) Gateway IP address. + * - {String} resolvers (optional) Static routes for hosts on network. + * - {String} description (optional) + * - {Boolean} internet_nat (optional) Whether to provision an Internet + * NAT on the gateway address (default: true). + * @param {Function} callback of the form f(err, vlan, res). + */ +CloudApi.prototype.createFabricNetwork = +function createFabricNetwork(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.string(opts.name, 'opts.name'); + assert.string(opts.subnet, 'opts.subnet'); + assert.string(opts.provision_start_ip, 'opts.provision_start_ip'); + assert.string(opts.provision_end_ip, 'opts.provision_end_ip'); + assert.optionalString(opts.gateway, 'opts.gateway'); + assert.optionalString(opts.resolvers, 'opts.resolvers'); + assert.optionalString(opts.routes, 'opts.routes'); + assert.optionalBool(opts.internet_nat, 'opts.internet_nat'); + + var data = common.objCopy(opts); + var vlanId = data.vlan_id; + delete data.vlan_id; + + this._request({ + method: 'POST', + path: format('/%s/fabrics/default/vlans/%d/networks', this.account, + vlanId), + data: data + }, function reqCb(err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * Lists all networks on a VLAN. + * + * Returns an array of objects. + * + * @param {Object} options object containing: + * - {Integer} vlan_id (required) VLAN's id, between 0-4095. + * @param {Function} callback of the form f(err, networks, res). + */ +CloudApi.prototype.listFabricNetworks = +function listFabricNetworks(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fabrics/default/vlans/%d/networks', + this.account, opts.vlan_id); + this._passThrough(endpoint, opts, cb); +}; + +/** + * Remove a fabric network + * + * @param {Object} opts (object) + * - {String} id: The network id. Required. + * - {Integer} vlan_id: The VLAN id. Required. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deleteFabricNetwork = +function deleteFabricNetwork(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/fabrics/default/vlans/%d/networks/%s', this.account, + opts.vlan_id, opts.id) + }, function (err, req, res) { + cb(err, res); + }); +}; + +/** + * Creates a VLAN on a fabric. + * + * @param {Object} options object containing: + * - {Integer} vlan_id (required) VLAN's id, between 0-4095. + * - {String} name (required) A name to identify the VLAN. + * - {String} description (optional) + * @param {Function} callback of the form f(err, vlan, res). + */ +CloudApi.prototype.createFabricVlan = +function createFabricVlan(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.string(opts.name, 'opts.name'); + assert.optionalString(opts.description, 'opts.description'); + + var data = { + vlan_id: opts.vlan_id + }; + + Object.keys(this.UPDATE_VLAN_FIELDS).forEach(function (attr) { + if (opts[attr] !== undefined) + data[attr] = opts[attr]; + }); + + this._request({ + method: 'POST', + path: format('/%s/fabrics/default/vlans', this.account), + data: data + }, function reqCb(err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * Lists all the VLANs. + * + * Returns an array of objects. + * + * @param opts {Object} Options + * @param {Function} callback of the form f(err, vlans, res). + */ +CloudApi.prototype.listFabricVlans = +function listFabricVlans(opts, cb) { + assert.object(opts, 'opts'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fabrics/default/vlans', this.account); + this._passThrough(endpoint, opts, cb); +}; + +/** + * Retrieves a VLAN. + * + * @param {Integer} id: The VLAN id. + * @param {Function} callback of the form `function (err, vlan, res)` + */ +CloudApi.prototype.getFabricVlan = +function getFabricVlan(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/fabrics/default/vlans/%d', this.account, + opts.vlan_id); + this._request(endpoint, function (err, req, res, body) { + cb(err, body, res); + }); +}; + +// -> +CloudApi.prototype.UPDATE_VLAN_FIELDS = { + name: 'string', + description: 'string' +}; + +/** + * Updates a VLAN. + * + * @param {Object} opts object containing: + * - {Integer} id: The VLAN id. Required. + * - {String} name: The VLAN name. Optional. + * - {String} description: Description of the VLAN. Optional. + * @param {Function} callback of the form `function (err, vlan, res)` + */ +CloudApi.prototype.updateFabricVlan = +function updateFabricVlan(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.optionalString(opts.rule, 'opts.name'); + assert.optionalString(opts.description, 'opts.description'); + assert.func(cb, 'cb'); + + var data = {}; + Object.keys(this.UPDATE_VLAN_FIELDS).forEach(function (attr) { + if (opts[attr] !== undefined) + data[attr] = opts[attr]; + }); + + var vlanId = opts.vlan_id; + + this._request({ + method: 'POST', + path: format('/%s/fabrics/default/vlans/%d', this.account, vlanId), + data: data + }, function onReq(err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * Remove a VLAN. + * + * @param {Object} opts (object) + * - {Integer} vlan_id: The vlan id. Required. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deleteFabricVlan = +function deleteFabricVlan(opts, cb) { + assert.object(opts, 'opts'); + assert.number(opts.vlan_id, 'opts.vlan_id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/fabrics/default/vlans/%d', this.account, opts.vlan_id) + }, function onReq(err, req, res) { + cb(err, res); + }); +}; + + // ---- datacenters /** diff --git a/lib/do_network/do_create.js b/lib/do_network/do_create.js new file mode 100644 index 0000000..71448b5 --- /dev/null +++ b/lib/do_network/do_create.js @@ -0,0 +1,187 @@ +/* + * 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 network create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var jsprim = require('jsprim'); + +var common = require('../common'); +var errors = require('../errors'); + + +var OPTIONAL_OPTS = ['description', 'gateway', 'resolvers', 'routes']; + + +function do_create(subcmd, opts, args, cb) { + assert.optionalString(opts.name, 'opts.name'); + assert.optionalString(opts.subnet, 'opts.subnet'); + assert.optionalString(opts.start_ip, 'opts.start_ip'); + assert.optionalString(opts.end_ip, 'opts.end_ip'); + assert.optionalString(opts.description, 'opts.description'); + assert.optionalString(opts.gateway, 'opts.gateway'); + assert.optionalString(opts.resolvers, 'opts.resolvers'); + assert.optionalString(opts.routes, 'opts.routes'); + assert.optionalBool(opts.no_nat, 'opts.no_nat'); + 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 === 0) { + cb(new errors.UsageError('missing VLAN argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var vlanId = jsprim.parseInteger(args[0], { allowSign: false }); + if (typeof (vlanId) !== 'number') { + cb(new errors.UsageError('VLAN must be an integer')); + return; + } + + var createOpts = { + vlan_id: vlanId, + name: opts.name, + subnet: opts.subnet, + provision_start_ip: opts.start_ip, + provision_end_ip: opts.end_ip + }; + + if (opts.no_nat) { + createOpts.internet_nat = false; + } + + OPTIONAL_OPTS.forEach(function (attr) { + if (opts[attr]) { + createOpts[attr] = opts[attr]; + } + }); + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + var cloudapi = cli.tritonapi.cloudapi; + + cloudapi.createFabricNetwork(createOpts, function onCreate(err, net) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(net)); + } else { + console.log('Created network %s (%s)', net.name, net.id); + } + + cb(); + }); + }); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['name', 'n'], + type: 'string', + helpArg: 'NAME', + help: 'Name of the NETWORK.' + }, + { + names: ['description', 'D'], + type: 'string', + helpArg: 'DESC', + help: 'Description of the NETWORK.' + }, + { + names: ['subnet'], + type: 'string', + helpArg: 'SUBNET', + help: 'A CIDR string describing the NETWORK.' + }, + { + names: ['start_ip'], + type: 'string', + helpArg: 'START_IP', + help: 'First assignable IP address on NETWORK.' + }, + { + names: ['end_ip'], + type: 'string', + helpArg: 'END_IP', + help: 'Last assignable IP address on NETWORK.' + }, + { + names: ['gateway'], + type: 'string', + helpArg: 'GATEWAY', + help: 'Gateway IP address.' + }, + { + names: ['resolvers'], + type: 'string', + helpArg: 'RESOLVERS', + help: 'Resolver IP addresses.' + }, + { + names: ['routes'], + type: 'string', + helpArg: 'ROUTES', + help: 'Static routes for hosts on NETWORK.' + }, + { + names: ['no_nat'], + type: 'bool', + helpArg: 'NO_NAT', + help: 'Disable creation of an Internet NAT zone on GATEWAY.' + } +]; + +do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN']; + +do_create.help = [ + 'Create a network on a VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + '', + 'Example:', + ' triton network create -n accounting --subnet=192.168.0.0/24', + ' --start_ip=192.168.0.1 --end_ip=192.168.0.254 2' +].join('\n'); + +do_create.helpOpts = { + helpCol: 25 +}; + +module.exports = do_create; diff --git a/lib/do_network/do_delete.js b/lib/do_network/do_delete.js new file mode 100644 index 0000000..a8e5f80 --- /dev/null +++ b/lib/do_network/do_delete.js @@ -0,0 +1,85 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton network delete ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_delete(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length < 1) { + cb(new errors.UsageError('missing NETWORK argument(s)')); + return; + } + + var cli = this.top; + var networks = args; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + vasync.forEachParallel({ + inputs: networks, + func: function deleteOne(id, next) { + cli.tritonapi.deleteFabricNetwork({ id: id }, + function onDelete(err) { + if (err) { + next(err); + return; + } + + console.log('Deleted network %s', id); + next(); + }); + } + }, cb); + }); +} + + +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; + +do_delete.synopses = ['{{name}} {{cmd}} NETWORK [NETWORK ...]']; + +do_delete.help = [ + 'Remove a fabric network.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where NETWORK is a network id (full UUID), name, or short id.' +].join('\n'); + +do_delete.aliases = ['rm']; + +do_delete.completionArgtypes = ['tritonnetwork']; + +module.exports = do_delete; diff --git a/lib/do_network/do_get_default.js b/lib/do_network/do_get_default.js new file mode 100644 index 0000000..3340547 --- /dev/null +++ b/lib/do_network/do_get_default.js @@ -0,0 +1,78 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton network get-default ...` + */ + +var assert = require('assert-plus'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_get_default(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length > 0) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + cli.tritonapi.cloudapi.getConfig({}, function getConf(err, conf) { + if (err) { + cb(err); + return; + } + + var defaultNetwork = conf.default_network; + + if (!defaultNetwork) { + cb('default network not found'); + return; + } + + cli.handlerFromSubcmd('network').dispatch({ + subcmd: 'get', + opts: opts, + args: [defaultNetwork] + }, cb); + }); + }); +} + + +do_get_default.options = require('./do_get').options; + +do_get_default.synopses = ['{{name}} {{cmd}}']; + +do_get_default.help = [ + 'Get default network.', + '', + '{{usage}}', + '', + '{{options}}' +].join('\n'); + +do_get_default.completionArgtypes = ['tritonnetwork']; + +module.exports = do_get_default; diff --git a/lib/do_network/do_list.js b/lib/do_network/do_list.js index faa4f9e..f4f1630 100644 --- a/lib/do_network/do_list.js +++ b/lib/do_network/do_list.js @@ -66,14 +66,23 @@ function do_list(subcmd, opts, args, callback) { common.cliSetupTritonApi, function searchNetworks(arg, next) { - self.top.tritonapi.cloudapi.listNetworks(function (err, networks) { + // since this command is also used by do_vlan/do_networks.js + if (opts.vlan_id) { + self.top.tritonapi.listFabricNetworks({ + vlan_id: opts.vlan_id + }, listedNetworks); + } else { + self.top.tritonapi.cloudapi.listNetworks({}, listedNetworks); + } + + function listedNetworks(err, networks) { if (err) { next(err); return; } arg.networks = networks; next(); - }); + } }, function filterNetworks(arg, next) { diff --git a/lib/do_network/do_set_default.js b/lib/do_network/do_set_default.js new file mode 100644 index 0000000..fb15d0e --- /dev/null +++ b/lib/do_network/do_set_default.js @@ -0,0 +1,92 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton network set-default ...` + */ + +var assert = require('assert-plus'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_set_default(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing NETWORK argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + cli.tritonapi.getNetwork(args[0], function onNetwork(err, net) { + if (err) { + cb(err); + return; + } + + var params = { + default_network: net.id + }; + + var cloudapi = cli.tritonapi.cloudapi; + + cloudapi.updateConfig(params, function onUpdate(err2) { + if (err2) { + cb(err2); + return; + } + + console.log('Set network %s (%s) as default.', net.name, + net.id); + cb(); + }); + }); + }); +} + + +do_set_default.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; + +do_set_default.synopses = ['{{name}} {{cmd}} NETWORK']; + +do_set_default.help = [ + 'Set default network.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where NETWORK is a network id (full UUID), name, or short id.' +].join('\n'); + +do_set_default.completionArgtypes = ['tritonnetwork']; + +module.exports = do_set_default; diff --git a/lib/do_network/index.js b/lib/do_network/index.js index 53d5e49..5d34289 100644 --- a/lib/do_network/index.js +++ b/lib/do_network/index.js @@ -33,7 +33,11 @@ function NetworkCLI(top) { 'help', 'list', 'get', - 'ip' + 'ip', + 'create', + 'delete', + 'get-default', + 'set-default' ] }); } @@ -47,6 +51,10 @@ NetworkCLI.prototype.init = function init(opts, args, cb) { NetworkCLI.prototype.do_list = require('./do_list'); NetworkCLI.prototype.do_get = require('./do_get'); NetworkCLI.prototype.do_ip = require('./do_ip'); +NetworkCLI.prototype.do_create = require('./do_create'); +NetworkCLI.prototype.do_delete = require('./do_delete'); +NetworkCLI.prototype.do_get_default = require('./do_get_default'); +NetworkCLI.prototype.do_set_default = require('./do_set_default'); module.exports = NetworkCLI; diff --git a/lib/do_vlan/do_create.js b/lib/do_vlan/do_create.js new file mode 100644 index 0000000..7e6192c --- /dev/null +++ b/lib/do_vlan/do_create.js @@ -0,0 +1,123 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton vlan create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_create(subcmd, opts, args, cb) { + assert.optionalString(opts.name, 'opts.name'); + assert.optionalString(opts.description, 'opts.description'); + 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 === 0) { + cb(new errors.UsageError('missing VLAN argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var createOpts = { + vlan_id: +args[0] + }; + if (opts.name) { + createOpts.name = opts.name; + } + if (opts.description) { + createOpts.description = opts.description; + } + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + var cloudapi = cli.tritonapi.cloudapi; + cloudapi.createFabricVlan(createOpts, function onCreate(err, vlan) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(vlan)); + } else { + if (vlan.name) { + console.log('Created vlan %s (%d)', vlan.name, + vlan.vlan_id); + } else { + console.log('Created vlan %d', vlan.vlan_id); + } + } + + cb(); + }); + }); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['name', 'n'], + type: 'string', + helpArg: 'NAME', + help: 'Name of the VLAN.' + }, + { + names: ['description', 'D'], + type: 'string', + helpArg: 'DESC', + help: 'Description of the VLAN.' + } +]; + +do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN']; + +do_create.help = [ + 'Create a VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + 'Example:', + ' triton vlan create -n "dmz" -D "Demilitarized zone" 73' +].join('\n'); + +do_create.helpOpts = { + helpCol: 25 +}; + +module.exports = do_create; diff --git a/lib/do_vlan/do_delete.js b/lib/do_vlan/do_delete.js new file mode 100644 index 0000000..1df5083 --- /dev/null +++ b/lib/do_vlan/do_delete.js @@ -0,0 +1,85 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton vlan delete ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_delete(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length < 1) { + cb(new errors.UsageError('missing VLAN argument(s)')); + return; + } + + var cli = this.top; + var vlanIds = args; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + vasync.forEachParallel({ + inputs: vlanIds, + func: function deleteOne(id, next) { + cli.tritonapi.deleteFabricVlan({ vlan_id: id }, + function onDelete(err) { + if (err) { + next(err); + return; + } + + console.log('Deleted vlan %s', id); + next(); + }); + } + }, cb); + }); +} + + +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; + +do_delete.synopses = ['{{name}} {{cmd}} VLAN [VLAN ...]']; + +do_delete.help = [ + 'Remove a VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VLAN is a VLAN id or name.' +].join('\n'); + +do_delete.aliases = ['rm']; + +do_delete.completionArgtypes = ['tritonvlan']; + +module.exports = do_delete; diff --git a/lib/do_vlan/do_get.js b/lib/do_vlan/do_get.js new file mode 100644 index 0000000..4878cfe --- /dev/null +++ b/lib/do_vlan/do_get.js @@ -0,0 +1,88 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton vlan get ...` + */ + +var assert = require('assert-plus'); + +var common = require('../common'); +var errors = require('../errors'); + + +function do_get(subcmd, opts, args, cb) { + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing VLAN argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var id = args[0]; + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + cli.tritonapi.getFabricVlan(id, function onGet(err, vlan) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(vlan)); + } else { + console.log(JSON.stringify(vlan, 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}} VLAN']; + +do_get.help = [ + 'Show a specific VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VLAN is a VLAN id or name.' +].join('\n'); + +do_get.completionArgtypes = ['tritonvlan', 'none']; + +module.exports = do_get; diff --git a/lib/do_vlan/do_list.js b/lib/do_vlan/do_list.js new file mode 100644 index 0000000..83af550 --- /dev/null +++ b/lib/do_vlan/do_list.js @@ -0,0 +1,123 @@ +/* + * 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 vlan list ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'vlan_id,name,description'; +var SORT_DEFAULT = 'vlan_id'; +var VALID_FILTERS = ['vlan_id', 'name', 'description']; + + +function do_list(subcmd, opts, args, cb) { + assert.object(opts, 'opts'); + assert.array(args, 'args'); + assert.func(cb, 'cb'); + + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + try { + var filters = common.objFromKeyValueArgs(args, { + validKeys: VALID_FILTERS, + disableDotted: true + }); + } catch (e) { + cb(e); + return; + } + + if (filters.vlan_id !== undefined) { + filters.vlan_id = +filters.vlan_id; + } + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + var cloudapi = cli.tritonapi.cloudapi; + cloudapi.listFabricVlans({}, function onList(err, vlans) { + if (err) { + cb(err); + return; + } + + // do filtering + Object.keys(filters).forEach(function doFilter(key) { + var val = filters[key]; + vlans = vlans.filter(function (vlan) { + return vlan[key] === val; + }); + }); + + if (opts.json) { + common.jsonStream(vlans); + } else { + var columns = COLUMNS_DEFAULT; + + if (opts.o) { + columns = opts.o; + } + + columns = columns.split(','); + var sort = opts.s.split(','); + + tabula(vlans, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + cb(); + }); + }); +} + + +do_list.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + sortDefault: SORT_DEFAULT +})); + +do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]']; + +do_list.help = [ + 'List VLANs.', + '', + '{{usage}}', + '', + 'Filters:', + ' FIELD= Number filter. Supported fields: vlan_id', + ' FIELD= String filter. Supported fields: name, description', + '', + '{{options}}', + 'Filters are applied client-side (i.e. done by the triton command itself).' +].join('\n'); + +do_list.aliases = ['ls']; + +module.exports = do_list; diff --git a/lib/do_vlan/do_networks.js b/lib/do_vlan/do_networks.js new file mode 100644 index 0000000..977f5a3 --- /dev/null +++ b/lib/do_vlan/do_networks.js @@ -0,0 +1,52 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton vlan networks ...` + */ + +var errors = require('../errors'); + + +function do_networks(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + if (args.length === 0) { + cb(new errors.UsageError('missing VLAN argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + opts.vlan_id = args[0]; + + this.top.handlerFromSubcmd('network').dispatch({ + subcmd: 'list', + opts: opts, + args: [] + }, cb); +} + +do_networks.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN']; + +do_networks.help = [ + 'Show all networks on a VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VLAN is a VLAN id or name.' +].join('\n'); + +do_networks.options = require('../do_network/do_list').options; + +module.exports = do_networks; diff --git a/lib/do_vlan/do_update.js b/lib/do_vlan/do_update.js new file mode 100644 index 0000000..464bf28 --- /dev/null +++ b/lib/do_vlan/do_update.js @@ -0,0 +1,201 @@ +/* + * 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 vlan update ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var fs = require('fs'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +var UPDATE_VLAN_FIELDS + = require('../cloudapi2').CloudApi.prototype.UPDATE_VLAN_FIELDS; + + +function do_update(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + var log = this.log; + var tritonapi = this.top.tritonapi; + + if (args.length === 0) { + cb(new errors.UsageError('missing VLAN argument')); + return; + } + + var id = args.shift(); + + vasync.pipeline({arg: {}, funcs: [ + function gatherDataArgs(ctx, next) { + if (opts.file) { + next(); + return; + } + + try { + ctx.data = common.objFromKeyValueArgs(args, { + disableDotted: true, + typeHintFromKey: UPDATE_VLAN_FIELDS + }); + } catch (err) { + next(err); + return; + } + + next(); + }, + + function gatherDataFile(ctx, next) { + if (!opts.file || opts.file === '-') { + next(); + return; + } + + var input = fs.readFileSync(opts.file, 'utf8'); + + try { + ctx.data = JSON.parse(input); + } catch (err) { + next(new errors.TritonError(format( + 'invalid JSON for vlan update in "%s": %s', + opts.file, err))); + return; + } + next(); + }, + + function gatherDataStdin(ctx, next) { + if (opts.file !== '-') { + next(); + return; + } + + var stdin = ''; + + process.stdin.resume(); + process.stdin.on('data', function (chunk) { + stdin += chunk; + }); + + process.stdin.on('error', console.error); + + process.stdin.on('end', function () { + try { + ctx.data = JSON.parse(stdin); + } catch (err) { + log.trace({stdin: stdin}, + 'invalid VLAN update JSON on stdin'); + next(new errors.TritonError(format( + 'invalid JSON for VLAN update on stdin: %s', + err))); + return; + } + next(); + }); + }, + + function validateIt(ctx, next) { + assert.object(ctx.data, 'ctx.data'); + + var keys = Object.keys(ctx.data); + + if (keys.length === 0) { + console.log('No fields given for VLAN update'); + next(); + return; + } + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = ctx.data[key]; + var type = UPDATE_VLAN_FIELDS[key]; + if (!type) { + next(new errors.UsageError(format('unknown or ' + + 'unupdateable field: %s (updateable fields are: %s)', + key, + Object.keys(UPDATE_VLAN_FIELDS).sort().join(', ')))); + return; + } + + if (typeof (value) !== type) { + next(new errors.UsageError(format('field "%s" must be ' + + 'of type "%s", but got a value of type "%s"', key, + type, typeof (value)))); + return; + } + } + next(); + }, + + function updateAway(ctx, next) { + var data = ctx.data; + data.vlan_id = id; + + tritonapi.updateFabricVlan(data, function onUpdate(err) { + if (err) { + next(err); + return; + } + + delete data.vlan_id; + console.log('Updated vlan %s (fields: %s)', id, + Object.keys(data).join(', ')); + + next(); + }); + } + ]}, cb); +} + +do_update.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['file', 'f'], + type: 'string', + helpArg: 'JSON-FILE', + help: 'A file holding a JSON file of updates, or "-" to read ' + + 'JSON from stdin.' + } +]; + +do_update.synopses = [ + '{{name}} {{cmd}} VLAN [FIELD=VALUE ...]', + '{{name}} {{cmd}} -f JSON-FILE VLAN' +]; + +do_update.help = [ + 'Update a VLAN.', + '', + '{{usage}}', + '', + '{{options}}', + + 'Updateable fields:', + ' ' + Object.keys(UPDATE_VLAN_FIELDS).sort().map(function (f) { + return f + ' (' + UPDATE_VLAN_FIELDS[f] + ')'; + }).join(', '), + '', + 'Where VLAN is a VLAN id or name.' +].join('\n'); + +do_update.completionArgtypes = ['tritonvlan', 'tritonupdatevlanfield']; + +module.exports = do_update; diff --git a/lib/do_vlan/index.js b/lib/do_vlan/index.js new file mode 100644 index 0000000..c8d9f55 --- /dev/null +++ b/lib/do_vlan/index.js @@ -0,0 +1,55 @@ +/* + * 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 2017 Joyent, Inc. + * + * `triton vlan ...` + */ + +var Cmdln = require('cmdln').Cmdln; +var util = require('util'); + + + +// ---- CLI class + +function VlanCLI(top) { + this.top = top; + + Cmdln.call(this, { + name: top.name + ' vlan', + desc: 'List and manage Triton fabric VLANs.', + helpSubcmds: [ + 'help', + 'list', + 'get', + 'create', + 'update', + 'delete', + { group: '' }, + 'networks' + ], + helpOpts: { + minHelpCol: 23 + } + }); +} +util.inherits(VlanCLI, Cmdln); + +VlanCLI.prototype.init = function init(opts, args, cb) { + this.log = this.top.log; + Cmdln.prototype.init.apply(this, arguments); +}; + +VlanCLI.prototype.do_list = require('./do_list'); +VlanCLI.prototype.do_create = require('./do_create'); +VlanCLI.prototype.do_get = require('./do_get'); +VlanCLI.prototype.do_update = require('./do_update'); +VlanCLI.prototype.do_delete = require('./do_delete'); +VlanCLI.prototype.do_networks = require('./do_networks'); + +module.exports = VlanCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 92882cc..b76aab1 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -282,7 +282,9 @@ function _stepFwRuleId(arg, next) { * and determines the network id (setting it as `arg.netId`). */ function _stepNetId(arg, next) { + assert.object(arg, 'arg'); assert.object(arg.client, 'arg.client'); + assert.func(next, 'next'); var id = arg.network || arg.id; assert.string(id, 'arg.network || arg.id'); @@ -291,7 +293,7 @@ function _stepNetId(arg, next) { arg.netId = id; next(); } else { - arg.client.getNetwork(id, function (err, net) { + arg.client.getNetwork(id, function onGet(err, net) { if (err) { next(err); } else { @@ -302,6 +304,98 @@ function _stepNetId(arg, next) { } } +/** + * A function appropriate for `vasync.pipeline` funcs that takes a `arg.id` and + * optionally a `arg.vlan_id`, where `arg.id` is a network name, shortid or + * uuid, and `arg.vlan_id` is a VLAN's id or name. Sets the network id as + * `arg.netId` and the VLAN id as `arg.vlanId`. + */ +function _stepFabricNetId(arg, next) { + assert.object(arg, 'arg'); + assert.object(arg.client, 'arg.client'); + assert.string(arg.id, 'arg.id'); + assert.func(next, 'next'); + + var id = arg.id; + var vlanId = arg.vlan_id; + var vlanIdType = typeof (vlanId); + + if (common.isUUID(id) && vlanIdType === 'number') { + arg.netId = id; + arg.vlanId = vlanId; + + next(); + return; + } + + arg.client.getNetwork(id, function onGetNetwork(err, net) { + if (err) { + next(err); + return; + } + + if (vlanIdType === 'number') { + assert.equal(net.vlan_id, vlanId, 'VLAN belongs to network'); + } + + if (vlanIdType === 'number' || vlanIdType === 'undefined') { + arg.netId = net.id; + arg.vlanId = net.vlan_id; + + next(); + return; + } + + // at this point the only type left we support are strings + assert.string(vlanId, 'arg.vlan_id'); + + arg.client.getFabricVlan(vlanId, function onGetFabric(err2, vlan) { + if (err2) { + next(err2); + return; + } + + assert.equal(net.vlan_id, vlan.vlan_id, 'VLAN belongs to network'); + arg.netId = net.id; + arg.vlanId = net.vlan_id; + next(); + }); + }); +} + +/** + * A function appropriate for `vasync.pipeline` funcs that takes a + * `arg.vlan_id`, where that is either a VLAN's id or name. Sets the + * VLAN id as `arg.vlanId`. + */ +function _stepFabricVlanId(arg, next) { + assert.object(arg, 'arg'); + assert.object(arg.client, 'arg.client'); + assert.ok(typeof (arg.vlan_id) === 'string' || + typeof (arg.vlan_id) === 'number', 'arg.vlan_id'); + assert.func(next, 'next'); + + var vlanId = arg.vlan_id; + + if (typeof (vlanId) === 'number') { + arg.vlanId = vlanId; + next(); + return; + } + + arg.client.getFabricVlan(vlanId, function onGet(err, vlan) { + if (err) { + next(err); + return; + } + + arg.vlanId = vlan.vlan_id; + next(); + }); +} + + + //---- TritonApi class /** @@ -934,7 +1028,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { assert.func(cb, 'cb'); if (common.isUUID(name)) { - this.cloudapi.getNetwork(name, function (err, net) { + this.cloudapi.getNetwork(name, function onGet(err, net) { if (err) { if (err.restCode === 'ResourceNotFound') { // Wrap with *our* ResourceNotFound for exitStatus=3. @@ -947,7 +1041,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { } }); } else { - this.cloudapi.listNetworks(function (err, nets) { + this.cloudapi.listNetworks({}, function onList(err, nets) { if (err) { return cb(err); } @@ -984,6 +1078,72 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { } }; + +/** + * List all fabric networks on a VLAN. Takes a network's VLAN ID or name as an + * argument. + */ +TritonApi.prototype.listFabricNetworks = +function listFabricNetworks(opts, cb) { + assert.object(opts, 'opts'); + assert.ok(typeof (opts.vlan_id) === 'string' || + typeof (opts.vlan_id) === 'number', 'opts.vlan_id'); + assert.func(cb, 'cb'); + + var self = this; + var networks; + + vasync.pipeline({ + arg: {client: self, vlan_id: opts.vlan_id}, funcs: [ + _stepFabricVlanId, + + function listNetworks(arg, next) { + self.cloudapi.listFabricNetworks({ + vlan_id: arg.vlanId + }, function listCb(err, nets) { + if (err) { + next(err); + return; + } + + networks = nets; + + next(); + }); + } + ]}, function (err) { + cb(err, networks); + }); +}; + + +/** + * Delete a fabric network by ID, exact name, or short ID, in that order. + * Can accept a network's VLAN ID or name as an optional argument. + * + * If the name is ambiguous, then this errors out. + */ +TritonApi.prototype.deleteFabricNetwork = +function deleteFabricNetwork(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + + vasync.pipeline({ + arg: {client: self, id: opts.id, vlan_id: opts.vlan_id}, + funcs: [ + _stepFabricNetId, + + function deleteNetwork(arg, next) { + self.cloudapi.deleteFabricNetwork({ + id: arg.netId, vlan_id: arg.vlanId + }, next); + } + ]}, cb); +}; + /** * List a network's IPs. * @@ -2137,14 +2297,15 @@ function deleteAllInstanceTags(opts, cb) { */ TritonApi.prototype.addNic = function addNic(opts, cb) { + assert.object(opts, 'opts'); assert.string(opts.id, 'opts.id'); assert.ok(opts.network, 'opts.network'); assert.func(cb, 'cb'); var self = this; + var nic; var pipeline = []; var res; - var nic; switch (typeof (opts.network)) { case 'string': @@ -2198,8 +2359,8 @@ function listNics(opts, cb) { assert.func(cb, 'cb'); var self = this; - var res; var nics; + var res; vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ _stepInstId, @@ -2207,7 +2368,7 @@ function listNics(opts, cb) { function list(arg, next) { self.cloudapi.listNics({ id: arg.instId - }, function (err, _nics, _res) { + }, function onList(err, _nics, _res) { res = _res; res.instId = arg.instId; // gross hack, in case caller needs it nics = _nics; @@ -2245,7 +2406,7 @@ function getNic(opts, cb) { self.cloudapi.getNic({ id: arg.instId, mac: arg.mac - }, function (err, _nic, _res) { + }, function onGet(err, _nic, _res) { res = _res; res.instId = arg.instId; // gross hack, in case caller needs it nic = _nic; @@ -2283,7 +2444,7 @@ function removeNic(opts, cb) { self.cloudapi.removeNic({ id: arg.instId, mac: arg.mac - }, function (err, _res) { + }, function onRemove(err, _res) { res = _res; res.instId = arg.instId; // gross hack, in case caller needs it next(err); @@ -2635,6 +2796,93 @@ TritonApi.prototype.deleteFirewallRule = function deleteFirewallRule(opts, cb) { }; +// ---- VLANs + +/** + * Get a VLAN by ID or exact name, in that order. + * + * If the name is ambiguous, then this errors out. + */ +TritonApi.prototype.getFabricVlan = function getFabricVlan(name, cb) { + assert.ok(typeof (name) === 'string' || + typeof (name) === 'number', 'name'); + assert.func(cb, 'cb'); + + if (+name >= 0 && +name < 4096) { + this.cloudapi.getFabricVlan({vlan_id: +name}, function on(err, vlan) { + if (err) { + if (err.restCode === 'ResourceNotFound') { + // Wrap with our own ResourceNotFound for exitStatus=3. + err = new errors.ResourceNotFoundError(err, + format('vlan with id %s was not found', name)); + } + cb(err); + } else { + cb(null, vlan); + } + }); + } else { + this.cloudapi.listFabricVlans({}, function onList(err, vlans) { + if (err) { + return cb(err); + } + + var nameMatches = []; + for (var i = 0; i < vlans.length; i++) { + var vlan = vlans[i]; + if (vlan.name === name) { + nameMatches.push(vlan); + } + } + + if (nameMatches.length === 1) { + cb(null, nameMatches[0]); + } else if (nameMatches.length > 1) { + cb(new errors.TritonError(format( + 'vlan name "%s" is ambiguous: matches %d vlans', + name, nameMatches.length))); + } else { + cb(new errors.ResourceNotFoundError(format( + 'no vlan with name "%s" was found', name))); + } + }); + } +}; + + +/** + * Delete a VLAN by ID or exact name, in that order. + * + * If the name is ambiguous, then this errors out. + */ +TritonApi.prototype.deleteFabricVlan = function deleteFabricVlan(opts, cb) { + assert.object(opts, 'opts'); + assert.ok(typeof (opts.vlan_id) === 'string' || + typeof (opts.vlan_id) === 'number', 'opts.vlan_id'); + assert.func(cb, 'cb'); + + var self = this; + var vlanId = opts.vlan_id; + + if (+vlanId >= 0 && +vlanId < 4096) { + deleteVlan(+vlanId); + } else { + self.getFabricVlan(vlanId, function onGet(err, vlan) { + if (err) { + cb(err); + return; + } + + deleteVlan(vlan.vlan_id); + }); + } + + function deleteVlan(id) { + self.cloudapi.deleteFabricVlan({vlan_id: id}, cb); + } +}; + + // ---- RBAC /** diff --git a/test/integration/api-networks.test.js b/test/integration/api-networks.test.js index feaaf70..6afc2c0 100644 --- a/test/integration/api-networks.test.js +++ b/test/integration/api-networks.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2016 Joyent, Inc. + * Copyright 2018 Joyent, Inc. */ /* @@ -18,6 +18,8 @@ var test = require('tape'); // --- Globals +var NET_NAME = 'node-triton-testnet967'; + var CLIENT; var NET; @@ -33,11 +35,14 @@ test('TritonApi networks', function (tt) { }); }); + tt.test(' cleanup: rm network ' + NET_NAME + ' if exists', function (t) { + CLIENT.deleteFabricNetwork({id: NET_NAME}, function () { + t.end(); + }); + }); + tt.test(' setup: net', function (t) { - var opts = { - account: CLIENT.profile.account - }; - CLIENT.cloudapi.listNetworks(opts, function (err, nets) { + CLIENT.cloudapi.listNetworks({}, function (err, nets) { if (h.ifErr(t, err)) return t.end(); @@ -78,6 +83,61 @@ test('TritonApi networks', function (tt) { }); + tt.test(' TritonApi deleteFabricNetwork', function (t) { + function check(genId, idType, vlanId, cb) { + CLIENT.cloudapi.createFabricNetwork({ + name: NET_NAME, + subnet: '192.168.97.0/24', + provision_start_ip: '192.168.97.1', + provision_end_ip: '192.168.97.254', + vlan_id: vlanId + }, function onCreate(err, net) { + if (h.ifErr(t, err, 'Error creating network')) { + t.end(); + return; + } + + var id = genId(net); + CLIENT.deleteFabricNetwork({id: id}, function onDelete(err2) { + if (h.ifErr(t, err, 'Error deleting net by ' + idType)) { + t.end(); + return; + } + + CLIENT.cloudapi.getNetwork(net.id, function onGet(err3) { + t.ok(err3, 'Network should be gone'); + cb(); + }); + }); + }); + } + + // get a VLAN, then create and delete a set of fabrics to check it's + // possible to delete by id, shortId and name + CLIENT.cloudapi.listFabricVlans({}, function onList(err, vlans) { + if (vlans.length === 0) { + t.end(); + return; + } + + function getId(net) { return net.id; } + function getName(net) { return net.name; } + function getShort(net) { return net.id.split('-')[0]; } + + var vlanId = +vlans[0].vlan_id; + + check(getId, 'id', vlanId, function onId() { + check(getName, 'name', vlanId, function onName() { + check(getShort, 'shortId', vlanId, function onShort() { + t.end(); + }); + }); + }); + }); + }); + + + tt.test(' teardown: client', function (t) { CLIENT.close(); t.end(); diff --git a/test/integration/api-nics.test.js b/test/integration/api-nics.test.js index 0e9094c..38dada6 100644 --- a/test/integration/api-nics.test.js +++ b/test/integration/api-nics.test.js @@ -25,9 +25,9 @@ var NIC; // --- Tests -test('TritonApi networks', function (tt) { +test('TritonApi nics', function (tt) { tt.test(' setup', function (t) { - h.createClient(function (err, client_) { + h.createClient(function onCreate(err, client_) { t.error(err); CLIENT = client_; t.end(); @@ -36,9 +36,11 @@ test('TritonApi networks', function (tt) { tt.test(' setup: inst', function (t) { - CLIENT.cloudapi.listMachines({}, function (err, vms) { - if (vms.length === 0) - return t.end(); + CLIENT.cloudapi.listMachines({}, function onList(err, vms) { + if (vms.length === 0) { + t.end(); + return; + } t.ok(Array.isArray(vms), 'vms array'); INST = vms[0]; @@ -49,13 +51,17 @@ test('TritonApi networks', function (tt) { tt.test(' TritonApi listNics', function (t) { - if (!INST) - return t.end(); + if (!INST) { + t.end(); + return; + } function check(val, valName, next) { - CLIENT.listNics({id: val}, function (err, nics) { - if (h.ifErr(t, err, 'no err ' + valName)) - return t.end(); + CLIENT.listNics({id: val}, function onList(err, nics) { + if (h.ifErr(t, err, 'no err ' + valName)) { + t.end(); + return; + } t.ok(Array.isArray(nics), 'nics array'); NIC = nics[0]; @@ -66,9 +72,9 @@ test('TritonApi networks', function (tt) { var shortId = INST.id.split('-')[0]; - check(INST.id, 'id', function () { - check(INST.name, 'name', function () { - check(shortId, 'shortId', function () { + check(INST.id, 'id', function doId() { + check(INST.name, 'name', function doName() { + check(shortId, 'shortId', function doShort() { t.end(); }); }); @@ -77,13 +83,17 @@ test('TritonApi networks', function (tt) { tt.test(' TritonApi getNic', function (t) { - if (!NIC) - return t.end(); + if (!NIC) { + t.end(); + return; + } 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(); + CLIENT.getNic({id: inst, mac: mac}, function onGet(err, nic) { + if (h.ifErr(t, err, 'no err for ' + instValName)) { + t.end(); + return; + } t.deepEqual(nic, NIC, instValName); @@ -93,9 +103,9 @@ test('TritonApi networks', function (tt) { 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 () { + check(INST.id, NIC.mac, 'id', function doId() { + check(INST.name, NIC.mac, 'name', function doName() { + check(shortId, NIC.mac, 'shortId', function doShort() { t.end(); }); }); diff --git a/test/integration/api-vlans.test.js b/test/integration/api-vlans.test.js new file mode 100644 index 0000000..03c7c5e --- /dev/null +++ b/test/integration/api-vlans.test.js @@ -0,0 +1,120 @@ +/* + * 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 VLAN-related APIs as a module. + */ + +var h = require('./helpers'); +var test = require('tape'); + + +// --- Globals + +var CLIENT; +var VLAN; + + +// --- Tests + +test('TritonApi vlan', function (tt) { + tt.test(' setup', function (t) { + h.createClient(function onCreate(err, client_) { + t.error(err); + CLIENT = client_; + t.end(); + }); + }); + + + tt.test(' setup: vlan', function (t) { + CLIENT.cloudapi.listFabricVlans({}, function onList(err, vlans) { + if (vlans.length === 0) { + t.end(); + return; + } + + VLAN = vlans[0]; + + t.end(); + }); + }); + + + tt.test(' TritonApi getFabricVlan', function (t) { + if (!VLAN) { + t.end(); + return; + } + + function check(val, valName, next) { + CLIENT.getFabricVlan(val, function onGet(err, vlan) { + if (h.ifErr(t, err, 'no err')) { + t.end(); + return; + } + + t.deepEqual(vlan, VLAN, valName); + + next(); + }); + } + + check(VLAN.vlan_id, 'vlan_id', function onId() { + check(VLAN.name, 'name', function onName() { + t.end(); + }); + }); + }); + + + tt.test(' TritonApi deleteFabricVlan', function (t) { + function check(genId, idType, cb) { + CLIENT.cloudapi.createFabricVlan({ + vlan_id: 3291, + name: 'test3291' + }, function onCreate(err, vlan) { + if (h.ifErr(t, err, 'Error creating VLAN')) { + t.end(); + return; + } + + var id = genId(vlan); + CLIENT.deleteFabricVlan({vlan_id: id}, function onDel(err2) { + if (h.ifErr(t, err, 'Error deleting VLAN by ' + idType)) { + t.end(); + return; + } + + CLIENT.cloudapi.getFabricVlan({vlan_id: vlan.vlan_id}, + function onGet(err3) { + t.ok(err3, 'VLAN should be gone'); + cb(); + }); + }); + }); + } + + function getVlanId(net) { return net.vlan_id; } + function getName(net) { return net.name; } + + check(getVlanId, 'vlan_id', function onId() { + check(getName, 'name', function onName() { + t.end(); + }); + }); + }); + + + tt.test(' teardown: client', function (t) { + CLIENT.close(); + t.end(); + }); +}); diff --git a/test/integration/cli-networks.test.js b/test/integration/cli-networks.test.js index dad5ef8..6091fa4 100644 --- a/test/integration/cli-networks.test.js +++ b/test/integration/cli-networks.test.js @@ -5,28 +5,53 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright 2018 Joyent, Inc. */ /* * Integration tests for `triton network(s)` */ -var h = require('./helpers'); +var f = require('util').format; +var os = require('os'); var test = require('tape'); +var h = require('./helpers'); var common = require('../../lib/common'); // --- Globals +var NET_NAME = f('nodetritontest-network-%s', os.hostname()); + var networks; +var vlan; + +var OPTS = { + skip: !h.CONFIG.allowWriteActions +}; // --- Tests +if (OPTS.skip) { + console.error('** skipping some %s tests', __filename); + console.error('** set "allowWriteActions" in test config to enable'); +} + test('triton networks', function (tt) { + tt.test(' setup: find a test VLAN', function (t) { + h.triton('vlan list -j', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + + vlan = JSON.parse(stdout.trim().split('\n')[0]); + t.ok(vlan, 'vlan for testing found'); + t.end(); + }); + }); + tt.test(' triton network list -h', function (t) { h.triton('networks -h', function (err, stdout, stderr) { if (h.ifErr(t, err)) @@ -209,3 +234,135 @@ test('triton network get', function (tt) { }); }); + + +test('triton network create', OPTS, function (tt) { + + tt.test(' cleanup: rm network ' + NET_NAME + ' if exists', function (t) { + h.triton('network delete ' + NET_NAME, function (err, stdout) { + t.end(); + }); + }); + + tt.test(' triton network create -h', function (t) { + h.triton('network create -h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network help create', function (t) { + h.triton('network help create', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network create\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network create', function (t) { + h.triton('network create', function (err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + tt.test(' triton network create VLAN', function (t) { + h.triton('network create --name=' + NET_NAME + + ' --subnet=192.168.97.0/24 --start_ip=192.168.97.1' + + ' --end_ip=192.168.97.254 -j ' + vlan.vlan_id, + function (err, stdout) { + if (h.ifErr(t, err)) + return t.end(); + + var network = JSON.parse(stdout.trim().split('\n')[0]); + + t.equal(network.name, NET_NAME); + t.equal(network.subnet, '192.168.97.0/24'); + t.equal(network.provision_start_ip, '192.168.97.1'); + t.equal(network.provision_end_ip, '192.168.97.254'); + t.equal(network.vlan_id, vlan.vlan_id); + + h.triton('network delete ' + network.id, function (err2) { + h.ifErr(t, err2); + t.end(); + }); + }); + }); + +}); + + +test('triton network delete', OPTS, function (tt) { + + tt.test(' triton network delete -h', function (t) { + h.triton('network delete -h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network help delete', function (t) { + h.triton('network help delete', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network delete\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network delete', function (t) { + h.triton('network delete', function (err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + function deleteNetworkTester(t, deleter) { + h.triton('network create --name=' + NET_NAME + + ' --subnet=192.168.97.0/24 --start_ip=192.168.97.1' + + ' --end_ip=192.168.97.254 -j ' + vlan.vlan_id, + function (err, stdout) { + if (h.ifErr(t, err, 'create test network')) + return t.end(); + + var network = JSON.parse(stdout.trim().split('\n')[0]); + + deleter(null, network, function (err2) { + if (h.ifErr(t, err2, 'deleting test network')) + return t.end(); + + h.triton('network get ' + network.id, function (err3) { + t.ok(err3, 'network should be gone'); + t.end(); + }); + }); + }); + } + + tt.test(' triton network delete ID', function (t) { + deleteNetworkTester(t, function (err, network, cb) { + h.triton('network delete ' + network.id, cb); + }); + }); + + tt.test(' triton network delete NAME', function (t) { + deleteNetworkTester(t, function (err, network, cb) { + h.triton('network delete ' + network.name, cb); + }); + }); + + tt.test(' triton network delete SHORTID', function (t) { + deleteNetworkTester(t, function (err, network, cb) { + var shortid = network.id.split('-')[0]; + h.triton('network delete ' + shortid, cb); + }); + }); + +}); diff --git a/test/integration/cli-nics.test.js b/test/integration/cli-nics.test.js index f4fe61b..e4bd17d 100644 --- a/test/integration/cli-nics.test.js +++ b/test/integration/cli-nics.test.js @@ -41,7 +41,7 @@ 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) { + h.deleteTestInst(t, INST_ALIAS, function onDelete(err) { t.ifErr(err); t.end(); }); @@ -49,8 +49,10 @@ test('triton instance nics', OPTS, function (tt) { 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(); + if (h.ifErr(t, err, 'triton instance create')) { + t.end(); + return; + } t.ok(instId, 'created instance ' + instId); INST = instId; @@ -61,8 +63,10 @@ test('triton instance nics', OPTS, function (tt) { 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(); + if (h.ifErr(t, err, 'triton network list')) { + t.end(); + return; + } NETWORK = JSON.parse(stdout.trim().split('\n')[0]); t.ok(NETWORK, 'NETWORK'); @@ -74,9 +78,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic create')) { + t.end(); + return; + } NIC = JSON.parse(stdout); t.ok(NIC, 'created NIC: ' + stdout.trim()); @@ -88,9 +94,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic get')) { + t.end(); + return; + } var obj = JSON.parse(stdout); t.equal(obj.mac, NIC.mac, 'nic MAC is correct'); @@ -104,9 +112,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic list')) { + t.end(); + return; + } var nics = stdout.trim().split('\n'); t.ok(nics[0].match(/IP\s+MAC\s+STATE\s+NETWORK/), 'nic list' + @@ -115,7 +125,7 @@ test('triton instance nics', OPTS, function (tt) { t.ok(nics.length >= 1, 'triton nic list expected nic num'); - var testNics = nics.filter(function (nic) { + var testNics = nics.filter(function doFilter(nic) { return nic.match(NIC.mac); }); @@ -128,17 +138,19 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic list')) { + t.end(); + return; + } - var nics = stdout.trim().split('\n').map(function (line) { + var nics = stdout.trim().split('\n').map(function doParse(line) { return JSON.parse(line); }); t.ok(nics.length >= 1, 'triton nic list expected nic num'); - var testNics = nics.filter(function (nic) { + var testNics = nics.filter(function doFilter(nic) { return nic.mac === NIC.mac; }); @@ -150,11 +162,13 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } - var nics = stdout.trim().split('\n').map(function (str) { + var nics = stdout.trim().split('\n').map(function doParse(str) { return JSON.parse(str); }); @@ -169,11 +183,13 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function doTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } - var nics = stdout.trim().split('\n').map(function (str) { + var nics = stdout.trim().split('\n').map(function doParse(str) { return JSON.parse(str); }); @@ -188,9 +204,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function doTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic delete')) { + t.end(); + return; + } t.ok(stdout.match('Deleted NIC ' + NIC.mac, 'deleted nic')); @@ -202,9 +220,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function doTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic create')) { + t.end(); + return; + } NIC2 = JSON.parse(stdout); @@ -215,9 +235,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic get')) { + t.end(); + return; + } var obj = JSON.parse(stdout); t.equal(obj.mac, NIC2.mac, 'nic MAC is correct'); @@ -231,9 +253,11 @@ test('triton instance nics', OPTS, function (tt) { 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(); + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err, 'triton instance nic with ip delete')) { + t.end(); + return; + } t.ok(stdout.match('Deleted NIC ' + NIC2.mac, 'deleted nic')); diff --git a/test/integration/cli-subcommands.test.js b/test/integration/cli-subcommands.test.js index 33173bf..a36dd21 100644 --- a/test/integration/cli-subcommands.test.js +++ b/test/integration/cli-subcommands.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright (c) 2018, Joyent, Inc. */ /* @@ -65,8 +65,18 @@ var subs = [ ['ip'], ['ssh'], ['network'], - ['network list', 'networks'], + ['network create'], + ['network list', 'network ls', 'networks'], ['network get'], + ['network get-default'], + ['network set-default'], + ['network delete', 'network rm'], + ['vlan'], + ['vlan create'], + ['vlan list', 'vlan ls'], + ['vlan get'], + ['vlan update'], + ['vlan delete', 'vlan rm'], ['key'], ['key add'], ['key list', 'key ls', 'keys'], diff --git a/test/integration/cli-vlans.test.js b/test/integration/cli-vlans.test.js new file mode 100644 index 0000000..3ebf4f2 --- /dev/null +++ b/test/integration/cli-vlans.test.js @@ -0,0 +1,418 @@ +/* + * 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 `triton vlans` + */ + +var f = require('util').format; +var os = require('os'); +var test = require('tape'); +var h = require('./helpers'); + +var common = require('../../lib/common'); + + +// --- Globals + +var VLAN_NAME = f('nodetritontest-vlan-%s', os.hostname()); +var VLAN_ID = 3197; + +var VLAN; + +var OPTS = { + skip: !h.CONFIG.allowWriteActions +}; + + +// --- Tests + +if (OPTS.skip) { + console.error('** skipping some %s tests', __filename); + console.error('** set "allowWriteActions" in test config to enable'); +} + +test('triton vlan list', function (tt) { + + tt.test(' triton vlan list -h', function (t) { + h.triton('vlan list -h', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan list/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan list', function (t) { + h.triton('vlan list', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/^VLAN_ID\b/.test(stdout)); + t.ok(/\bNAME\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan list -j', function (t) { + h.triton('vlan list -j', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + VLAN = JSON.parse(stdout.trim().split('\n')[0]); + + t.end(); + }); + }); + + tt.test(' triton vlan list vlan_id=<...>', function (t) { + var cmd = 'vlan list -j vlan_id=' + VLAN.vlan_id; + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlans = stdout.trim().split('\n').map(function onParse(str) { + return JSON.parse(str); + }); + + t.deepEqual(vlans, [VLAN]); + + t.end(); + }); + }); + + tt.test(' triton vlan list vlan_id=<...> name=<...> (good)', function (t) { + var cmd = 'vlan list -j vlan_id=' + VLAN.vlan_id + ' name=' + VLAN.name; + + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlans = stdout.trim().split('\n').map(function onParse(str) { + return JSON.parse(str); + }); + + t.deepEqual(vlans, [VLAN]); + + t.end(); + }); + }); + + tt.test(' triton vlan list vlan_id=<...> name=<...> (bad)', function (t) { + // search for a mismatch, should return nada + var cmd = 'vlan list -j vlan_id=' + VLAN.vlan_id + ' name=' + + VLAN.name + 'a'; + + h.triton(cmd, function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.equal(stdout, ''); + + t.end(); + }); + }); + +}); + + +test('triton vlan get', function (tt) { + + tt.test(' triton vlan get -h', function (t) { + h.triton('vlan get -h', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan help get', function (t) { + h.triton('vlan help get', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan get\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan get', function (t) { + h.triton('vlan get', function onTriton(err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + tt.test(' triton vlan get ID', function (t) { + h.triton('vlan get ' + VLAN.vlan_id, + function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout); + t.equal(vlan.vlan_id, VLAN.vlan_id); + + t.end(); + }); + }); + + tt.test(' triton vlan get NAME', function (t) { + h.triton('vlan get ' + VLAN.name, + function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout); + t.equal(vlan.vlan_id, VLAN.vlan_id); + + t.end(); + }); + }); + +}); + + +test('triton vlan networks', function (tt) { + + tt.test(' triton vlan networks -h', function (t) { + h.triton('vlan networks -h', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan networks/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan help networks', function (t) { + h.triton('vlan help networks', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan networks/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan networks', function (t) { + h.triton('vlan networks', function onTriton(err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + tt.test(' triton vlan networks ID', function (t) { + h.triton('vlan networks -j ' + VLAN.vlan_id, + function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout); + t.equal(vlan.vlan_id, VLAN.vlan_id); + + t.end(); + }); + }); + + tt.test(' triton vlan networks NAME', function (t) { + h.triton('vlan networks -j ' + VLAN.name, + function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout); + t.equal(vlan.vlan_id, VLAN.vlan_id); + + t.end(); + }); + }); + +}); + + +test('triton vlan create', OPTS, function (tt) { + + tt.test(' cleanup: rm vlan ' + VLAN_NAME + ' if exists', function (t) { + h.triton('vlan delete ' + VLAN_NAME, function onTriton(err, stdout) { + t.end(); + }); + }); + + tt.test(' triton vlan create -h', function (t) { + h.triton('vlan create -h', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan help create', function (t) { + h.triton('vlan help create', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan create\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan create', function (t) { + h.triton('vlan create', function onTriton(err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + + t.end(); + }); + }); + + tt.test(' triton vlan create VLAN', function (t) { + h.triton('vlan create -j --name=' + VLAN_NAME + ' ' + VLAN_ID, + function onTriton(err, stdout) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout.trim().split('\n')[0]); + + t.equal(vlan.name, VLAN_NAME); + t.equal(vlan.vlan_id, VLAN_ID); + + h.triton('vlan delete ' + vlan.vlan_id, function onTriton2(err2) { + h.ifErr(t, err2); + t.end(); + }); + }); + }); + +}); + + +test('triton vlan delete', OPTS, function (tt) { + + tt.test(' triton vlan delete -h', function (t) { + h.triton('vlan delete -h', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan help delete', function (t) { + h.triton('vlan help delete', function onTriton(err, stdout, stderr) { + if (h.ifErr(t, err)) { + t.end(); + return; + } + + t.ok(/Usage:\s+triton vlan delete\b/.test(stdout)); + + t.end(); + }); + }); + + tt.test(' triton vlan delete', function (t) { + h.triton('vlan delete', function onTriton(err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + + t.end(); + }); + }); + + function deleteNetworkTester(t, deleter) { + h.triton('vlan create -j --name=' + VLAN_NAME + ' ' + VLAN_ID, + function onTriton(err, stdout) { + if (h.ifErr(t, err, 'create test vlan')) { + t.end(); + return; + } + + var vlan = JSON.parse(stdout.trim().split('\n')[0]); + + deleter(null, vlan, function onDelete(err2) { + if (h.ifErr(t, err2, 'deleting test vlan')) { + t.end(); + return; + } + + h.triton('vlan get ' + vlan.vlan_id, function onTriton2(err3) { + t.ok(err3, 'vlan should be gone'); + t.end(); + }); + }); + }); + } + + tt.test(' triton vlan delete ID', function (t) { + deleteNetworkTester(t, function doDelete(err, vlan, cb) { + h.triton('vlan delete ' + vlan.vlan_id, cb); + }); + }); + + tt.test(' triton vlan delete NAME', function (t) { + deleteNetworkTester(t, function doDelete(err, vlan, cb) { + h.triton('vlan delete ' + vlan.name, cb); + }); + }); + +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 360ee82..0b6af0c 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -183,6 +183,7 @@ function getTestImg(t, cb) { var candidateImageNames = { 'base-64-lts': true, 'base-64': true, + 'minimal-64-lts': true, 'minimal-64': true, 'base-32-lts': true, 'base-32': true, From 3584c82e05d49cf4638dd52f038530ff6c411b41 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Mon, 23 Apr 2018 23:48:02 +1200 Subject: [PATCH 3/8] TRITON-324 node-triton cli-affinity.test.js failures: create timeout, .end() called twice Reviewed by: Pedro P. Candel Approved by: Pedro P. Candel --- test/integration/cli-affinity.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/cli-affinity.test.js b/test/integration/cli-affinity.test.js index f427aa6..d68cca3 100644 --- a/test/integration/cli-affinity.test.js +++ b/test/integration/cli-affinity.test.js @@ -110,7 +110,8 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) { var db2Alias = ALIAS_PREFIX + '-db2'; var db2; tt.test(' triton create -n db2 -a \'instance!=db*\'', function (t) { - var argv = ['create', '-wj', '-n', db2Alias, '-a', 'instance!=db*', + var argv = ['create', '-wj', '-n', db2Alias, '-a', + 'instance!=' + ALIAS_PREFIX + '-db*', imgId, pkgId]; h.safeTriton(t, argv, function (err, stdout) { var lines = h.jsonStreamParse(stdout); From 264f69dc54b94618a45444766c8255593be80fcd Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 26 Jun 2018 16:57:15 -0700 Subject: [PATCH 4/8] =?UTF-8?q?joyent/node-triton#250=20triton=20profile?= =?UTF-8?q?=20list=20doesn't=20seem=20to=20work=20without=20full=20env=20R?= =?UTF-8?q?eviewed=20by:=20Robert=20Mustacchi=20=20Reviewed?= =?UTF-8?q?=20by:=20Pedro=20Palaz=C3=B3n=20Candel=20=20A?= =?UTF-8?q?pproved=20by:=20Marsell=20Kukuljevic=20=20A?= =?UTF-8?q?pproved=20by:=20Pedro=20Palaz=C3=B3n=20Candel=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 2 ++ lib/config.js | 10 +++++----- lib/do_profile/do_list.js | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 36548df..194bddc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ Known issues: ## not yet released +- [joyent/node-triton#250] Avoid an error from `triton profile list` if + only *some* of the minimal `TRITON_` or `SDC_` envvars are defined. - [TRITON-401] Add `triton network` and `triton vlan` commands, for creating/changing/removing network fabrics and VLANs. - [TRITON-524] Add `triton inst get --credentials ...` option to match diff --git a/lib/config.js b/lib/config.js index fe2fda0..ea9dc36 100644 --- a/lib/config.js +++ b/lib/config.js @@ -299,12 +299,11 @@ function _loadEnvProfile(profileOverrides) { for (var attr in profileOverrides) { envProfile[attr] = profileOverrides[attr]; } + /* - * If none of the above envvars are defined, then there is no env profile. + * If missing any of the required vars, then there is no env profile. */ - if (!envProfile.account && !envProfile.user && !envProfile.url && - !envProfile.keyId) - { + if (!envProfile.account || !envProfile.url || !envProfile.keyId) { return null; } validateProfile(envProfile, 'environment variables'); @@ -366,10 +365,11 @@ function loadProfile(opts) { function loadAllProfiles(opts) { assert.string(opts.configDir, 'opts.configDir'); assert.object(opts.log, 'opts.log'); + assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides'); var profiles = []; - var envProfile = _loadEnvProfile(); + var envProfile = _loadEnvProfile(opts.profileOverrides); if (envProfile) { profiles.push(envProfile); } diff --git a/lib/do_profile/do_list.js b/lib/do_profile/do_list.js index 35ece1d..c629d6a 100644 --- a/lib/do_profile/do_list.js +++ b/lib/do_profile/do_list.js @@ -31,7 +31,8 @@ function _listProfiles(cli, opts, args, cb) { try { profiles = mod_config.loadAllProfiles({ configDir: cli.configDir, - log: cli.log + log: cli.log, + profileOverrides: cli._cliOptsAsProfile() }); } catch (e) { return cb(e); From dc5dc12052b152759295c33d967d0f3d5de126de Mon Sep 17 00:00:00 2001 From: Todd Whiteman Date: Wed, 27 Jun 2018 17:28:02 -0700 Subject: [PATCH 5/8] TRITON-53 x-account image clone Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 9 +++ lib/cloudapi2.js | 27 ++++++++- lib/common.js | 16 +++++- lib/do_image/do_clone.js | 107 +++++++++++++++++++++++++++++++++++ lib/do_image/do_list.js | 72 ++++++++++++++++++----- lib/do_image/index.js | 2 + lib/do_instance/do_create.js | 8 +++ lib/tritonapi.js | 38 ++++++++++++- package.json | 2 +- 9 files changed, 264 insertions(+), 17 deletions(-) create mode 100644 lib/do_image/do_clone.js diff --git a/CHANGES.md b/CHANGES.md index 194bddc..e44c01b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ Known issues: ## not yet released +## 6.1.0 + - [joyent/node-triton#250] Avoid an error from `triton profile list` if only *some* of the minimal `TRITON_` or `SDC_` envvars are defined. - [TRITON-401] Add `triton network` and `triton vlan` commands, for @@ -17,6 +19,13 @@ Known issues: 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. +- [TRITON-53] x-account image clone. A user can make a copy of a shared image + using the `triton image clone` command. +- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no + longer provisionable by default - you will need to explicitly add the + --allow-shared-images cli option when calling `triton create` command to + provision from a shared image (or clone the image then provision from the + clone). ## 6.0.0 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 3deeefd..f98a773 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -1039,7 +1039,7 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) { * - {Object} fields Required. The fields to update in the image. * @param {Function} cb of the form `function (err, body, res)` */ -CloudApi.prototype.updateImage = function shareImage(opts, cb) { +CloudApi.prototype.updateImage = function updateImage(opts, cb) { assert.uuid(opts.id, 'id'); assert.object(opts.fields, 'fields'); assert.func(cb, 'cb'); @@ -1057,6 +1057,31 @@ CloudApi.prototype.updateImage = function shareImage(opts, cb) { }); }; +/** + * Clone an image. + * + * + * @param {Object} opts + * - {UUID} id Required. The id of the image to update. + * @param {Function} cb of the form `function (err, body, res)` + */ +CloudApi.prototype.cloneImage = function cloneImage(opts, cb) { + assert.uuid(opts.id, 'id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/images/%s?action=clone', this.account, opts.id), + data: {} + }, function (err, req, res, body) { + if (err) { + cb(err, null, res); + return; + } + cb(null, body, res); + }); +}; + /** * Wait for an image to go one of a set of specfic states. * diff --git a/lib/common.js b/lib/common.js index 613732e..73051e3 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1461,6 +1461,19 @@ function parseNicStr(nic) { return obj; } +/* + * Return a short image string that represents the given image object. + * + * @param img {Object} The image object. + * @returns {String} A network object. E.g. + * 'a6cf222d-73f4-414c-a427-5c238ef8e1b7 (jillmin@1.0.0)' + */ +function imageRepr(img) { + assert.object(img); + + return format('%s (%s@%s)', img.id, img.name, img.version); +} + //---- exports @@ -1502,6 +1515,7 @@ module.exports = { readStdin: readStdin, validateObject: validateObject, ipv4ToLong: ipv4ToLong, - parseNicStr: parseNicStr + parseNicStr: parseNicStr, + imageRepr: imageRepr }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/do_image/do_clone.js b/lib/do_image/do_clone.js new file mode 100644 index 0000000..814a570 --- /dev/null +++ b/lib/do_image/do_clone.js @@ -0,0 +1,107 @@ +/* + * 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. + * + * `triton image clone ...` + */ + +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +// ---- the command + +function do_clone(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 1) { + cb(new errors.UsageError( + 'incorrect number of args: expected 1, got ' + args.length)); + return; + } + + var log = this.top.log; + var tritonapi = this.top.tritonapi; + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function cloneImage(ctx, next) { + log.trace({dryRun: opts.dry_run, account: ctx.account}, + 'image clone account'); + + if (opts.dry_run) { + next(); + return; + } + + tritonapi.cloneImage({image: args[0]}, function _cloneCb(err, img) { + if (err) { + next(new errors.TritonError(err, 'error cloning image')); + return; + } + + log.trace({img: img}, 'image clone result'); + + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Cloned image %s to %s', + args[0], common.imageRepr(img)); + } + + next(); + }); + } + ]}, cb); +} + +do_clone.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Other options' + }, + { + names: ['dry-run'], + type: 'bool', + help: 'Go through the motions without actually cloning.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_clone.synopses = [ + '{{name}} {{cmd}} [OPTIONS] IMAGE' +]; + +do_clone.help = [ + /* BEGIN JSSTYLED */ + 'Clone a shared image.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "IMAGE" is an image id (a full UUID), an image name (selects the', + 'latest, by "published_at", image with that name), an image "name@version"', + '(selects latest match by "published_at"), or an image short ID (ID prefix).', + '', + 'Note: Only shared images can be cloned.' + /* END JSSTYLED */ +].join('\n'); + +do_clone.completionArgtypes = ['tritonimage', 'none']; + +module.exports = do_clone; diff --git a/lib/do_image/do_list.js b/lib/do_image/do_list.js index 5ef8063..22eca82 100644 --- a/lib/do_image/do_list.js +++ b/lib/do_image/do_list.js @@ -5,13 +5,15 @@ */ /* - * Copyright 2016 Joyent, Inc. + * Copyright 2018 Joyent, Inc. * * `triton image list ...` */ +var assert = require('assert-plus'); var format = require('util').format; var tabula = require('tabula'); +var vasync = require('vasync'); var common = require('../common'); var errors = require('../errors'); @@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) { listOpts.state = 'all'; } + var self = this; var tritonapi = this.top.tritonapi; - common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { - if (setupErr) { - callback(setupErr); - return; - } - tritonapi.listImages(listOpts, function onRes(err, imgs, res) { - if (err) { - return callback(err); - } + vasync.pipeline({ arg: {}, funcs: [ + function setupTritonApi(_, next) { + common.cliSetupTritonApi({cli: self.top}, next); + }, + function getImages(ctx, next) { + tritonapi.listImages(listOpts, function onRes(err, imgs, res) { + if (err) { + next(err); + return; + } + ctx.imgs = imgs; + next(); + }); + }, + function getUserAccount(ctx, next) { + // If using json output, or when there are no images that use an ACL + // - we don't need to fetch the account, as the account is only used + // to check if the image is shared (i.e. the account is in the image + // ACL) so it can output image flags in non-json mode. + if (opts.json || ctx.imgs.every(function _checkAcl(img) { + return !Array.isArray(img.acl) || img.acl.length === 0; + })) { + next(); + return; + } + tritonapi.cloudapi.getAccount(function _accountCb(err, account) { + if (err) { + next(err); + return; + } + ctx.account = account; + next(); + }); + }, + function formatImages(ctx, next) { + var imgs = ctx.imgs; if (opts.json) { common.jsonStream(imgs); } else { @@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) { if (img.origin) flags.push('I'); if (img['public']) flags.push('P'); if (img.state !== 'active') flags.push('X'); + + // Add image sharing flags. + if (Array.isArray(img.acl) && img.acl.length > 0) { + assert.string(ctx.account, 'ctx.account'); + if (img.owner === ctx.account.id) { + // This image has been shared with other accounts. + flags.push('+'); + } + if (img.acl.indexOf(ctx.account.id) !== -1) { + // This image has been shared with this account. + flags.push('S'); + } + } + img.flags = flags.length ? flags.join('') : undefined; } @@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) { sort: sort }); } - callback(); - }); - }); + next(); + } + ]}, callback); } do_list.options = [ @@ -157,6 +201,8 @@ do_list.help = [ ' shortid* A short ID prefix.', ' flags* Single letter flags summarizing some fields:', ' "P" image is public', + ' "+" you are sharing this image with others', + ' "S" this image has been shared with you', ' "I" an incremental image (i.e. has an origin)', ' "X" has a state *other* than "active"', ' pubdate* Short form of "published_at" with just the date', diff --git a/lib/do_image/index.js b/lib/do_image/index.js index 832cd59..e719037 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -33,6 +33,7 @@ function ImageCLI(top) { 'help', 'list', 'get', + 'clone', 'create', 'delete', 'export', @@ -51,6 +52,7 @@ ImageCLI.prototype.init = function init(opts, args, cb) { ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_get = require('./do_get'); +ImageCLI.prototype.do_clone = require('./do_clone'); ImageCLI.prototype.do_create = require('./do_create'); ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_export = require('./do_export'); diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index 7bc5291..cbedb35 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -289,6 +289,9 @@ function do_create(subcmd, opts, args, cb) { createOpts['tag.'+key] = ctx.tags[key]; }); } + if (opts.allow_shared_images) { + createOpts.allow_shared_images = true; + } for (var i = 0; i < opts._order.length; i++) { var opt = opts._order[i]; @@ -498,6 +501,11 @@ do_create.options = [ 'Joyent-provided images, the user-script is run at every boot ' + 'of the instance. This is a shortcut for `-M user-script=FILE`.' }, + { + names: ['allow-shared-images'], + type: 'bool', + help: 'Allow instance creation to use a shared image.' + }, { group: 'Other options' diff --git a/lib/tritonapi.js b/lib/tritonapi.js index b76aab1..f32e330 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -133,7 +133,7 @@ var errors = require('./errors'); // ---- globals -var CLOUDAPI_ACCEPT_VERSION = '~8'; +var CLOUDAPI_ACCEPT_VERSION = '~9||~8'; @@ -958,6 +958,42 @@ TritonApi.prototype.unshareImage = function unshareImage(opts, cb) }); }; +/** + * Clone a shared image. + * + * @param {Object} opts + * - {String} image The image UUID, name, or short ID. Required. + * @param {Function} cb `function (err, img)` + * On failure `err` is an error instance, else it is null. + * On success: `img` is the cloned image object. + */ +TritonApi.prototype.cloneImage = function cloneImage(opts, cb) +{ + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.image, 'opts.image'); + assert.func(cb, 'cb'); + + var arg = { + image: opts.image, + client: self + }; + var img; + + vasync.pipeline({arg: arg, funcs: [ + _stepImg, + function cloudApiCloneImage(ctx, next) { + self.cloudapi.cloneImage({id: ctx.img.id}, + function _cloneImageCb(err, img_) { + img = img_; + next(err); + }); + } + ]}, function (err) { + cb(err, img); + }); +}; + /** * Get an active package by ID, exact name, or short ID, in that order. * diff --git a/package.json b/package.json index e0da727..2c7354f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "6.0.0", + "version": "6.1.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": { From c86804cfe416360d6918a476f05de2c5cdd743e5 Mon Sep 17 00:00:00 2001 From: Todd Whiteman Date: Fri, 29 Jun 2018 16:44:17 -0700 Subject: [PATCH 6/8] TRITON-52 x-DC image copy Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 7 +++ lib/cloudapi2.js | 34 ++++++++++++ lib/do_image/do_copy.js | 119 ++++++++++++++++++++++++++++++++++++++++ lib/do_image/do_list.js | 2 +- lib/do_image/index.js | 2 + lib/tritonapi.js | 100 +++++++++++++++++++++++++++++++-- 6 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 lib/do_image/do_copy.js diff --git a/CHANGES.md b/CHANGES.md index e44c01b..8b5dca7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,13 @@ Known issues: --allow-shared-images cli option when calling `triton create` command to provision from a shared image (or clone the image then provision from the clone). +- [TRITON-52] x-DC image copy. A user can copy an image that they own into + another datacenter within the same cloud using the `triton image copy` cli + command. Example: + + ``` + triton -p us-east-1 image cp my-custom-image us-sw-1 + ``` ## 6.0.0 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index f98a773..945b8d5 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -1082,6 +1082,40 @@ CloudApi.prototype.cloneImage = function cloneImage(opts, cb) { }); }; +/** + * Import image from another datacenter in the same cloud. + * + * + * @param {Object} opts + * - {String} datacenter Required. The datacenter to import from. + * - {UUID} id Required. The id of the image to update. + * @param {Function} cb of the form `function (err, body, res)` + */ +CloudApi.prototype.importImageFromDatacenter = +function importImageFromDatacenter(opts, cb) { + assert.string(opts.datacenter, 'datacenter'); + assert.uuid(opts.id, 'id'); + assert.func(cb, 'cb'); + + var p = this._path(format('/%s/images', this.account), { + action: 'import-from-datacenter', + datacenter: opts.datacenter, + id: opts.id + }); + + this._request({ + method: 'POST', + path: p, + data: {} + }, function (err, req, res, body) { + if (err) { + cb(err, null, res); + return; + } + cb(null, body, res); + }); +}; + /** * Wait for an image to go one of a set of specfic states. * diff --git a/lib/do_image/do_copy.js b/lib/do_image/do_copy.js new file mode 100644 index 0000000..88f673b --- /dev/null +++ b/lib/do_image/do_copy.js @@ -0,0 +1,119 @@ +/* + * 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. + * + * `triton image copy ...` + */ + +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +// ---- the command + +function do_copy(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 2) { + cb(new errors.UsageError( + 'incorrect number of args: expected 2, got ' + args.length)); + return; + } + + var log = this.top.log; + var tritonapi = this.top.tritonapi; + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function copyImage(ctx, next) { + log.trace({dryRun: opts.dry_run, account: ctx.account, args: args}, + 'image copy'); + + if (opts.dry_run) { + next(); + return; + } + + tritonapi.copyImageToDatacenter( + {image: args[0], datacenter: args[1]}, + function (err, img) { + if (err) { + next(new errors.TritonError(err, 'error copying image')); + return; + } + + log.trace({img: img}, 'image copy result'); + + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Copied image %s to datacenter %s', + common.imageRepr(img), args[1]); + } + + next(); + }); + } + ]}, function (err) { + cb(err); + }); +} + +do_copy.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Other options' + }, + { + names: ['dry-run'], + type: 'bool', + help: 'Go through the motions without actually copying.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_copy.synopses = [ + '{{name}} {{cmd}} [OPTIONS] IMAGE DATACENTER' +]; + +do_copy.help = [ + /* BEGIN JSSTYLED */ + 'Copy image to another datacenter.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "IMAGE" is an image id (a full UUID), an image name (selects the', + 'latest, by "published_at", image with that name), an image "name@version"', + '(selects latest match by "published_at"), or an image short ID (ID prefix).', + 'You must be the owner of the image to copy it. (You can use `triton image', + 'clone` to get your own image clone of an image shared to you.)', + '', + '"DATACENTER" is the name of the datacenter to which to copy your image.', + 'Use `triton datacenters` to show the available datacenter names.' + /* END JSSTYLED */ +].join('\n'); + +do_copy.aliases = ['cp']; + +// TODO: tritonimage should really be 'tritonownedimage' or something to +// limit to images owned by this account +// TODO: tritondatacenter bash completion +do_copy.completionArgtypes = ['tritonimage', 'tritondatacenter', 'none']; + +module.exports = do_copy; diff --git a/lib/do_image/do_list.js b/lib/do_image/do_list.js index 22eca82..89aaad9 100644 --- a/lib/do_image/do_list.js +++ b/lib/do_image/do_list.js @@ -132,7 +132,7 @@ function do_list(subcmd, opts, args, callback) { // Add image sharing flags. if (Array.isArray(img.acl) && img.acl.length > 0) { - assert.string(ctx.account, 'ctx.account'); + assert.string(ctx.account.id, 'ctx.account.id'); if (img.owner === ctx.account.id) { // This image has been shared with other accounts. flags.push('+'); diff --git a/lib/do_image/index.js b/lib/do_image/index.js index e719037..6ee6e4b 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -34,6 +34,7 @@ function ImageCLI(top) { 'list', 'get', 'clone', + 'copy', 'create', 'delete', 'export', @@ -53,6 +54,7 @@ ImageCLI.prototype.init = function init(opts, args, cb) { ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_get = require('./do_get'); ImageCLI.prototype.do_clone = require('./do_clone'); +ImageCLI.prototype.do_copy = require('./do_copy'); ImageCLI.prototype.do_create = require('./do_create'); ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_export = require('./do_export'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index f32e330..6e26c1d 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -493,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { ? true : !profile.insecure); var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; - var opts = { + self._cloudapiOpts = { url: profile.url, account: profile.actAsAccount || profile.account, principal: { @@ -509,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { if (profile.privKey) { var key = sshpk.parsePrivateKey(profile.privKey); this.keyPair = - opts.principal.keyPair = + self._cloudapiOpts.principal.keyPair = auth.KeyPair.fromPrivateKey(key); - this.cloudapi = cloudapi.createClient(opts); + this.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); } else { var kr = new auth.KeyRing(); @@ -521,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { cb(err); return; } - self.keyPair = opts.principal.keyPair = kp; - self.cloudapi = cloudapi.createClient(opts); + self.keyPair = self._cloudapiOpts.principal.keyPair = kp; + self.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); }); } @@ -994,6 +994,96 @@ TritonApi.prototype.cloneImage = function cloneImage(opts, cb) }); }; +/** + * Copy an image to another Datacenter. + * + * Note: This somewhat flips the sense of the CloudAPI ImportImageFromDatacenter + * endpoint, in that it instead calls *the target DC* to pull from this + * profile's DC. The target DC's CloudAPI URL is determined from this DC's + * `ListDatacenters` endpoint. It is assumed that all other Triton profile + * attributes (account, keyId) suffice to auth with the target DC. + * + * @param {Object} opts + * - {String} datacenter The datacenter name to copy to. Required. + * - {String} image The image UUID, name, or short ID. Required. + * @param {Function} cb `function (err, img)` + * On failure `err` is an error instance, else it is null. + * On success: `img` is the copied image object. + */ +TritonApi.prototype.copyImageToDatacenter = +function copyImageToDatacenter(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.datacenter, 'opts.datacenter'); + assert.string(opts.image, 'opts.image'); + assert.func(cb, 'cb'); + + var arg = { + client: self, + datacenter: opts.datacenter, + image: opts.image + }; + var img; + + vasync.pipeline({arg: arg, funcs: [ + _stepImg, + function getDatacenters(ctx, next) { + self.cloudapi.listDatacenters({}, function (err, dcs, res) { + if (err) { + next(err); + return; + } + if (!dcs.hasOwnProperty(ctx.datacenter)) { + next(new errors.TritonError(format( + '"%s" is not a valid datacenter name, possible ' + + 'names are: %s', + ctx.datacenter, + Object.keys(dcs).join(', ')))); + return; + } + ctx.datacenterUrl = dcs[ctx.datacenter]; + assert.string(ctx.datacenterUrl, 'ctx.datacenterUrl'); + + // CloudAPI added image copying in 9.2.0, which is also + // the version that included this header. + var currentDcName = res.headers['triton-datacenter-name']; + if (!currentDcName) { + next(new errors.TritonError(err, format( + 'this datacenter does not support image copying (%s)', + res.headers['server']))); + return; + } + // Note: currentDcName is where the image currently resides. + ctx.currentDcName = currentDcName; + + next(); + }); + }, + function cloudApiImportImageFromDatacenter(ctx, next) { + var targetCloudapiOpts = jsprim.mergeObjects( + { + url: ctx.datacenterUrl, + log: self.log.child({datacenter: opts.datacenter}, true) + }, + null, + self._cloudapiOpts + ); + var targetCloudapi = cloudapi.createClient(targetCloudapiOpts); + + targetCloudapi.importImageFromDatacenter({ + datacenter: ctx.currentDcName, + id: ctx.img.id + }, function _importImageCb(err, img_) { + targetCloudapi.close(); + img = img_; + next(err); + }); + } + ]}, function (err) { + cb(err, img); + }); +}; + /** * Get an active package by ID, exact name, or short ID, in that order. * From aa58982e2a16dfaf4f83768d6eaff3c1aa4809fc Mon Sep 17 00:00:00 2001 From: "Joshua M. Clulow" Date: Tue, 17 Jul 2018 16:51:13 +0000 Subject: [PATCH 7/8] TRITON-598 "triton network get-default" should print error when no default network is set Reviewed by: Robert Mustacchi Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 5 +++++ lib/do_network/do_get_default.js | 4 ++-- package.json | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8b5dca7..1b3d4c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,11 @@ Known issues: ## not yet released +## 6.1.1 + +- [TRITON-598] Fix error handling for `triton network get-default` when + no default network is set on the account. + ## 6.1.0 - [joyent/node-triton#250] Avoid an error from `triton profile list` if diff --git a/lib/do_network/do_get_default.js b/lib/do_network/do_get_default.js index 3340547..df848ea 100644 --- a/lib/do_network/do_get_default.js +++ b/lib/do_network/do_get_default.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright (c) 2018, Joyent, Inc. * * `triton network get-default ...` */ @@ -47,7 +47,7 @@ function do_get_default(subcmd, opts, args, cb) { var defaultNetwork = conf.default_network; if (!defaultNetwork) { - cb('default network not found'); + cb(new Error('account has no default network configured')); return; } diff --git a/package.json b/package.json index 2c7354f..337c993 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "6.1.0", + "version": "6.1.1", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": { From aea9b2b7b335fc24862ab76acde47821b41fdcb4 Mon Sep 17 00:00:00 2001 From: Todd Whiteman Date: Thu, 19 Jul 2018 17:17:50 -0700 Subject: [PATCH 8/8] joyent/node-triton#249 Error when creating or deleting profiles when using node v10 Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 5 +++++ lib/common.js | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1b3d4c3..501276a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,11 @@ Known issues: ## not yet released +## 6.1.2 + +- [joyent/node-triton#249] Error when creating or deleting profiles when + using node v10. + ## 6.1.1 - [TRITON-598] Fix error handling for `triton network get-default` when diff --git a/lib/common.js b/lib/common.js index 73051e3..609d0a2 100644 --- a/lib/common.js +++ b/lib/common.js @@ -618,9 +618,9 @@ function promptYesNo(opts_, cb) { stdin.on('data', onData); function postInput() { + stdout.write('\n'); stdin.setRawMode(false); stdin.pause(); - stdin.write('\n'); stdin.removeListener('data', onData); } diff --git a/package.json b/package.json index 337c993..909b0af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "6.1.1", + "version": "6.1.2", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": {