diff --git a/.gitignore b/.gitignore index 4f3616f..88cbe0a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /triton-*.tgz .DS_Store .git +*.swp diff --git a/CHANGES.md b/CHANGES.md index 51acbd5..501276a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,10 +6,43 @@ Known issues: ## not yet released +## 6.1.2 + +- [joyent/node-triton#249] Error when creating or deleting profiles when + using node v10. + +## 6.1.1 + +- [TRITON-598] Fix error handling for `triton network get-default` when + no default network is set on the account. + +## 6.1.0 + +- [joyent/node-triton#250] Avoid an error from `triton profile list` if + only *some* of the minimal `TRITON_` or `SDC_` envvars are defined. +- [TRITON-401] Add `triton network` and `triton vlan` commands, for + 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. - [joyent/node-triton#245] `triton profile` now generates fresh new keys during Docker setup and signs them with an account key, rather than copying (and decrypting) the account key itself. This makes using Docker simpler with keys in an SSH Agent. +- [TRITON-53] x-account image clone. A user can make a copy of a shared image + using the `triton image clone` command. +- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no + longer provisionable by default - you will need to explicitly add the + --allow-shared-images cli option when calling `triton create` command to + provision from a shared image (or clone the image then provision from the + clone). +- [TRITON-52] x-DC image copy. A user can copy an image that they own into + another datacenter within the same cloud using the `triton image copy` cli + command. Example: + + ``` + triton -p us-east-1 image cp my-custom-image us-sw-1 + ``` ## 6.0.0 diff --git a/lib/cli.js b/lib/cli.js index 5ad69b4..b53bef9 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -206,6 +206,7 @@ function CLI() { 'package', 'network', 'fwrule', + 'vlan', { group: 'Other Commands' }, 'info', 'account', @@ -700,6 +701,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 94c57eb..945b8d5 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 /** @@ -775,7 +1039,7 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) { * - {Object} fields Required. The fields to update in the image. * @param {Function} cb of the form `function (err, body, res)` */ -CloudApi.prototype.updateImage = function shareImage(opts, cb) { +CloudApi.prototype.updateImage = function updateImage(opts, cb) { assert.uuid(opts.id, 'id'); assert.object(opts.fields, 'fields'); assert.func(cb, 'cb'); @@ -793,6 +1057,65 @@ CloudApi.prototype.updateImage = function shareImage(opts, cb) { }); }; +/** + * Clone an image. + * + * + * @param {Object} opts + * - {UUID} id Required. The id of the image to update. + * @param {Function} cb of the form `function (err, body, res)` + */ +CloudApi.prototype.cloneImage = function cloneImage(opts, cb) { + assert.uuid(opts.id, 'id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'POST', + path: format('/%s/images/%s?action=clone', this.account, opts.id), + data: {} + }, function (err, req, res, body) { + if (err) { + cb(err, null, res); + return; + } + cb(null, body, res); + }); +}; + +/** + * Import image from another datacenter in the same cloud. + * + * + * @param {Object} opts + * - {String} datacenter Required. The datacenter to import from. + * - {UUID} id Required. The id of the image to update. + * @param {Function} cb of the form `function (err, body, res)` + */ +CloudApi.prototype.importImageFromDatacenter = +function importImageFromDatacenter(opts, cb) { + assert.string(opts.datacenter, 'datacenter'); + assert.uuid(opts.id, 'id'); + assert.func(cb, 'cb'); + + var p = this._path(format('/%s/images', this.account), { + action: 'import-from-datacenter', + datacenter: opts.datacenter, + id: opts.id + }); + + this._request({ + method: 'POST', + path: p, + data: {} + }, function (err, req, res, body) { + if (err) { + cb(err, null, res); + return; + } + cb(null, body, res); + }); +}; + /** * Wait for an image to go one of a set of specfic states. * @@ -872,13 +1195,12 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) { /** * Get a machine by id. * - * XXX add getCredentials equivalent - * XXX cloudapi docs don't doc the credentials=true option - * * For backwards compat, calling with `getMachine(id, cb)` is allowed. * * @param {Object} opts - * - id {UUID} Required. The machine id. + * - {UUID} id - Required. The machine id. + * - {Boolean} credentials - Optional. Set to true to include generated + * credentials for this machine in `machine.metadata.credentials`. * @param {Function} cb of the form `function (err, machine, res)` */ CloudApi.prototype.getMachine = function getMachine(opts, cb) { @@ -887,9 +1209,14 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) { } assert.object(opts, 'opts'); assert.uuid(opts.id, 'opts.id'); + assert.optionalBool(opts.credentials, 'opts.credentials'); - var endpoint = format('/%s/machines/%s', this.account, opts.id); - this._request(endpoint, function (err, req, res, body) { + var query = {}; + if (opts.credentials) { + query.credentials = 'true'; + } + var p = this._path(format('/%s/machines/%s', this.account, opts.id), query); + this._request(p, function (err, req, res, body) { cb(err, body, res); }); }; diff --git a/lib/common.js b/lib/common.js index 613732e..609d0a2 100644 --- a/lib/common.js +++ b/lib/common.js @@ -618,9 +618,9 @@ function promptYesNo(opts_, cb) { stdin.on('data', onData); function postInput() { + stdout.write('\n'); stdin.setRawMode(false); stdin.pause(); - stdin.write('\n'); stdin.removeListener('data', onData); } @@ -1461,6 +1461,19 @@ function parseNicStr(nic) { return obj; } +/* + * Return a short image string that represents the given image object. + * + * @param img {Object} The image object. + * @returns {String} A network object. E.g. + * 'a6cf222d-73f4-414c-a427-5c238ef8e1b7 (jillmin@1.0.0)' + */ +function imageRepr(img) { + assert.object(img); + + return format('%s (%s@%s)', img.id, img.name, img.version); +} + //---- exports @@ -1502,6 +1515,7 @@ module.exports = { readStdin: readStdin, validateObject: validateObject, ipv4ToLong: ipv4ToLong, - parseNicStr: parseNicStr + parseNicStr: parseNicStr, + imageRepr: imageRepr }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/config.js b/lib/config.js index ab049a8..814d2ac 100644 --- a/lib/config.js +++ b/lib/config.js @@ -296,12 +296,11 @@ function _loadEnvProfile(profileOverrides) { for (var attr in profileOverrides) { envProfile[attr] = profileOverrides[attr]; } + /* - * If none of the above envvars are defined, then there is no env profile. + * If missing any of the required vars, then there is no env profile. */ - if (!envProfile.account && !envProfile.user && !envProfile.url && - !envProfile.keyId) - { + if (!envProfile.account || !envProfile.url || !envProfile.keyId) { return null; } validateProfile(envProfile, 'environment variables'); @@ -363,10 +362,11 @@ function loadProfile(opts) { function loadAllProfiles(opts) { assert.string(opts.configDir, 'opts.configDir'); assert.object(opts.log, 'opts.log'); + assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides'); var profiles = []; - var envProfile = _loadEnvProfile(); + var envProfile = _loadEnvProfile(opts.profileOverrides); if (envProfile) { profiles.push(envProfile); } diff --git a/lib/do_image/do_clone.js b/lib/do_image/do_clone.js new file mode 100644 index 0000000..814a570 --- /dev/null +++ b/lib/do_image/do_clone.js @@ -0,0 +1,107 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2018, Joyent, Inc. + * + * `triton image clone ...` + */ + +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +// ---- the command + +function do_clone(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 1) { + cb(new errors.UsageError( + 'incorrect number of args: expected 1, got ' + args.length)); + return; + } + + var log = this.top.log; + var tritonapi = this.top.tritonapi; + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function cloneImage(ctx, next) { + log.trace({dryRun: opts.dry_run, account: ctx.account}, + 'image clone account'); + + if (opts.dry_run) { + next(); + return; + } + + tritonapi.cloneImage({image: args[0]}, function _cloneCb(err, img) { + if (err) { + next(new errors.TritonError(err, 'error cloning image')); + return; + } + + log.trace({img: img}, 'image clone result'); + + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Cloned image %s to %s', + args[0], common.imageRepr(img)); + } + + next(); + }); + } + ]}, cb); +} + +do_clone.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Other options' + }, + { + names: ['dry-run'], + type: 'bool', + help: 'Go through the motions without actually cloning.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_clone.synopses = [ + '{{name}} {{cmd}} [OPTIONS] IMAGE' +]; + +do_clone.help = [ + /* BEGIN JSSTYLED */ + 'Clone a shared image.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "IMAGE" is an image id (a full UUID), an image name (selects the', + 'latest, by "published_at", image with that name), an image "name@version"', + '(selects latest match by "published_at"), or an image short ID (ID prefix).', + '', + 'Note: Only shared images can be cloned.' + /* END JSSTYLED */ +].join('\n'); + +do_clone.completionArgtypes = ['tritonimage', 'none']; + +module.exports = do_clone; diff --git a/lib/do_image/do_copy.js b/lib/do_image/do_copy.js new file mode 100644 index 0000000..88f673b --- /dev/null +++ b/lib/do_image/do_copy.js @@ -0,0 +1,119 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2018, Joyent, Inc. + * + * `triton image copy ...` + */ + +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +// ---- the command + +function do_copy(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 2) { + cb(new errors.UsageError( + 'incorrect number of args: expected 2, got ' + args.length)); + return; + } + + var log = this.top.log; + var tritonapi = this.top.tritonapi; + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function copyImage(ctx, next) { + log.trace({dryRun: opts.dry_run, account: ctx.account, args: args}, + 'image copy'); + + if (opts.dry_run) { + next(); + return; + } + + tritonapi.copyImageToDatacenter( + {image: args[0], datacenter: args[1]}, + function (err, img) { + if (err) { + next(new errors.TritonError(err, 'error copying image')); + return; + } + + log.trace({img: img}, 'image copy result'); + + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Copied image %s to datacenter %s', + common.imageRepr(img), args[1]); + } + + next(); + }); + } + ]}, function (err) { + cb(err); + }); +} + +do_copy.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + group: 'Other options' + }, + { + names: ['dry-run'], + type: 'bool', + help: 'Go through the motions without actually copying.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_copy.synopses = [ + '{{name}} {{cmd}} [OPTIONS] IMAGE DATACENTER' +]; + +do_copy.help = [ + /* BEGIN JSSTYLED */ + 'Copy image to another datacenter.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where "IMAGE" is an image id (a full UUID), an image name (selects the', + 'latest, by "published_at", image with that name), an image "name@version"', + '(selects latest match by "published_at"), or an image short ID (ID prefix).', + 'You must be the owner of the image to copy it. (You can use `triton image', + 'clone` to get your own image clone of an image shared to you.)', + '', + '"DATACENTER" is the name of the datacenter to which to copy your image.', + 'Use `triton datacenters` to show the available datacenter names.' + /* END JSSTYLED */ +].join('\n'); + +do_copy.aliases = ['cp']; + +// TODO: tritonimage should really be 'tritonownedimage' or something to +// limit to images owned by this account +// TODO: tritondatacenter bash completion +do_copy.completionArgtypes = ['tritonimage', 'tritondatacenter', 'none']; + +module.exports = do_copy; diff --git a/lib/do_image/do_list.js b/lib/do_image/do_list.js index 80ff36e..db1b94e 100644 --- a/lib/do_image/do_list.js +++ b/lib/do_image/do_list.js @@ -5,13 +5,15 @@ */ /* - * Copyright 2016 Joyent, Inc. + * Copyright 2018 Joyent, Inc. * * `triton image list ...` */ +var assert = require('assert-plus'); var format = require('util').format; var tabula = require('tabula'); +var vasync = require('vasync'); var common = require('../common'); var errors = require('../errors'); @@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) { listOpts.state = 'all'; } + var self = this; var tritonapi = this.top.tritonapi; - common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { - if (setupErr) { - callback(setupErr); - return; - } - tritonapi.listImages(listOpts, function onRes(err, imgs, res) { - if (err) { - return callback(err); - } + vasync.pipeline({ arg: {}, funcs: [ + function setupTritonApi(_, next) { + common.cliSetupTritonApi({cli: self.top}, next); + }, + function getImages(ctx, next) { + tritonapi.listImages(listOpts, function onRes(err, imgs, res) { + if (err) { + next(err); + return; + } + ctx.imgs = imgs; + next(); + }); + }, + function getUserAccount(ctx, next) { + // If using json output, or when there are no images that use an ACL + // - we don't need to fetch the account, as the account is only used + // to check if the image is shared (i.e. the account is in the image + // ACL) so it can output image flags in non-json mode. + if (opts.json || ctx.imgs.every(function _checkAcl(img) { + return !Array.isArray(img.acl) || img.acl.length === 0; + })) { + next(); + return; + } + tritonapi.cloudapi.getAccount(function _accountCb(err, account) { + if (err) { + next(err); + return; + } + ctx.account = account; + next(); + }); + }, + function formatImages(ctx, next) { + var imgs = ctx.imgs; if (opts.json) { common.jsonStream(imgs); } else { @@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) { if (img.origin) flags.push('I'); if (img['public']) flags.push('P'); if (img.state !== 'active') flags.push('X'); + + // Add image sharing flags. + if (Array.isArray(img.acl) && img.acl.length > 0) { + assert.string(ctx.account.id, 'ctx.account.id'); + if (img.owner === ctx.account.id) { + // This image has been shared with other accounts. + flags.push('+'); + } + if (img.acl.indexOf(ctx.account.id) !== -1) { + // This image has been shared with this account. + flags.push('S'); + } + } + img.flags = flags.length ? flags.join('') : undefined; } @@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) { sort: sort }); } - callback(); - }); - }); + next(); + } + ]}, callback); } do_list.options = [ @@ -156,6 +200,8 @@ do_list.help = [ ' shortid* A short ID prefix.', ' flags* Single letter flags summarizing some fields:', ' "P" image is public', + ' "+" you are sharing this image with others', + ' "S" this image has been shared with you', ' "I" an incremental image (i.e. has an origin)', ' "X" has a state *other* than "active"', ' pubdate* Short form of "published_at" with just the date', diff --git a/lib/do_image/index.js b/lib/do_image/index.js index e7eceb0..2b4fff8 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -33,6 +33,8 @@ function ImageCLI(top) { 'help', 'list', 'get', + 'clone', + 'copy', 'create', 'delete', 'export', @@ -51,6 +53,8 @@ ImageCLI.prototype.init = function init(opts, args, cb) { ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_get = require('./do_get'); +ImageCLI.prototype.do_clone = require('./do_clone'); +ImageCLI.prototype.do_copy = require('./do_copy'); ImageCLI.prototype.do_create = require('./do_create'); ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_export = require('./do_export'); diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index 1d1ea03..888f967 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -289,6 +289,9 @@ function do_create(subcmd, opts, args, cb) { createOpts['tag.'+key] = ctx.tags[key]; }); } + if (opts.allow_shared_images) { + createOpts.allow_shared_images = true; + } for (var i = 0; i < opts._order.length; i++) { var opt = opts._order[i]; @@ -498,6 +501,11 @@ do_create.options = [ 'Joyent-provided images, the user-script is run at every boot ' + 'of the instance. This is a shortcut for `-M user-script=FILE`.' }, + { + names: ['allow-shared-images'], + type: 'bool', + help: 'Allow instance creation to use a shared image.' + }, { group: 'Other options' diff --git a/lib/do_instance/do_get.js b/lib/do_instance/do_get.js index c7f3c4e..ce22914 100644 --- a/lib/do_instance/do_get.js +++ b/lib/do_instance/do_get.js @@ -25,7 +25,10 @@ function do_get(subcmd, opts, args, cb) { cb(setupErr); return; } - tritonapi.getInstance(args[0], function (err, inst) { + tritonapi.getInstance({ + id: args[0], + credentials: opts.credentials + }, function onInst(err, inst) { if (inst) { if (opts.json) { console.log(JSON.stringify(inst)); @@ -44,6 +47,13 @@ do_get.options = [ type: 'bool', help: 'Show this help.' }, + { + names: ['credentials'], + type: 'bool', + help: 'Include generated credentials, in the "metadata.credentials" ' + + 'field, if any. Typically used with "-j", though one can show ' + + 'values with "-o metadata.credentials".' + }, { names: ['json', 'j'], type: 'bool', @@ -58,7 +68,6 @@ do_get.help = [ '{{usage}}', '', '{{options}}', - '', 'Where "INST" is an instance name, id, or short id.', '', 'A *deleted* instance may still respond with the instance object. In that', diff --git a/lib/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..df848ea --- /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 (c) 2018, 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(new Error('account has no default network configured')); + 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 96fc1e4..b6e705e 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_profile/do_list.js b/lib/do_profile/do_list.js index da9a18e..427c8d9 100644 --- a/lib/do_profile/do_list.js +++ b/lib/do_profile/do_list.js @@ -31,7 +31,8 @@ function _listProfiles(cli, opts, args, cb) { try { profiles = mod_config.loadAllProfiles({ configDir: cli.configDir, - log: cli.log + log: cli.log, + profileOverrides: cli._cliOptsAsProfile() }); } catch (e) { return cb(e); 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 7bc46df..6e26c1d 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -133,7 +133,7 @@ var errors = require('./errors'); // ---- globals -var CLOUDAPI_ACCEPT_VERSION = '~8'; +var CLOUDAPI_ACCEPT_VERSION = '~9||~8'; @@ -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 /** @@ -399,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { ? true : !profile.insecure); var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; - var opts = { + self._cloudapiOpts = { url: profile.url, account: profile.actAsAccount || profile.account, principal: { @@ -415,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { if (profile.privKey) { var key = sshpk.parsePrivateKey(profile.privKey); this.keyPair = - opts.principal.keyPair = + self._cloudapiOpts.principal.keyPair = auth.KeyPair.fromPrivateKey(key); - this.cloudapi = cloudapi.createClient(opts); + this.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); } else { var kr = new auth.KeyRing(); @@ -427,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { cb(err); return; } - self.keyPair = opts.principal.keyPair = kp; - self.cloudapi = cloudapi.createClient(opts); + self.keyPair = self._cloudapiOpts.principal.keyPair = kp; + self.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); }); } @@ -864,6 +958,132 @@ TritonApi.prototype.unshareImage = function unshareImage(opts, cb) }); }; +/** + * Clone a shared image. + * + * @param {Object} opts + * - {String} image The image UUID, name, or short ID. Required. + * @param {Function} cb `function (err, img)` + * On failure `err` is an error instance, else it is null. + * On success: `img` is the cloned image object. + */ +TritonApi.prototype.cloneImage = function cloneImage(opts, cb) +{ + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.image, 'opts.image'); + assert.func(cb, 'cb'); + + var arg = { + image: opts.image, + client: self + }; + var img; + + vasync.pipeline({arg: arg, funcs: [ + _stepImg, + function cloudApiCloneImage(ctx, next) { + self.cloudapi.cloneImage({id: ctx.img.id}, + function _cloneImageCb(err, img_) { + img = img_; + next(err); + }); + } + ]}, function (err) { + cb(err, img); + }); +}; + +/** + * Copy an image to another Datacenter. + * + * Note: This somewhat flips the sense of the CloudAPI ImportImageFromDatacenter + * endpoint, in that it instead calls *the target DC* to pull from this + * profile's DC. The target DC's CloudAPI URL is determined from this DC's + * `ListDatacenters` endpoint. It is assumed that all other Triton profile + * attributes (account, keyId) suffice to auth with the target DC. + * + * @param {Object} opts + * - {String} datacenter The datacenter name to copy to. Required. + * - {String} image The image UUID, name, or short ID. Required. + * @param {Function} cb `function (err, img)` + * On failure `err` is an error instance, else it is null. + * On success: `img` is the copied image object. + */ +TritonApi.prototype.copyImageToDatacenter = +function copyImageToDatacenter(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.datacenter, 'opts.datacenter'); + assert.string(opts.image, 'opts.image'); + assert.func(cb, 'cb'); + + var arg = { + client: self, + datacenter: opts.datacenter, + image: opts.image + }; + var img; + + vasync.pipeline({arg: arg, funcs: [ + _stepImg, + function getDatacenters(ctx, next) { + self.cloudapi.listDatacenters({}, function (err, dcs, res) { + if (err) { + next(err); + return; + } + if (!dcs.hasOwnProperty(ctx.datacenter)) { + next(new errors.TritonError(format( + '"%s" is not a valid datacenter name, possible ' + + 'names are: %s', + ctx.datacenter, + Object.keys(dcs).join(', ')))); + return; + } + ctx.datacenterUrl = dcs[ctx.datacenter]; + assert.string(ctx.datacenterUrl, 'ctx.datacenterUrl'); + + // CloudAPI added image copying in 9.2.0, which is also + // the version that included this header. + var currentDcName = res.headers['triton-datacenter-name']; + if (!currentDcName) { + next(new errors.TritonError(err, format( + 'this datacenter does not support image copying (%s)', + res.headers['server']))); + return; + } + // Note: currentDcName is where the image currently resides. + ctx.currentDcName = currentDcName; + + next(); + }); + }, + function cloudApiImportImageFromDatacenter(ctx, next) { + var targetCloudapiOpts = jsprim.mergeObjects( + { + url: ctx.datacenterUrl, + log: self.log.child({datacenter: opts.datacenter}, true) + }, + null, + self._cloudapiOpts + ); + var targetCloudapi = cloudapi.createClient(targetCloudapiOpts); + + targetCloudapi.importImageFromDatacenter({ + datacenter: ctx.currentDcName, + id: ctx.img.id + }, function _importImageCb(err, img_) { + targetCloudapi.close(); + img = img_; + next(err); + }); + } + ]}, function (err) { + cb(err, img); + }); +}; + /** * Get an active package by ID, exact name, or short ID, in that order. * @@ -934,7 +1154,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 +1167,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 +1204,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. * @@ -1123,6 +1409,8 @@ TritonApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) { * - {Array} fields: Optional. An array of instance field names that are * wanted by the caller. This *can* allow the implementation to avoid * extra API calls. E.g. `['id', 'name']`. + * - {Boolean} credentials: Optional. Set to true to include generated + * credentials for this instance in `inst.metadata.credentials`. * @param {Function} cb `function (err, inst, res)` * Note that deleted instances will result in `err` being a * `InstanceDeletedError` and `inst` being defined. On success, `res` is @@ -1137,6 +1425,7 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { assert.object(opts, 'opts'); assert.string(opts.id, 'opts.id'); assert.optionalArrayOfString(opts.fields, 'opts.fields'); + assert.optionalBool(opts.credentials, 'opts.credentials'); assert.func(cb, 'cb'); /* @@ -1176,7 +1465,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { return next(); } } - self.cloudapi.getMachine(uuid, function (err, inst_, res_) { + self.cloudapi.getMachine({ + id: uuid, + credentials: opts.credentials + }, function onMachine(err, inst_, res_) { res = res_; inst = inst_; err = errFromGetMachineErr(err); @@ -1264,7 +1556,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) { } var uuid = instFromList.id; - self.cloudapi.getMachine(uuid, function (err, inst_, res_) { + self.cloudapi.getMachine({ + id: uuid, + credentials: opts.credentials + }, function onMachine(err, inst_, res_) { res = res_; inst = inst_; err = errFromGetMachineErr(err); @@ -2128,14 +2423,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': @@ -2189,8 +2485,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, @@ -2198,7 +2494,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; @@ -2236,7 +2532,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; @@ -2274,7 +2570,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); @@ -2626,6 +2922,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-affinity.test.js b/test/integration/cli-affinity.test.js index f427aa6..d68cca3 100644 --- a/test/integration/cli-affinity.test.js +++ b/test/integration/cli-affinity.test.js @@ -110,7 +110,8 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) { var db2Alias = ALIAS_PREFIX + '-db2'; var db2; tt.test(' triton create -n db2 -a \'instance!=db*\'', function (t) { - var argv = ['create', '-wj', '-n', db2Alias, '-a', 'instance!=db*', + var argv = ['create', '-wj', '-n', db2Alias, '-a', + 'instance!=' + ALIAS_PREFIX + '-db*', imgId, pkgId]; h.safeTriton(t, argv, function (err, stdout) { var lines = h.jsonStreamParse(stdout); 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 7d7a658..08791c6 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,