merge with upstream

This commit is contained in:
Marius Pana 2018-07-24 13:22:09 +03:00
commit 75ec73a31c
36 changed files with 3163 additions and 120 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
/triton-*.tgz /triton-*.tgz
.DS_Store .DS_Store
.git .git
*.swp

View File

@ -6,10 +6,43 @@ Known issues:
## not yet released ## 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 - [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 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 decrypting) the account key itself. This makes using Docker simpler with keys
in an SSH Agent. 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 ## 6.0.0

View File

@ -206,6 +206,7 @@ function CLI() {
'package', 'package',
'network', 'network',
'fwrule', 'fwrule',
'vlan',
{ group: 'Other Commands' }, { group: 'Other Commands' },
'info', 'info',
'account', 'account',
@ -700,6 +701,9 @@ CLI.prototype.do_package = require('./do_package');
CLI.prototype.do_networks = require('./do_networks'); CLI.prototype.do_networks = require('./do_networks');
CLI.prototype.do_network = require('./do_network'); CLI.prototype.do_network = require('./do_network');
// VLANs
CLI.prototype.do_vlan = require('./do_vlan');
// Hidden commands // Hidden commands
CLI.prototype.do_cloudapi = require('./do_cloudapi'); CLI.prototype.do_cloudapi = require('./do_cloudapi');
CLI.prototype.do_badger = require('./do_badger'); CLI.prototype.do_badger = require('./do_badger');

View File

@ -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 // ---- networks
/** /**
@ -458,6 +500,228 @@ CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = {
reserved: 'boolean' 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);
});
};
// <updatable account field> -> <expected typeof>
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 // ---- datacenters
/** /**
@ -775,7 +1039,7 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
* - {Object} fields Required. The fields to update in the image. * - {Object} fields Required. The fields to update in the image.
* @param {Function} cb of the form `function (err, body, res)` * @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.uuid(opts.id, 'id');
assert.object(opts.fields, 'fields'); assert.object(opts.fields, 'fields');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
@ -793,6 +1057,65 @@ CloudApi.prototype.updateImage = function shareImage(opts, cb) {
}); });
}; };
/**
* Clone an image.
* <http://apidocs.joyent.com/cloudapi/#CloneImage>
*
* @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.
* <http://apidocs.joyent.com/cloudapi/#ImportImageFromDatacenter>
*
* @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. * 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. * 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. * For backwards compat, calling with `getMachine(id, cb)` is allowed.
* *
* @param {Object} opts * @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)` * @param {Function} cb of the form `function (err, machine, res)`
*/ */
CloudApi.prototype.getMachine = function getMachine(opts, cb) { CloudApi.prototype.getMachine = function getMachine(opts, cb) {
@ -887,9 +1209,14 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) {
} }
assert.object(opts, 'opts'); assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
assert.optionalBool(opts.credentials, 'opts.credentials');
var endpoint = format('/%s/machines/%s', this.account, opts.id); var query = {};
this._request(endpoint, function (err, req, res, body) { 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); cb(err, body, res);
}); });
}; };

View File

