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,