From a08974ebd9599668509eec6bd39582e7871165c3 Mon Sep 17 00:00:00 2001 From: Jason King Date: Wed, 16 Dec 2020 16:39:20 -0600 Subject: [PATCH] TRITON-2191 Initial support of AWS style VPCs --- CHANGES.md | 4 + lib/cli.js | 6 +- lib/cloudapi2.js | 203 ++++++++++++++++++++++++++++++++++++++ lib/do_vpc/do_create.js | 142 ++++++++++++++++++++++++++ lib/do_vpc/do_delete.js | 85 ++++++++++++++++ lib/do_vpc/do_get.js | 88 +++++++++++++++++ lib/do_vpc/do_list.js | 119 ++++++++++++++++++++++ lib/do_vpc/do_networks.js | 52 ++++++++++ lib/do_vpc/do_update.js | 201 +++++++++++++++++++++++++++++++++++++ lib/do_vpc/index.js | 54 ++++++++++ lib/tritonapi.js | 159 ++++++++++++++++++++++++++++- package-lock.json | 2 +- package.json | 2 +- 13 files changed, 1113 insertions(+), 4 deletions(-) create mode 100644 lib/do_vpc/do_create.js create mode 100644 lib/do_vpc/do_delete.js create mode 100644 lib/do_vpc/do_get.js create mode 100644 lib/do_vpc/do_list.js create mode 100644 lib/do_vpc/do_networks.js create mode 100644 lib/do_vpc/do_update.js create mode 100644 lib/do_vpc/index.js diff --git a/CHANGES.md b/CHANGES.md index 704436a..c798e1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ Known issues: ## not yet released +## 7.13.0 + +- [TRITON-2182] Added `triton changefeed` subcommand + ## 7.12.2 - Add in sourcing from an instance tag an alternate port to ssh to for diff --git a/lib/cli.js b/lib/cli.js index ef5e799..577a910 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2017, Joyent, Inc. + * Copyright 2020 Joyent, Inc. * * The `triton` CLI class. */ @@ -211,6 +211,7 @@ function CLI() { 'network', 'fwrule', 'vlan', + 'vpc', { group: 'Other Commands' }, 'info', 'account', @@ -710,6 +711,9 @@ CLI.prototype.do_network = require('./do_network'); // VLANs CLI.prototype.do_vlan = require('./do_vlan'); +// VPCs +CLI.prototype.do_vpc = require('./do_vpc'); + // 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 629a5c3..0be4fad 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -804,6 +804,209 @@ function deleteFabricVlan(opts, cb) { }); }; +// ---- VPCs + +/* + * Creates a VPC. + * + * @param {Object} options object containing: + * - {String} name (required) A unique name to identify the VPC. + * - {String} cidr (required) A CIDR description of the VPC. + * - {String} description (optional) + * @param {Function} callback for the form f(err, vpc, res). + */ +CloudApi.prototype.createVPC = +function createVPC(opts, cb) { + assert.objects(opts, 'opts'); + assert.string(opts.subnet, 'opts.subnet'); + assert.string(opts.name, 'opts.name'); + assert.optionalString(opts.description, 'opts.description'); + + var data = common.objCopy(opts); + + this._request({ + method: 'POST', + path: format('/%s/vpc', this.account), + data: data + }, function onReq(err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * List all VPCs for a user. + * + * Returns an array of objects. + * + * @param opts {Object} Options + * @param {Function} callback of the form f(err, vpcs, res). + */ +CloudApi.prototype.listVPCs = +function listVPCs(opts, cb) { + assert.object(opts, 'opts'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/vpc', this.account); + this._passThrough(endpoint, opts, cb); +}; + +CloudApi.prototype.getVPC = +function getVPC(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.vpc_id, 'opts.vpc_id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/vpc/%s', this.account, opts.vpc_id); + this._request(endpoint, function onReq(err, req, res, body) { + cb(err, body, res); + }); +}; + +// -> +CloudApi.prototype.UPDATE_VPC_FIELDS = { + name: 'string', + description: 'string' +}; + +/** + * Updates a VPC. + * + * @param {Object} opts opject containing: + * - {String} vpc_id: The VPC uuid. Required. + * - {String} name: The VPC name. Optional. + * - {String} description: Description of the VLAN. Optional. + * @param {Function} callback for the form `function (err, vpc, res)` + */ +CloudApi.prototype.updateVPC = +function updateVPC(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.vpc_id, 'opts.vpc_id'); + assert.optionalString(opts.name, 'opts.name'); + assert.optionalStirng(opts.description, 'opts.description'); + assert.func(cb, 'cb'); + + var data = {}; + Object.keys(this.UPDATE_VPC_FIELDS).forEach(function forCb(attr) { + if (opts[attr] !== undefined) + data[attr] = opts[attr]; + }); + + var vpcId = opts.vpc_id; + + this._request({ + method: 'POST', + path: format('/%s/vpc/%s', this.account, vpcId), + data: data + }, function onReq(err, req, res, body) { + cb(err, body, res); + }); +}; + +/* + * Remove a VPC. + * + * @param {Object} opts (object) + * - {String} vpc_id: The VPC uuid. Required. + * @param {Function} cb of the form `function (err, res)`. + */ +CloudApi.prototype.deleteVPC = +function deleteVPC(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.vpc_id, 'opts.vpc_id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/vpc/%s', this.account, opts.vpc_id) + }, function onReq(err, req, res) { + cb(err, res); + }); +}; + +/** + * Creates a network on a VPC + * + * @param {Object} options object containing: + * - {String} vpc_id (required) The VPC uuid + * - {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. + * - {Array} resolvers (optional) DNS resolvers for hosts on network. + * - {Object} routes (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.createVPCNetwork = +function createVPCNetwork(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.vpc_id, 'opts.vpc_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.optionalArrayOfString(opts.resolvers, 'opts.resolvers'); + assert.optionalObject(opts.routes, 'opts.routes'); + assert.optionalBool(opts.internet_nat, 'opts.internet_nat'); + + var data = common.objCopy(opts); + var vpcId = data.vpc_id; + delete data.vpc_id; + + this._request({ + method: 'POST', + path: format('/%s/vpc/%s/networks', this.account, vpcId), + data: data + }, function reqCb(err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * Lists all networks on a VPC. + * + * Returns an array of objects. + * + * @param {Object} options object containing: + * - {String} vpc_id (required) VPC uuid. + * @param {Function} callback of the form f(err, networks, res). + */ +CloudApi.prototype.listVPCNetworks = +function listVPCNetworks(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.vpc_id, 'opts.vpc_id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/vpc/%s/networks', this.account, opts.vpc_id); + this._passThrough(endpoint, opts, cb); +}; + +/** + * Remove a VPC network + * + * @param {Object} opts (object) + * - {String} id: The network id. Required. + * - {String} vpc_id: The VPC id. Required. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deleteVPCNetwork = +function deleteVPCNetwork(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.uuid(opts.vpc_id, 'opts.vpc_id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/vpc/networks/%s', this.account, opts.vpc_id, opts.id) + }, function reqCb(err, req, res) { + cb(err, res); + }); +}; // ---- datacenters diff --git a/lib/do_vpc/do_create.js b/lib/do_vpc/do_create.js new file mode 100644 index 0000000..ed5de16 --- /dev/null +++ b/lib/do_vpc/do_create.js @@ -0,0 +1,142 @@ +/* + * 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 2021 Joyent, Inc. + * + * `triton vpc create ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var jsprim = require('jsprim'); +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 CIDR block')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + var cidr = args[0]; + + if (typeof (cidr) !== 'string') { + cb(new errors.UsageError('CIDR must be a string')); + return; + } + + if (!opts.name) { + cb(new errors.UsageError('must provide a --name (-n)')); + return; + } + + var createOpts = { + name: opts.name, + ip4_cidr: cidr + }; + + 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.createVPC(createOpts, function onCreate(err, vpc) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(vpc)); + } else { + if (vpc.name) { + console.log('Created VPC %s (%s)', vpc.name, + vpc.vpc_id); + } else { + console.log('Created vlan %s', vpc.vpc_id); + } + } + + cb(); + }); + }); +} + + +do_create.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Create options' + }, + { + names: ['name', 'n'], + type: 'string', + helpArg: 'NAME', + help: 'Name of the VPC.' + }, + { + names: ['description', 'D'], + type: 'string', + helpArg: 'DESC', + help: 'Description of the VPC.' + }, + { + group: 'Other options' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] CIDR']; + +do_create.help = [ + 'Create a VPC.', + '', + '{{usage}}', + '', + '{{options}}', + 'Example:', + ' triton vpc create -n "prod" -D "Production VPC" 192.168.0.0/16' +].join('\n'); + +do_create.helpOpts = { + helpCol: 16 +}; + +module.exports = do_create; diff --git a/lib/do_vpc/do_delete.js b/lib/do_vpc/do_delete.js new file mode 100644 index 0000000..2038972 --- /dev/null +++ b/lib/do_vpc/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 2020 Joyent, Inc. + * + * `triton vpc 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 VPC argument(s)')); + return; + } + + var cli = this.top; + var vpcIds = args; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + vasync.forEachParallel({ + inputs: vpcIds, + func: function deleteOne(id, next) { + cli.tritonapi.deleteVPC({ vpc_id: id }, + function onDelete(err) { + if (err) { + next(err); + return; + } + + console.log('Deleted VPC %s', id); + next(); + }); + } + }, cb); + }); +} + + +do_delete.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +]; + +do_delete.synopses = ['{{name}} {{cmd}} VPC [VPC ...]']; + +do_delete.help = [ + 'Remove a VPC.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VPC is a VPC id or name.' +].join('\n'); + +do_delete.aliases = ['rm']; + +do_delete.completionArgtypes = ['tritonvpc']; + +module.exports = do_delete; diff --git a/lib/do_vpc/do_get.js b/lib/do_vpc/do_get.js new file mode 100644 index 0000000..c24efdf --- /dev/null +++ b/lib/do_vpc/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 2020 Joyent, Inc. + * + * `triton vpc 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 VPC 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.getVPC(id, function onGet(err, vpc) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(vpc)); + } else { + console.log(JSON.stringify(vpc, 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}} VPC']; + +do_get.help = [ + 'Show a specific VPC.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VPC is a VPC id or name.' +].join('\n'); + +do_get.completionArgtypes = ['tritonvpc', 'none']; + +module.exports = do_get; diff --git a/lib/do_vpc/do_list.js b/lib/do_vpc/do_list.js new file mode 100644 index 0000000..c56ae90 --- /dev/null +++ b/lib/do_vpc/do_list.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 2020 Joyent, Inc. + * + * `triton vpc list ...` + */ + +var assert = require('assert-plus'); +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + +var COLUMNS_DEFAULT = 'vpc_id,name,description'; +var SORT_DEFAULT = 'vpc_id'; +var VALID_FILTERS = ['vpc_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; + } + + var cli = this.top; + + common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + var cloudapi = cli.tritonapi.cloudapi; + cloudapi.listVPCs({}, function onList(err, vpcs) { + if (err) { + cb(err); + return; + } + + // do filtering + Object.keys(filters).forEach(function doFilter(key) { + var val = filters[key]; + vpcs = vpcs.filter(function eachVPC(vpc) { + return vpc[key] === val; + }); + }); + + if (opts.json) { + common.jsonStream(vpcs); + } else { + var columns = COLUMNS_DEFAULT; + + if (opts.o) { + columns = opts.o; + } + + columns = columns.split(','); + var sort = opts.s.split(','); + + tabula(vpcs, { + 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 VPCs.', + '', + '{{usage}}', + '', + 'Filters:', + ' FIELD= UUID filter. Supported fields: vpc_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_vpc/do_networks.js b/lib/do_vpc/do_networks.js new file mode 100644 index 0000000..483f24b --- /dev/null +++ b/lib/do_vpc/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 2020 Joyent, Inc. + * + * `triton vpc 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 VPC argument')); + return; + } else if (args.length > 1) { + cb(new errors.UsageError('incorrect number of arguments')); + return; + } + + opts.vpc_id = args[0]; + + this.top.handlerFromSubcmd('network').dispatch({ + subcmd: 'list', + opts: opts, + args: [] + }, cb); +} + +do_networks.synopses = ['{{name}} {{cmd}} [OPTIONS] VPC']; + +do_networks.help = [ + 'Show all networks on a VPC.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where VPC is a VPC id or name.' +].join('\n'); + +do_networks.options = require('../do_network/do_list').options; + +module.exports = do_networks; diff --git a/lib/do_vpc/do_update.js b/lib/do_vpc/do_update.js new file mode 100644 index 0000000..5bd16fe --- /dev/null +++ b/lib/do_vpc/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 2020 Joyent, Inc. + * + * `triton vpc 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_VPC_FIELDS + = require('../cloudapi2').CloudApi.prototype.UPDATE_VPC_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 VPC 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_VPC_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 vpc 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 VPC update JSON on stdin'); + next(new errors.TritonError(format( + 'invalid JSON for VPC 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_VPC_FIELDS[key]; + if (!type) { + next(new errors.UsageError(format('unknown or ' + + 'unupdateable field: %s (updateable fields are: %s)', + key, + Object.keys(UPDATE_VPC_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.vpc_id = id; + + tritonapi.updateVPC(data, function onUpdate(err) { + if (err) { + next(err); + return; + } + + delete data.vpc_id; + console.log('Updated vpc %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}} VPC [FIELD=VALUE ...]', + '{{name}} {{cmd}} -f JSON-FILE VPC' +]; + +do_update.help = [ + 'Update a VPC.', + '', + '{{usage}}', + '', + '{{options}}', + + 'Updateable fields:', + ' ' + Object.keys(UPDATE_VPC_FIELDS).sort().map(function (f) { + return f + ' (' + UPDATE_VPC_FIELDS[f] + ')'; + }).join(', '), + '', + 'Where VPC is a VPC id or name.' +].join('\n'); + +do_update.completionArgtypes = ['tritonvpc', 'tritonupdatevpcfield']; + +module.exports = do_update; diff --git a/lib/do_vpc/index.js b/lib/do_vpc/index.js new file mode 100644 index 0000000..360e70c --- /dev/null +++ b/lib/do_vpc/index.js @@ -0,0 +1,54 @@ +/* + * 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 2020 Joyent, Inc. + * + * `triton vpc ...` + */ + +var Cmdln = require('cmdln').Cmdln; +var util = require('util'); + + + +// ---- CLI class +function VPCCLI(top) { + this.top = top; + + Cmdln.call(this, { + name: top.name + ' vpc', + desc: 'List and manage Triton VPCs.', + helpSubcmds: [ + 'help', + 'list', + 'get', + 'create', + 'update', + 'delete', + { group: '' }, + 'networks' + ], + helpOpts: { + minHelpCol: 23 + } + }); +} +util.inherits(VPCCLI, Cmdln); + +VPCCLI.prototype.init = function init(opts, args, cb) { + this.log = this.top.log; + Cmdln.prototype.init.apply(this, arguments); +}; + +VPCCLI.prototype.do_list = require('./do_list'); +VPCCLI.prototype.do_create = require('./do_create'); +VPCCLI.prototype.do_get = require('./do_get'); +VPCCLI.prototype.do_update = require('./do_update'); +VPCCLI.prototype.do_delete = require('./do_delete'); +VPCCLI.prototype.do_networks = require('./do_networks'); + +module.exports = VPCCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index e6f0b9a..a95fc57 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -392,7 +392,34 @@ function _stepFabricVlanId(arg, next) { }); } +/** + * A function appropriate for `vasync.pipeline` funcs that takes a `arg.vpc_id`, + * where `arg.vpc_id` is a VPC name, shortid, uuid. Sets the VPC UUID as + * `arg.vpcId`. + */ +function _stepVPCId(arg, next) { + assert.object(arg, 'arg'); + assert.object(arg.client, 'arg.client'); + assert.string(arg.vpc_id, 'arg.vpc_id'); + + var vpcId = arg.vpc_id; + + if (!common.isUUID()) { + arg.vpcId = vpcId; + next(); + return; + } + + arg.client.getVPC(vpcId, function onGet(err, vpc) { + if (err) { + next(err); + return; + } + arg.vpcId = vpc.vpc_id; + next(); + }); +} // ---- TritonApi class @@ -1278,7 +1305,6 @@ function listFabricNetworks(opts, cb) { }); }; - /** * 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. @@ -1306,6 +1332,63 @@ function deleteFabricNetwork(opts, cb) { ]}, cb); }; +/** + * List all of the networks on a VPC. Takes a VPC's UUID, short name, or name + * as an argument. + */ +TritonApi.prototype.listVPCNetworks = +function listVPCNetworks(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.vpc_id, 'opts.vpc_id'); + assert.func(cb, 'cb'); + + var self = this; + var networks; + + vasync.pipeline({ + arg: {client: self, vpc_id: opts.vpc_id}, funcs: [ + _stepVPCId, + + function listNetworks(arg, next) { + self.cloudapi.listVPCNetworks({ + vpc_id: arg.vpcId + }, function listCb(err, nets) { + if (err) { + next(err); + return; + } + + networks = nets; + + next(); + }); + } + ]}, function vaCb(err) { + cb(err, networks); + }); +}; + +TritonApi.prototype.deleteVPCNetwork = +function deleteVPCNetwork(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.deleteVPCFabricNetwork({ + id: arg.netId, vlan_id: arg.vlanId + }, next); + } + ]}, cb); +}; + /** * List a network's IPs. * @@ -3181,6 +3264,80 @@ TritonApi.prototype.deleteFabricVlan = function deleteFabricVlan(opts, cb) { } }; +// ---- VPC + +TritonApi.prototype.getVPC = function getVPC(id, cb) { + assert.string(id, 'id'); + assert.func(cb, 'cb'); + + if (common.isUUID(id)) { + this.cloudapi.getVPC(id, function getCb(err, vpc) { + if (err) { + cb(err); + return; + } + cb(null, vpc); + }); + return; + } + + this.cloudapi.listVPCs({}, function listCb(err, vpcs) { + if (err) { + cb(err); + return; + } + + var shortIdMatches = vpcs.filter(function shortIdCb(vpc) { + return vpc.id.slice(0, 8) === id; + }); + + var nameMatches = vpcs.filter(function nameCb(vpc) { + return vpc.hasOwnProperty('name') && vpc.name === id; + }); + + if (shortIdMatches.length === 1) { + cb(null, shortIdMatches[0]); + return; + } + + if (nameMatches.length === 1) { + cb(null, nameMatches[0]); + return; + } + + if (shortIdMatches.length === 0 && nameMatches.length === 0) { + cb(new errors.ResourceNotFoundError( + format('no VPC with name or short id "%s" was found', id))); + return; + } + + cb(new errors.ResourceNotFoundError( + format('"%s" is an ambiguous VPC identifier with multiple ' + + 'matching names', id))); + }); +}; + +TritonApi.prototype.deleteVpc = function deleteVPC(opts, cb) { + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var self = this; + var vpcId = opts.vpc_id; + + if (common.isUUID(vpcId)) { + self.cloudapi.deleteVPC({vpc_id: vpcId}, cb); + return; + } + + self.getVPC(vpcId, function onGet(err, vpc) { + if (err) { + cb(err); + return; + } + + self.cloudapi.deleteVPC({vpc_id: vpcId}, cb); + }); +}; // ---- RBAC diff --git a/package-lock.json b/package-lock.json index ecfaa6f..5c3f665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "triton", - "version": "7.12.0", + "version": "7.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0685553..7debfc4 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": "7.12.2", + "version": "7.13.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": {