@ -618,9 +618,9 @@ function promptYesNo(opts_, cb) {
stdin.on('data', onData); stdin.on('data', onData);
function postInput() { function postInput() {
stdout.write('\n');
stdin.setRawMode(false); stdin.setRawMode(false);
stdin.pause(); stdin.pause();
stdin.write('\n');
stdin.removeListener('data', onData); stdin.removeListener('data', onData);
} }
@ -1461,6 +1461,19 @@ function parseNicStr(nic) {
return obj; 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 //---- exports
@ -1502,6 +1515,7 @@ module.exports = {
readStdin: readStdin, readStdin: readStdin,
validateObject: validateObject, validateObject: validateObject,
ipv4ToLong: ipv4ToLong, ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr parseNicStr: parseNicStr,
imageRepr: imageRepr
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

View File

@ -296,12 +296,11 @@ function _loadEnvProfile(profileOverrides) {
for (var attr in profileOverrides) { for (var attr in profileOverrides) {
envProfile[attr] = profileOverrides[attr]; 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 && if (!envProfile.account || !envProfile.url || !envProfile.keyId) {
!envProfile.keyId)
{
return null; return null;
} }
validateProfile(envProfile, 'environment variables'); validateProfile(envProfile, 'environment variables');
@ -363,10 +362,11 @@ function loadProfile(opts) {
function loadAllProfiles(opts) { function loadAllProfiles(opts) {
assert.string(opts.configDir, 'opts.configDir'); assert.string(opts.configDir, 'opts.configDir');
assert.object(opts.log, 'opts.log'); assert.object(opts.log, 'opts.log');
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
var profiles = []; var profiles = [];
var envProfile = _loadEnvProfile(); var envProfile = _loadEnvProfile(opts.profileOverrides);
if (envProfile) { if (envProfile) {
profiles.push(envProfile); profiles.push(envProfile);
} }

107
lib/do_image/do_clone.js Normal file
View File

@ -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;

119
lib/do_image/do_copy.js Normal file
View File

@ -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;

View File

@ -5,13 +5,15 @@
*/ */
/* /*
* Copyright 2016 Joyent, Inc. * Copyright 2018 Joyent, Inc.
* *
* `triton image list ...` * `triton image list ...`
*/ */
var assert = require('assert-plus');
var format = require('util').format; var format = require('util').format;
var tabula = require('tabula'); var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all'; listOpts.state = 'all';
} }
var self = this;
var tritonapi = this.top.tritonapi; var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) { vasync.pipeline({ arg: {}, funcs: [
callback(setupErr); function setupTritonApi(_, next) {
return; common.cliSetupTritonApi({cli: self.top}, next);
} },
function getImages(ctx, next) {
tritonapi.listImages(listOpts, function onRes(err, imgs, res) { tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) { if (err) {
return callback(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) { if (opts.json) {
common.jsonStream(imgs); common.jsonStream(imgs);
} else { } else {
@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) {
if (img.origin) flags.push('I'); if (img.origin) flags.push('I');
if (img['public']) flags.push('P'); if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X'); 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; img.flags = flags.length ? flags.join('') : undefined;
} }
@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) {
sort: sort sort: sort
}); });
} }
callback(); next();
}); }
}); ]}, callback);
} }
do_list.options = [ do_list.options = [
@ -156,6 +200,8 @@ do_list.help = [
' shortid* A short ID prefix.', ' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:', ' flags* Single letter flags summarizing some fields:',
' "P" image is public', ' "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)', ' "I" an incremental image (i.e. has an origin)',
' "X" has a state *other* than "active"', ' "X" has a state *other* than "active"',
' pubdate* Short form of "published_at" with just the date', ' pubdate* Short form of "published_at" with just the date',

View File

@ -33,6 +33,8 @@ function ImageCLI(top) {
'help', 'help',
'list', 'list',
'get', 'get',
'clone',
'copy',
'create', 'create',
'delete', 'delete',
'export', 'export',
@ -51,6 +53,8 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_list = require('./do_list');
ImageCLI.prototype.do_get = require('./do_get'); 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_create = require('./do_create');
ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_delete = require('./do_delete');
ImageCLI.prototype.do_export = require('./do_export'); ImageCLI.prototype.do_export = require('./do_export');

View File

@ -289,6 +289,9 @@ function do_create(subcmd, opts, args, cb) {
createOpts['tag.'+key] = ctx.tags[key]; 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++) { for (var i = 0; i < opts._order.length; i++) {
var opt = opts._order[i]; var opt = opts._order[i];
@ -498,6 +501,11 @@ do_create.options = [
'Joyent-provided images, the user-script is run at every boot ' + 'Joyent-provided images, the user-script is run at every boot ' +
'of the instance. This is a shortcut for `-M user-script=FILE`.' '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' group: 'Other options'

View File

@ -25,7 +25,10 @@ function do_get(subcmd, opts, args, cb) {
cb(setupErr); cb(setupErr);
return; return;
} }
tritonapi.getInstance(args[0], function (err, inst) { tritonapi.getInstance({
id: args[0],
credentials: opts.credentials
}, function onInst(err, inst) {
if (inst) { if (inst) {
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(inst)); console.log(JSON.stringify(inst));
@ -44,6 +47,13 @@ do_get.options = [
type: 'bool', type: 'bool',
help: 'Show this help.' 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'], names: ['json', 'j'],
type: 'bool', type: 'bool',
@ -58,7 +68,6 @@ do_get.help = [
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'',
'Where "INST" is an instance name, id, or short id.', 'Where "INST" is an instance name, id, or short id.',
'', '',
'A *deleted* instance may still respond with the instance object. In that', 'A *deleted* instance may still respond with the instance object. In that',

187
lib/do_network/do_create.js Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -66,14 +66,23 @@ function do_list(subcmd, opts, args, callback) {
common.cliSetupTritonApi, common.cliSetupTritonApi,
function searchNetworks(arg, next) { 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) { if (err) {
next(err); next(err);
return; return;
} }
arg.networks = networks; arg.networks = networks;
next(); next();
}); }
}, },
function filterNetworks(arg, next) { function filterNetworks(arg, next) {

View File

@ -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;

View File

@ -33,7 +33,11 @@ function NetworkCLI(top) {
'help', 'help',
'list', 'list',
'get', '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_list = require('./do_list');
NetworkCLI.prototype.do_get = require('./do_get'); NetworkCLI.prototype.do_get = require('./do_get');
NetworkCLI.prototype.do_ip = require('./do_ip'); 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; module.exports = NetworkCLI;

View File

@ -31,7 +31,8 @@ function _listProfiles(cli, opts, args, cb) {
try { try {
profiles = mod_config.loadAllProfiles({ profiles = mod_config.loadAllProfiles({
configDir: cli.configDir, configDir: cli.configDir,
log: cli.log log: cli.log,
profileOverrides: cli._cliOptsAsProfile()
}); });
} catch (e) { } catch (e) {
return cb(e); return cb(e);

123
lib/do_vlan/do_create.js Normal file
View File

@ -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;

85
lib/do_vlan/do_delete.js Normal file
View File

@ -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;

88
lib/do_vlan/do_get.js Normal file
View File

@ -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;

123
lib/do_vlan/do_list.js Normal file
View File

@ -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=<integer> Number filter. Supported fields: vlan_id',
' FIELD=<string> 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;

View File

@ -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;

201
lib/do_vlan/do_update.js Normal file
View File

@ -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;

55
lib/do_vlan/index.js Normal file
View File

@ -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;

View File

@ -133,7 +133,7 @@ var errors = require('./errors');
// ---- globals // ---- 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`). * and determines the network id (setting it as `arg.netId`).
*/ */
function _stepNetId(arg, next) { function _stepNetId(arg, next) {
assert.object(arg, 'arg');
assert.object(arg.client, 'arg.client'); assert.object(arg.client, 'arg.client');
assert.func(next, 'next');
var id = arg.network || arg.id; var id = arg.network || arg.id;
assert.string(id, 'arg.network || arg.id'); assert.string(id, 'arg.network || arg.id');
@ -291,7 +293,7 @@ function _stepNetId(arg, next) {
arg.netId = id; arg.netId = id;
next(); next();
} else { } else {
arg.client.getNetwork(id, function (err, net) { arg.client.getNetwork(id, function onGet(err, net) {
if (err) { if (err) {
next(err); next(err);
} else { } 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 //---- TritonApi class
/** /**
@ -399,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
? true : !profile.insecure); ? true : !profile.insecure);
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
var opts = { self._cloudapiOpts = {
url: profile.url, url: profile.url,
account: profile.actAsAccount || profile.account, account: profile.actAsAccount || profile.account,
principal: { principal: {
@ -415,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
if (profile.privKey) { if (profile.privKey) {
var key = sshpk.parsePrivateKey(profile.privKey); var key = sshpk.parsePrivateKey(profile.privKey);
this.keyPair = this.keyPair =
opts.principal.keyPair = self._cloudapiOpts.principal.keyPair =
auth.KeyPair.fromPrivateKey(key); auth.KeyPair.fromPrivateKey(key);
this.cloudapi = cloudapi.createClient(opts); this.cloudapi = cloudapi.createClient(self._cloudapiOpts);
cb(null); cb(null);
} else { } else {
var kr = new auth.KeyRing(); var kr = new auth.KeyRing();
@ -427,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
cb(err); cb(err);
return; return;
} }
self.keyPair = opts.principal.keyPair = kp; self.keyPair = self._cloudapiOpts.principal.keyPair = kp;
self.cloudapi = cloudapi.createClient(opts); self.cloudapi = cloudapi.createClient(self._cloudapiOpts);
cb(null); 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. * 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'); assert.func(cb, 'cb');
if (common.isUUID(name)) { if (common.isUUID(name)) {
this.cloudapi.getNetwork(name, function (err, net) { this.cloudapi.getNetwork(name, function onGet(err, net) {
if (err) { if (err) {
if (err.restCode === 'ResourceNotFound') { if (err.restCode === 'ResourceNotFound') {
// Wrap with *our* ResourceNotFound for exitStatus=3. // Wrap with *our* ResourceNotFound for exitStatus=3.
@ -947,7 +1167,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
} }
}); });
} else { } else {
this.cloudapi.listNetworks(function (err, nets) { this.cloudapi.listNetworks({}, function onList(err, nets) {
if (err) { if (err) {
return cb(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. * 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 * - {Array} fields: Optional. An array of instance field names that are
* wanted by the caller. This *can* allow the implementation to avoid * wanted by the caller. This *can* allow the implementation to avoid
* extra API calls. E.g. `['id', 'name']`. * 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)` * @param {Function} cb `function (err, inst, res)`
* Note that deleted instances will result in `err` being a * Note that deleted instances will result in `err` being a
* `InstanceDeletedError` and `inst` being defined. On success, `res` is * `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.object(opts, 'opts');
assert.string(opts.id, 'opts.id'); assert.string(opts.id, 'opts.id');
assert.optionalArrayOfString(opts.fields, 'opts.fields'); assert.optionalArrayOfString(opts.fields, 'opts.fields');
assert.optionalBool(opts.credentials, 'opts.credentials');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
/* /*
@ -1176,7 +1465,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
return next(); 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_; res = res_;
inst = inst_; inst = inst_;
err = errFromGetMachineErr(err); err = errFromGetMachineErr(err);
@ -1264,7 +1556,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
} }
var uuid = instFromList.id; 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_; res = res_;
inst = inst_; inst = inst_;
err = errFromGetMachineErr(err); err = errFromGetMachineErr(err);
@ -2128,14 +2423,15 @@ function deleteAllInstanceTags(opts, cb) {
*/ */
TritonApi.prototype.addNic = TritonApi.prototype.addNic =
function addNic(opts, cb) { function addNic(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id'); assert.string(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network'); assert.ok(opts.network, 'opts.network');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var self = this; var self = this;
var nic;
var pipeline = []; var pipeline = [];
var res; var res;
var nic;
switch (typeof (opts.network)) { switch (typeof (opts.network)) {
case 'string': case 'string':
@ -2189,8 +2485,8 @@ function listNics(opts, cb) {
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var self = this; var self = this;
var res;
var nics; var nics;
var res;
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId, _stepInstId,
@ -2198,7 +2494,7 @@ function listNics(opts, cb) {
function list(arg, next) { function list(arg, next) {
self.cloudapi.listNics({ self.cloudapi.listNics({
id: arg.instId id: arg.instId
}, function (err, _nics, _res) { }, function onList(err, _nics, _res) {
res = _res; res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it res.instId = arg.instId; // gross hack, in case caller needs it
nics = _nics; nics = _nics;
@ -2236,7 +2532,7 @@ function getNic(opts, cb) {
self.cloudapi.getNic({ self.cloudapi.getNic({
id: arg.instId, id: arg.instId,
mac: arg.mac mac: arg.mac
}, function (err, _nic, _res) { }, function onGet(err, _nic, _res) {
res = _res; res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it res.instId = arg.instId; // gross hack, in case caller needs it
nic = _nic; nic = _nic;
@ -2274,7 +2570,7 @@ function removeNic(opts, cb) {
self.cloudapi.removeNic({ self.cloudapi.removeNic({
id: arg.instId, id: arg.instId,
mac: arg.mac mac: arg.mac
}, function (err, _res) { }, function onRemove(err, _res) {
res = _res; res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it res.instId = arg.instId; // gross hack, in case caller needs it
next(err); 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 // ---- RBAC
/** /**

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2016 Joyent, Inc. * Copyright 2018 Joyent, Inc.
*/ */
/* /*
@ -18,6 +18,8 @@ var test = require('tape');
// --- Globals // --- Globals
var NET_NAME = 'node-triton-testnet967';
var CLIENT; var CLIENT;
var NET; 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) { tt.test(' setup: net', function (t) {
var opts = { CLIENT.cloudapi.listNetworks({}, function (err, nets) {
account: CLIENT.profile.account
};
CLIENT.cloudapi.listNetworks(opts, function (err, nets) {
if (h.ifErr(t, err)) if (h.ifErr(t, err))
return t.end(); 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) { tt.test(' teardown: client', function (t) {
CLIENT.close(); CLIENT.close();
t.end(); t.end();

View File

@ -25,9 +25,9 @@ var NIC;
// --- Tests // --- Tests
test('TritonApi networks', function (tt) { test('TritonApi nics', function (tt) {
tt.test(' setup', function (t) { tt.test(' setup', function (t) {
h.createClient(function (err, client_) { h.createClient(function onCreate(err, client_) {
t.error(err); t.error(err);
CLIENT = client_; CLIENT = client_;
t.end(); t.end();
@ -36,9 +36,11 @@ test('TritonApi networks', function (tt) {
tt.test(' setup: inst', function (t) { tt.test(' setup: inst', function (t) {
CLIENT.cloudapi.listMachines({}, function (err, vms) { CLIENT.cloudapi.listMachines({}, function onList(err, vms) {
if (vms.length === 0) if (vms.length === 0) {
return t.end(); t.end();
return;
}
t.ok(Array.isArray(vms), 'vms array'); t.ok(Array.isArray(vms), 'vms array');
INST = vms[0]; INST = vms[0];
@ -49,13 +51,17 @@ test('TritonApi networks', function (tt) {
tt.test(' TritonApi listNics', function (t) { tt.test(' TritonApi listNics', function (t) {
if (!INST) if (!INST) {
return t.end(); t.end();
return;
}
function check(val, valName, next) { function check(val, valName, next) {
CLIENT.listNics({id: val}, function (err, nics) { CLIENT.listNics({id: val}, function onList(err, nics) {
if (h.ifErr(t, err, 'no err ' + valName)) if (h.ifErr(t, err, 'no err ' + valName)) {
return t.end(); t.end();
return;
}
t.ok(Array.isArray(nics), 'nics array'); t.ok(Array.isArray(nics), 'nics array');
NIC = nics[0]; NIC = nics[0];
@ -66,9 +72,9 @@ test('TritonApi networks', function (tt) {
var shortId = INST.id.split('-')[0]; var shortId = INST.id.split('-')[0];
check(INST.id, 'id', function () { check(INST.id, 'id', function doId() {
check(INST.name, 'name', function () { check(INST.name, 'name', function doName() {
check(shortId, 'shortId', function () { check(shortId, 'shortId', function doShort() {
t.end(); t.end();
}); });
}); });
@ -77,13 +83,17 @@ test('TritonApi networks', function (tt) {
tt.test(' TritonApi getNic', function (t) { tt.test(' TritonApi getNic', function (t) {
if (!NIC) if (!NIC) {
return t.end(); t.end();
return;
}
function check(inst, mac, instValName, next) { function check(inst, mac, instValName, next) {
CLIENT.getNic({id: inst, mac: mac}, function (err, nic) { CLIENT.getNic({id: inst, mac: mac}, function onGet(err, nic) {
if (h.ifErr(t, err, 'no err for ' + instValName)) if (h.ifErr(t, err, 'no err for ' + instValName)) {
return t.end(); t.end();
return;
}
t.deepEqual(nic, NIC, instValName); t.deepEqual(nic, NIC, instValName);
@ -93,9 +103,9 @@ test('TritonApi networks', function (tt) {
var shortId = INST.id.split('-')[0]; var shortId = INST.id.split('-')[0];
check(INST.id, NIC.mac, 'id', function () { check(INST.id, NIC.mac, 'id', function doId() {
check(INST.name, NIC.mac, 'name', function () { check(INST.name, NIC.mac, 'name', function doName() {
check(shortId, NIC.mac, 'shortId', function () { check(shortId, NIC.mac, 'shortId', function doShort() {
t.end(); t.end();
}); });
}); });

View File

@ -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();
});
});

View File

@ -110,7 +110,8 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
var db2Alias = ALIAS_PREFIX + '-db2'; var db2Alias = ALIAS_PREFIX + '-db2';
var db2; var db2;
tt.test(' triton create -n db2 -a \'instance!=db*\'', function (t) { 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]; imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) { h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout); var lines = h.jsonStreamParse(stdout);

View File

@ -5,28 +5,53 @@
*/ */
/* /*
* Copyright 2017 Joyent, Inc. * Copyright 2018 Joyent, Inc.
*/ */
/* /*
* Integration tests for `triton network(s)` * Integration tests for `triton network(s)`
*/ */
var h = require('./helpers'); var f = require('util').format;
var os = require('os');
var test = require('tape'); var test = require('tape');
var h = require('./helpers');
var common = require('../../lib/common'); var common = require('../../lib/common');
// --- Globals // --- Globals
var NET_NAME = f('nodetritontest-network-%s', os.hostname());
var networks; var networks;
var vlan;
var OPTS = {
skip: !h.CONFIG.allowWriteActions
};
// --- Tests // --- 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) { 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) { tt.test(' triton network list -h', function (t) {
h.triton('networks -h', function (err, stdout, stderr) { h.triton('networks -h', function (err, stdout, stderr) {
if (h.ifErr(t, err)) 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);
});
});
});

View File

@ -41,7 +41,7 @@ test('triton instance nics', OPTS, function (tt) {
h.printConfig(tt); h.printConfig(tt);
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) { 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.ifErr(err);
t.end(); t.end();
}); });
@ -49,8 +49,10 @@ test('triton instance nics', OPTS, function (tt) {
tt.test(' setup: triton instance create', function (t) { tt.test(' setup: triton instance create', function (t) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err, instId) { h.createTestInst(t, INST_ALIAS, {}, function onInst(err, instId) {
if (h.ifErr(t, err, 'triton instance create')) if (h.ifErr(t, err, 'triton instance create')) {
return t.end(); t.end();
return;
}
t.ok(instId, 'created instance ' + instId); t.ok(instId, 'created instance ' + instId);
INST = instId; INST = instId;
@ -61,8 +63,10 @@ test('triton instance nics', OPTS, function (tt) {
tt.test(' setup: find network for tests', function (t) { tt.test(' setup: find network for tests', function (t) {
h.triton('network list -j', function onNetworks(err, stdout) { h.triton('network list -j', function onNetworks(err, stdout) {
if (h.ifErr(t, err, 'triton network list')) if (h.ifErr(t, err, 'triton network list')) {
return t.end(); t.end();
return;
}
NETWORK = JSON.parse(stdout.trim().split('\n')[0]); NETWORK = JSON.parse(stdout.trim().split('\n')[0]);
t.ok(NETWORK, 'NETWORK'); t.ok(NETWORK, 'NETWORK');
@ -74,9 +78,11 @@ test('triton instance nics', OPTS, function (tt) {
tt.test(' triton instance nic create', function (t) { tt.test(' triton instance nic create', function (t) {
var cmd = 'instance nic create -j -w ' + INST + ' ' + NETWORK.id; var cmd = 'instance nic create -j -w ' + INST + ' ' + NETWORK.id;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create')) if (h.ifErr(t, err, 'triton instance nic create')) {
return t.end(); t.end();
return;
}
NIC = JSON.parse(stdout); NIC = JSON.parse(stdout);
t.ok(NIC, 'created NIC: ' + stdout.trim()); 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) { tt.test(' triton instance nic get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC.mac; var cmd = 'instance nic get ' + INST + ' ' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get')) if (h.ifErr(t, err, 'triton instance nic get')) {
return t.end(); t.end();
return;
}
var obj = JSON.parse(stdout); var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC.mac, 'nic MAC is correct'); 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) { tt.test(' triton instance nic list', function (t) {
var cmd = 'instance nic list ' + INST; var cmd = 'instance nic list ' + INST;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list')) if (h.ifErr(t, err, 'triton instance nic list')) {
return t.end(); t.end();
return;
}
var nics = stdout.trim().split('\n'); var nics = stdout.trim().split('\n');
t.ok(nics[0].match(/IP\s+MAC\s+STATE\s+NETWORK/), 'nic list' + 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'); 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); 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) { tt.test(' triton instance nic list -j', function (t) {
var cmd = 'instance nic list -j ' + INST; var cmd = 'instance nic list -j ' + INST;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list')) if (h.ifErr(t, err, 'triton instance nic list')) {
return t.end(); 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); return JSON.parse(line);
}); });
t.ok(nics.length >= 1, 'triton nic list expected nic num'); 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; 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) { tt.test(' triton instance nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac; var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err)) if (h.ifErr(t, err)) {
return t.end(); 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); return JSON.parse(str);
}); });
@ -169,11 +183,13 @@ test('triton instance nics', OPTS, function (tt) {
tt.test(' triton nic list mac=<...>', function (t) { tt.test(' triton nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac; var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err)) if (h.ifErr(t, err)) {
return t.end(); 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); return JSON.parse(str);
}); });
@ -188,9 +204,11 @@ test('triton instance nics', OPTS, function (tt) {
tt.test(' triton instance nic delete', function (t) { tt.test(' triton instance nic delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC.mac; var cmd = 'instance nic delete --force ' + INST + ' ' + NIC.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic delete')) if (h.ifErr(t, err, 'triton instance nic delete')) {
return t.end(); t.end();
return;
}
t.ok(stdout.match('Deleted NIC ' + NIC.mac, 'deleted nic')); 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=' + var cmd = 'instance nic create -j -w ' + INST + ' ipv4_uuid=' +
NETWORK.id; NETWORK.id;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create')) if (h.ifErr(t, err, 'triton instance nic create')) {
return t.end(); t.end();
return;
}
NIC2 = JSON.parse(stdout); 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) { tt.test(' triton instance nic with ip get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC2.mac; var cmd = 'instance nic get ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get')) if (h.ifErr(t, err, 'triton instance nic get')) {
return t.end(); t.end();
return;
}
var obj = JSON.parse(stdout); var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC2.mac, 'nic MAC is correct'); 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) { tt.test(' triton instance nic with ip delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC2.mac; var cmd = 'instance nic delete --force ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function (err, stdout, stderr) { h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic with ip delete')) if (h.ifErr(t, err, 'triton instance nic with ip delete')) {
return t.end(); t.end();
return;
}
t.ok(stdout.match('Deleted NIC ' + NIC2.mac, 'deleted nic')); t.ok(stdout.match('Deleted NIC ' + NIC2.mac, 'deleted nic'));

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright (c) 2018, Joyent, Inc.
*/ */
/* /*
@ -65,8 +65,18 @@ var subs = [
['ip'], ['ip'],
['ssh'], ['ssh'],
['network'], ['network'],
['network list', 'networks'], ['network create'],
['network list', 'network ls', 'networks'],
['network get'], ['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'],
['key add'], ['key add'],
['key list', 'key ls', 'keys'], ['key list', 'key ls', 'keys'],

View File

@ -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);
});
});
});

View File

@ -183,6 +183,7 @@ function getTestImg(t, cb) {
var candidateImageNames = { var candidateImageNames = {
'base-64-lts': true, 'base-64-lts': true,
'base-64': true, 'base-64': true,
'minimal-64-lts': true,
'minimal-64': true, 'minimal-64': true,
'base-32-lts': true, 'base-32-lts': true,
'base-32': true, 'base-32': true,