Compare commits

...

36 Commits

Author SHA1 Message Date
Marius Pana caa6da7821 merge with upstream 2019-03-12 21:55:51 +02:00
Alyssa Ross 5212530b37 joyent/node-triton#254 Fix Markdown syntax in README 2019-03-11 17:38:02 -07:00
Trent Mick cc07717008 joyent/node-triton#258 triton instance create picks disabled image
Reviewed by: Brian Bennett <brian.bennett@joyent.com>
Approved by: Brian Bennett <brian.bennett@joyent.com>
2019-03-06 17:25:00 -08:00
Joshua M. Clulow 4921fd2e36 joyent/node-triton#259 "triton ssh" could support proxying through a bastion host
Reviewed by: Tim Foster <tim.foster@joyent.com>
Approved by: Brian Bennett <brian.bennett@joyent.com>
2018-12-18 00:40:41 +00:00
Joshua M. Clulow 05f1bae869 joyent/node-triton#255 creating fabric networks still broken
joyent/node-triton#257 "triton vlan create" should not assert on missing argument
Reviewed by: Dave Eddy <dave.eddy@joyent.com>
Reviewed by: Robert Mustacchi <rm@joyent.com>
Approved by: Cody Mello <cody.mello@joyent.com>
2018-10-05 21:45:52 +00:00
Marius Pana 66114eb940 npm version bump 2018-08-09 09:15:25 +03:00
Marius Pana 161f879723 changed default api endpoint when creating profile 2018-08-09 09:14:33 +03:00
Marius Pana 2d92219fef fixed triton/spearhead in eval output 2018-07-26 17:39:00 +03:00
Marius Pana e980829ca9 sync with upstream 2018-07-24 13:22:28 +03:00
Marius Pana 75ec73a31c merge with upstream 2018-07-24 13:22:09 +03:00
Todd Whiteman aea9b2b7b3 joyent/node-triton#249 Error when creating or deleting profiles when using node v10
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-07-19 17:17:50 -07:00
Joshua M. Clulow aa58982e2a TRITON-598 "triton network get-default" should print error when no default network is set
Reviewed by: Robert Mustacchi <rm@joyent.com>
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-07-17 16:51:29 +00:00
Marius Pana 4684efe22a changed triton to spearhead in help out for env setup 2018-07-07 13:35:39 +03:00
Todd Whiteman c86804cfe4 TRITON-52 x-DC image copy
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-06-29 16:44:17 -07:00
Todd Whiteman dc5dc12052 TRITON-53 x-account image clone
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-06-27 17:28:02 -07:00
Trent Mick 264f69dc54 joyent/node-triton#250 triton profile list doesn't seem to work without full env
Reviewed by: Robert Mustacchi <rm@joyent.com>
Reviewed by: Pedro Palazón Candel <pedro@joyent.com>
Approved by: Marsell Kukuljevic <marsell@joyent.com>
Approved by: Pedro Palazón Candel <pedro@joyent.com>
2018-06-26 16:57:15 -07:00
Marsell Kukuljevic 3584c82e05 TRITON-324 node-triton cli-affinity.test.js failures: create timeout, .end() called twice
Reviewed by: Pedro P. Candel <pedro@joyent.com>
Approved by: Pedro P. Candel <pedro@joyent.com>
2018-06-26 11:36:28 +00:00
Marsell Kukuljevic 0bc11c1e33 TRITON-401 Add support for fabric vlans and networks to node-triton
Reviewed by: Trent Mick <trent.mick@joyent.com>
Reviewed by: Julien Gilli <julien.gilli@joyent.com>
Reviewed by: Pedro P. Candel <pedro@joyent.com>
Approved by: Pedro P. Candel <pedro@joyent.com>
2018-06-25 17:42:33 +02:00
Trent Mick f100c4dbb5 TRITON-524 'triton inst get' should support '--credentials'
Reviewed by: Marsell Kukuljevic <marsell@joyent.com>
Approved by: Marsell Kukuljevic <marsell@joyent.com>
2018-06-19 14:42:10 -07:00
Marius Pana ef91454769 Revert "6.1.0"
This reverts commit 180560dc1e.
2018-06-06 13:50:35 +03:00
Marius Pana 180560dc1e 6.1.0 2018-06-06 13:49:41 +03:00
Marius Pana 5438723d06 merge with latest from upstream 2018-06-06 13:21:15 +03:00
Alex Wilson 5734123e75 joyent/node-triton#245 `triton profile` should generate separate keys for Docker
Reviewed by: Trent Mick <trent.mick@joyent.com>
Reviewed by: Marsell Kukuljevic <marsell@joyent.com>
2018-05-15 15:06:22 -07:00
Trent Mick 6015cf2145 TRITON-323 Markdown syntax tweak in node-triton/CHANGES.md 2018-04-13 14:12:57 -07:00
Marsell Kukuljevic 6417595ba6 TRITON-167 Anti-affinity rules fail when no instances match the name
TRITON-168 Regex anti-affinity rules fail unexpectedly
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-04-11 12:56:38 +12:00
Trent Mick d3d3216a38 TRITON-304 node-triton test crash in 'triton inst nic ...' tests (extra output in tests to help diagnose failure) 2018-04-10 16:30:43 -07:00
Trent Mick 91b4c23a52 TRITON-316 cut node-triton 5.10.0 release
Reviewed by: Cody Peter Mello <cody.mello@joyent.com>
Approved by: Cody Peter Mello <cody.mello@joyent.com>
2018-04-10 16:23:05 -07:00
Mike Zeller 96a5be8ce7 TRITON-301 TRITON-58 created a circular import dep between lib/cloudapi2.js and lib/common.js
Reviewed by: Trent Mick <trentm@gmail.com>
Reviewed by: Dave Eddy <dave.eddy@joyent.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-04-03 14:55:51 -07:00
Dave Eddy 06812c9cd4 TRITON-42 node-triton should support nics when creating an instance
Reviewed by: Michael Zeller <mike.zeller@joyent.com>
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-03-19 18:41:32 -04:00
Marsell Kukuljevic 8e6cf27121 TRITON-19 Triton equivalent to AWS' termination protection
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-03-14 01:51:43 +00:00
Dave Eddy 002171ea06 TRITON-33 node-triton use common functions to cut down on code duplication
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-03-06 15:15:06 -05:00
Mike Zeller bf64899685 TRITON-58 node-triton should support nic operations
Reviewed by: Marsell Kukuljevic <marsell@joyent.com>
Approved by: Marsell Kukuljevic <marsell@joyent.com>
2018-03-05 14:04:47 -08:00
Josh Wilsdon 39635cd0a2 TRITON-190 remove node-triton support for passing --brand flag 2018-02-27 16:45:23 -08:00
Marius Pana be74f307e0 Merge branch 'master' of https://github.com/joyent/node-triton 2018-02-23 15:28:53 +02:00
Josh Wilsdon 26b97b5bed TRITON-124 add node-triton support for bhyve
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-02-19 17:28:42 -08:00
Todd Whiteman 3f243f8c8f TRITON-116 node-triton image sharing
Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
2018-02-14 11:52:29 -08:00
70 changed files with 6428 additions and 446 deletions

1
.gitignore vendored
View File

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

View File

@ -6,6 +6,139 @@ Known issues:
## not yet released
(nothing)
## 7.0.0
- [Backward incompatible.] `triton image get NAME|SHORTID` will now *exclude*
inactive images by default. Before this change inactive images (e.g. those
with a state of "creating" or "unactivated" or "disabled") would be
included. Use the new `-a,--all` option to include inactive images. This
matches the behavior of `triton image list [-a,--all] ...`.
- [joyent/node-triton#258] `triton instance create IMAGE ...` will now exclude
inactive images when looking for an image with the given name.
## 6.3.0
- [joyent/node-triton#259] Added basic support for use of SSH bastion hosts
to access zones on private fabrics. If the `tritoncli.ssh.proxy` tag is set
on an instance, `triton ssh` will look up the name or UUID of the proxy
instance and use `ssh -o ProxyJump` to tunnel the connection to the target.
If the `tritoncli.ssh.ip` tag is set on an instance, `triton ssh` will use
that IP address instead of the `primaryIp` when making its connection.
## 6.2.0
- [joyent/node-triton#255, joyent/node-triton#257] Improved the interface
and documentation of `triton network create` and `triton vlan create`. In
particular, it is now possible to specify static routes and DNS resolvers.
## 6.1.2
- [joyent/node-triton#249] Error when creating or deleting profiles when
using node v10.
## 6.1.1
- [TRITON-598] Fix error handling for `triton network get-default` when
no default network is set on the account.
## 6.1.0
- [joyent/node-triton#250] Avoid an error from `triton profile list` if
only *some* of the minimal `TRITON_` or `SDC_` envvars are defined.
- [TRITON-401] Add `triton network` and `triton vlan` commands, for
creating/changing/removing network fabrics and VLANs.
- [TRITON-524] Add `triton inst get --credentials ...` option to match
`triton inst list --credentials ...` for including generated credentials
in instance metadata.
- [joyent/node-triton#245] `triton profile` now generates fresh new keys during
Docker setup and signs them with an account key, rather than copying (and
decrypting) the account key itself. This makes using Docker simpler with keys
in an SSH Agent.
- [TRITON-53] x-account image clone. A user can make a copy of a shared image
using the `triton image clone` command.
- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no
longer provisionable by default - you will need to explicitly add the
--allow-shared-images cli option when calling `triton create` command to
provision from a shared image (or clone the image then provision from the
clone).
- [TRITON-52] x-DC image copy. A user can copy an image that they own into
another datacenter within the same cloud using the `triton image copy` cli
command. Example:
```
triton -p us-east-1 image cp my-custom-image us-sw-1
```
## 6.0.0
This release containes some breaking changes with the --affinity flag to
`triton instance create`. It also does not work with cloudapi endpoints older
than 8.0.0 (mid 2016); for an older cloudapi endpoint, use node-triton 5.9.0.
- [TRITON-167, TRITON-168] update support for
`triton instance create --affinity=...`. It now fully supports regular
expressions, tags and globs, and works across a wider variety of situations.
Examples:
```
# regular expressions
triton instance create --affinity='instance!=/^production-db/' ...
# globs
triton instance create --affinity='instance!=production-db*' ...
# tags
triton instance create --affinity='role!=db'
```
See <https://apidocs.joyent.com/cloudapi/#affinity-rules> for more details
how affinities work.
However:
- Use of regular expressions requires a cloudapi version of 8.8.0 or later.
- 'inst' as a affinity shorthand no longer works. Use 'instance' instead.
E.g.: --affinity='instance==db1' instead of --affinity='inst==db1'
- The shorthand --affinity=<INST> no longer works. Use
--affinity='instance===<INST>' instead.
## 5.10.0
- [TRITON-19] add support for deletion protection on instances. An instance with
the deletion protection flag set true cannot be destroyed until the flag is
set false. It is exposed through
`triton instance create --deletion-protection ...`,
`triton instance enable-deletion-protection ...`, and
`triton instance disable-deletion-protection ...`. This flag is only supported
on cloudapi versions 8.7.0 or above.
- [TRITON-59] node-triton should support nic operations
`triton instance nic get ...`
`triton instance nic create ...`
`triton instance nic list ...`
`triton instance nic delete ...`
- [TRITON-42] node-triton should support nics when creating an instance, e.g.
`triton instance create --nic <Network Object> IMAGE PACKAGE`
## 5.9.0
- [TRITON-190] remove support for `triton instance create --brand=bhyve ...`.
The rest of bhyve support will remain, but selection of bhyve brand will
happen via images or packages that are bhyve-specific.
## 5.8.0
- [TRITON-124] add node-triton support for bhyve. This adds a `triton instance
create --brand=bhyve ...` option that can be used for zvol images that support
it. Note that bhyve support is alpha in TritonDC -- most datacenters won't yet
support this option.
## 5.7.0
- [TRITON-116] node-triton image sharing. Adds `triton image share` and
`triton image unshare` commands.
## 5.6.1
- [PUBAPI-1470] volume objects should expose their creation timestamp in a

View File

@ -2,18 +2,21 @@
# node-spearhead
This repository holds the node-spearhead CLI tool to work with the Spearhead Cloud.
This repository holds the node-spearhead CLI tool to work with the Spearhead
Cloud. It is a fork of [node-triton](https://github.com/joyent/node-triton).
## Installation and configuration
### Get a Spearhead Cloud account
Create an account on the Spearhead Cloud and upload your SSH key.[!TBD: docs]You can create an account [here](https://spearhead.cloud/).
Create an account on the Spearhead Cloud and upload your SSH key. You can create an account
[here](https://spearhead.cloud/).
### Data-centers
The list of available Spearhead Cloud data-centers is available [here](https://spearhead.cloud/datacenters).
The list of available Spearhead Cloud data-centers is available
[here](https://spearhead.cloud/datacenters).
### Installation
@ -22,8 +25,14 @@ Install [node.js](http://nodejs.org/), then:
npm install -g spearhead
Now you ca use `spearhead` to interact with our Public Cloud. More details about installation and configuration are available [here](https://docs.spearhead.cloud).
Verify that it is installed and on your PATH:
$ spearhead --version
Spearhead CLI 6.1.4
https://code.spearhead.cloud/Spearhead/node-spearhead
Now you ca use `spearhead` to interact with our Public Cloud. More details
about installation and configuration are available
[here](https://docs.spearhead.cloud).
## License
MPL 2.0

View File

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

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* Client library for the SmartDataCenter Cloud API (cloudapi).
* http://apidocs.joyent.com/cloudapi/
@ -154,7 +154,6 @@ function CloudApi(options) {
this.client = new SaferJsonClient(options);
}
CloudApi.prototype.close = function close(callback) {
this.log.trace({host: this.client.url && this.client.url.host},
'close cloudapi http client');
@ -358,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
/**
@ -459,6 +500,229 @@ CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = {
reserved: 'boolean'
};
// --- Fabric VLANs
/**
* Creates a network on 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.
* - {Array} resolvers (optional) DNS resolvers for hosts on network.
* - {Object} routes (optional) Static routes for hosts on network.
* - {String} description (optional)
* - {Boolean} internet_nat (optional) Whether to provision an Internet
* NAT on the gateway address (default: true).
* @param {Function} callback of the form f(err, vlan, res).
*/
CloudApi.prototype.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.optionalArrayOfString(opts.resolvers, 'opts.resolvers');
assert.optionalObject(opts.routes, 'opts.routes');
assert.optionalBool(opts.internet_nat, 'opts.internet_nat');
var data = common.objCopy(opts);
var 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
/**
@ -767,6 +1031,92 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
});
};
/**
* Update an image.
* <http://apidocs.joyent.com/cloudapi/#UpdateImage>
*
* @param {Object} opts
* - {UUID} id Required. The id of the image to update.
* - {Object} fields Required. The fields to update in the image.
* @param {Function} cb of the form `function (err, body, res)`
*/
CloudApi.prototype.updateImage = function updateImage(opts, cb) {
assert.uuid(opts.id, 'id');
assert.object(opts.fields, 'fields');
assert.func(cb, 'cb');
this._request({
method: 'POST',
path: format('/%s/images/%s?action=update', this.account, opts.id),
data: opts.fields
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
cb(null, body, res);
});
};
/**
* 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.
*
@ -846,13 +1196,12 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) {
/**
* Get a machine by id.
*
* XXX add getCredentials equivalent
* XXX cloudapi docs don't doc the credentials=true option
*
* For backwards compat, calling with `getMachine(id, cb)` is allowed.
*
* @param {Object} opts
* - id {UUID} Required. The machine id.
* - {UUID} id - Required. The machine id.
* - {Boolean} credentials - Optional. Set to true to include generated
* credentials for this machine in `machine.metadata.credentials`.
* @param {Function} cb of the form `function (err, machine, res)`
*/
CloudApi.prototype.getMachine = function getMachine(opts, cb) {
@ -861,9 +1210,14 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) {
}
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.optionalBool(opts.credentials, 'opts.credentials');
var endpoint = format('/%s/machines/%s', this.account, opts.id);
this._request(endpoint, function (err, req, res, body) {
var query = {};
if (opts.credentials) {
query.credentials = 'true';
}
var p = this._path(format('/%s/machines/%s', this.account, opts.id), query);
this._request(p, function (err, req, res, body) {
cb(err, body, res);
});
};
@ -979,7 +1333,6 @@ function enableMachineFirewall(uuid, callback) {
return this._doMachine('enable_firewall', uuid, callback);
};
/**
* Disables machine firewall.
*
@ -991,6 +1344,28 @@ function disableMachineFirewall(uuid, callback) {
return this._doMachine('disable_firewall', uuid, callback);
};
/**
* Enables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.enableMachineDeletionProtection =
function enableMachineDeletionProtection(uuid, callback) {
return this._doMachine('enable_deletion_protection', uuid, callback);
};
/**
* Disables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.disableMachineDeletionProtection =
function disableMachineDeletionProtection(uuid, callback) {
return this._doMachine('disable_deletion_protection', uuid, callback);
};
/**
* internal function for start/stop/reboot/enable_firewall/disable_firewall
*/
@ -1123,7 +1498,7 @@ CloudApi.prototype.createMachine = function createMachine(options, callback) {
assert.optionalString(options.name, 'options.name');
assert.uuid(options.image, 'options.image');
assert.uuid(options.package, 'options.package');
assert.optionalArrayOfUuid(options.networks, 'options.networks');
assert.optionalArray(options.networks, 'options.networks');
// TODO: assert the other fields
assert.func(callback, 'callback');
@ -1201,6 +1576,53 @@ function waitForMachineFirewallEnabled(opts, cb) {
};
/**
* Wait for a machine's `deletion_protection` field to go true or
* false/undefined.
*
* @param {Object} options
* - {String} id: Required. The machine UUID.
* - {Boolean} state: Required. The desired `deletion_protection` state.
* - {Number} interval: Optional. Time (in ms) to poll.
* @param {Function} callback of the form f(err, machine, res).
*/
CloudApi.prototype.waitForDeletionProtectionEnabled =
function waitForDeletionProtectionEnabled(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.bool(opts.state, 'opts.state');
assert.optionalNumber(opts.interval, 'opts.interval');
assert.func(cb, 'cb');
var interval = opts.interval || 1000;
assert.ok(interval > 0, 'interval must be a positive number');
poll();
function poll() {
self.getMachine({
id: opts.id
}, function getMachineCb(err, machine, res) {
if (err) {
cb(err, null, res);
return;
}
// !! converts an undefined to a false
if (opts.state === !!machine.deletion_protection) {
cb(null, machine, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- machine tags
/**
@ -1509,6 +1931,150 @@ function deleteMachineSnapshot(opts, cb) {
};
// --- NICs
/**
* Adds a NIC on a network to an instance.
*
* @param {Object} options object containing:
* - {String} id (required) the instance id.
* - {String|Object} (required) network uuid or network object.
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.addNic =
function addNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network');
var data = {
network: opts.network
};
this._request({
method: 'POST',
path: format('/%s/machines/%s/nics', this.account, opts.id),
data: data
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Lists all NICs on an instance.
*
* Returns an array of objects.
*
* @param opts {Object} Options
* - {String} id (required) the instance id.
* @param {Function} callback of the form f(err, nics, res).
*/
CloudApi.prototype.listNics =
function listNics(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s/nics', this.account, opts.id);
this._passThrough(endpoint, opts, cb);
};
/**
* Retrieves a NIC on an instance.
*
* @param {Object} options object containing:
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} callback of the form `function (err, nic, res)`
*/
CloudApi.prototype.getNic =
function getNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
var endpoint = format('/%s/machines/%s/nics/%s', this.account, opts.id,
mac);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Remove a NIC off an instance.
*
* @param {Object} opts (object)
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} cb of the form `function (err, res)`
*/
CloudApi.prototype.removeNic =
function removeNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
this._request({
method: 'DELETE',
path: format('/%s/machines/%s/nics/%s', this.account, opts.id, mac)
}, function (err, req, res) {
cb(err, res);
});
};
/**
* Wait for a machine's nic to go one of a set of specfic states.
*
* @param {Object} options
* - {String} id {required} machine id
* - {String} mac {required} mac for new nic
* - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.waitForNicStates =
function waitForNicStates(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.arrayOfString(opts.states, 'opts.states');
assert.optionalNumber(opts.interval, 'opts.interval');
assert.func(cb, 'cb');
var interval = opts.interval || 1000;
assert.ok(interval > 0, 'interval must be a positive number');
poll();
function poll() {
self.getNic({
id: opts.id,
mac: opts.mac
}, function onPoll(err, nic, res) {
if (err) {
cb(err, null, res);
return;
}
if (opts.states.indexOf(nic.state) !== -1) {
cb(null, nic, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- firewall rules
/**

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2017, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
var assert = require('assert-plus');
@ -24,7 +24,8 @@ var wordwrap = require('wordwrap');
var errors = require('./errors'),
InternalError = errors.InternalError;
var NETWORK_OBJECT_FIELDS =
require('./constants').NETWORK_OBJECT_FIELDS;
// ---- support stuff
@ -617,9 +618,9 @@ function promptYesNo(opts_, cb) {
stdin.on('data', onData);
function postInput() {
stdout.write('\n');
stdin.setRawMode(false);
stdin.pause();
stdin.write('\n');
stdin.removeListener('data', onData);
}
@ -1412,6 +1413,68 @@ function ipv4ToLong(ip) {
return l;
}
/*
* Parse the input from the `--nics <nic>` CLI argument.
*
* @param a {Array} The array of strings formatted as key=value
* ex: ['ipv4_uuid=1234', 'ipv4_ips=1.2.3.4|5.6.7.8']
* @return {Object} A network object. From the example above:
* {
* "ipv4_uuid": 1234,
* "ipv4_ips": [
* "1.2.3.4",
* "5.6.7.8"
* ]
* }
* Note: "1234" is used as the UUID for this example, but would actually cause
* `parseNicStr` to throw as it is not a valid UUID.
*/
function parseNicStr(nic) {
assert.arrayOfString(nic);
var obj = objFromKeyValueArgs(nic, {
disableDotted: true,
typeHintFromKey: NETWORK_OBJECT_FIELDS,
validKeys: Object.keys(NETWORK_OBJECT_FIELDS)
});
if (!obj.ipv4_uuid) {
throw new errors.UsageError(
'ipv4_uuid must be specified in network object');
}
if (obj.ipv4_ips) {
obj.ipv4_ips = obj.ipv4_ips.split('|');
}
assert.uuid(obj.ipv4_uuid, 'obj.ipv4_uuid');
assert.optionalArrayOfString(obj.ipv4_ips, 'obj.ipv4_ips');
/*
* Only 1 IP address may be specified at this time. In the future, this
* limitation should be removed.
*/
if (obj.ipv4_ips && obj.ipv4_ips.length !== 1) {
throw new errors.UsageError('only 1 ipv4_ip may be specified');
}
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
module.exports = {
@ -1451,6 +1514,8 @@ module.exports = {
monotonicTimeDiffMs: monotonicTimeDiffMs,
readStdin: readStdin,
validateObject: validateObject,
ipv4ToLong: ipv4ToLong
ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr,
imageRepr: imageRepr
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -296,12 +296,11 @@ function _loadEnvProfile(profileOverrides) {
for (var attr in profileOverrides) {
envProfile[attr] = profileOverrides[attr];
}
/*
* If none of the above envvars are defined, then there is no env profile.
* If missing any of the required vars, then there is no env profile.
*/
if (!envProfile.account && !envProfile.user && !envProfile.url &&
!envProfile.keyId)
{
if (!envProfile.account || !envProfile.url || !envProfile.keyId) {
return null;
}
validateProfile(envProfile, 'environment variables');
@ -363,10 +362,11 @@ function loadProfile(opts) {
function loadAllProfiles(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.object(opts.log, 'opts.log');
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
var profiles = [];
var envProfile = _loadEnvProfile();
var envProfile = _loadEnvProfile(opts.profileOverrides);
if (envProfile) {
profiles.push(envProfile);
}

View File

@ -46,11 +46,18 @@ if (process.env.SCTEST_CLI_CONFIG_DIR) {
CLI_CONFIG_DIR = mod_path.resolve(process.env.HOME, '.spearhead');
}
// <Network Object Key> -> <expected typeof>
var NETWORK_OBJECT_FIELDS = {
ipv4_uuid: 'string',
ipv4_ips: 'string'
};
// ---- exports
module.exports = {
CLI_CONFIG_DIR: CLI_CONFIG_DIR
CLI_CONFIG_DIR: CLI_CONFIG_DIR,
NETWORK_OBJECT_FIELDS: NETWORK_OBJECT_FIELDS
};

View File

@ -72,12 +72,8 @@ function do_update(subcmd, opts, args, callback) {
next();
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -92,36 +88,18 @@ function do_update(subcmd, opts, args, callback) {
},
function validateIt(ctx, next) {
var keys = Object.keys(ctx.data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_ACCOUNT_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_ACCOUNT_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;
}
try {
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS);
} catch (e) {
next(e);
return;
}
next();
},
function updateAway(ctx, next) {
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for account update');
next();
return;
}
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
if (err) {

View File

@ -160,7 +160,7 @@ function do_env(subcmd, opts, args, cb) {
});
p('# Run this command to configure your shell:');
p('# eval "$(triton env%s%s)"',
p('# eval "$(spearhead env%s%s)"',
(shortOpts ? ' -'+shortOpts : ''),
(profile.name === this.tritonapi.profile.name
? '' : ' ' + profile.name));

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*
* `triton fwrule instances ...`
*/
@ -111,9 +111,11 @@ function do_instances(subcmd, opts, args, cb) {
common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0];
var flags = [];
if (inst.brand === 'bhyve') flags.push('B');
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined;
});
@ -159,9 +161,11 @@ do_instances.help = [
'for convenience):',
' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:',
' "B" the brand is "bhyve"',
' "D" docker instance',
' "F" firewall is enabled',
' "K" the brand is "kvm"',
' "P" deletion protected',
' age* Approximate time since created, e.g. 1y, 2w.',
' img* The image "name@version", if available, else its',
' "shortid".'

View File

@ -84,14 +84,7 @@ function do_update(subcmd, opts, args, cb) {
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -107,33 +100,13 @@ function do_update(subcmd, opts, args, cb) {
},
function validateIt(ctx, next) {
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for firewall rule update');
next();
try {
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
} catch (e) {
next(e);
return;
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_FWRULE_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_FWRULE_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();
},

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

@ -31,7 +31,11 @@ function do_get(subcmd, opts, args, callback) {
callback(setupErr);
return;
}
tritonapi.getImage(args[0], function onRes(err, img) {
var getOpts = {
name: args[0],
excludeInactive: !opts.all
};
tritonapi.getImage(getOpts, function onRes(err, img) {
if (err) {
return callback(err);
}
@ -56,6 +60,15 @@ do_get.options = [
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
group: 'Filtering options'
},
{
names: ['all', 'a'],
type: 'bool',
help: 'Include all images when matching by name or short ID, not ' +
'just "active" ones. By default only active images are included.'
}
];

View File

@ -5,13 +5,15 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*
* `triton image list ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
@ -67,17 +69,45 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all';
}
var self = this;
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
return;
}
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
}
vasync.pipeline({ arg: {}, funcs: [
function setupTritonApi(_, next) {
common.cliSetupTritonApi({cli: self.top}, next);
},
function getImages(ctx, next) {
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
next(err);
return;
}
ctx.imgs = imgs;
next();
});
},
function getUserAccount(ctx, next) {
// If using json output, or when there are no images that use an ACL
// - we don't need to fetch the account, as the account is only used
// to check if the image is shared (i.e. the account is in the image
// ACL) so it can output image flags in non-json mode.
if (opts.json || ctx.imgs.every(function _checkAcl(img) {
return !Array.isArray(img.acl) || img.acl.length === 0;
})) {
next();
return;
}
tritonapi.cloudapi.getAccount(function _accountCb(err, account) {
if (err) {
next(err);
return;
}
ctx.account = account;
next();
});
},
function formatImages(ctx, next) {
var imgs = ctx.imgs;
if (opts.json) {
common.jsonStream(imgs);
} else {
@ -99,6 +129,20 @@ function do_list(subcmd, opts, args, callback) {
if (img.origin) flags.push('I');
if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X');
// Add image sharing flags.
if (Array.isArray(img.acl) && img.acl.length > 0) {
assert.string(ctx.account.id, 'ctx.account.id');
if (img.owner === ctx.account.id) {
// This image has been shared with other accounts.
flags.push('+');
}
if (img.acl.indexOf(ctx.account.id) !== -1) {
// This image has been shared with this account.
flags.push('S');
}
}
img.flags = flags.length ? flags.join('') : undefined;
}
@ -108,9 +152,9 @@ function do_list(subcmd, opts, args, callback) {
sort: sort
});
}
callback();
});
});
next();
}
]}, callback);
}
do_list.options = [
@ -156,6 +200,8 @@ do_list.help = [
' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:',
' "P" image is public',
' "+" you are sharing this image with others',
' "S" this image has been shared with you',
' "I" an incremental image (i.e. has an origin)',
' "X" has a state *other* than "active"',
' pubdate* Short form of "published_at" with just the date',

115
lib/do_image/do_share.js Normal file
View File

@ -0,0 +1,115 @@
/*
* 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 share ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_share(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: expect 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 shareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image share account');
if (opts.dry_run) {
next();
return;
}
tritonapi.shareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error sharing image'));
return;
}
log.trace({img: img}, 'image share result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Shared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_share.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 sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_share.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_share.help = [
/* BEGIN JSSTYLED */
'Share an image with another account.',
'',
'{{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).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be shared.'
/* END JSSTYLED */
].join('\n');
do_share.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_share;

115
lib/do_image/do_unshare.js Normal file
View File

@ -0,0 +1,115 @@
/*
* 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 unshare ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_unshare(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: expect 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 unshareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image unshare account');
if (opts.dry_run) {
next();
return;
}
tritonapi.unshareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error unsharing image'));
return;
}
log.trace({img: img}, 'image unshare result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Unshared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_unshare.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 sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_unshare.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_unshare.help = [
/* BEGIN JSSTYLED */
'Unshare an image with another account.',
'',
'{{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).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be unshared.'
/* END JSSTYLED */
].join('\n');
do_unshare.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_unshare;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image ...`
*/
@ -33,9 +33,13 @@ function ImageCLI(top) {
'help',
'list',
'get',
'clone',
'copy',
'create',
'delete',
'export',
'share',
'unshare',
'wait'
]
});
@ -49,9 +53,13 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
ImageCLI.prototype.do_list = require('./do_list');
ImageCLI.prototype.do_get = require('./do_get');
ImageCLI.prototype.do_clone = require('./do_clone');
ImageCLI.prototype.do_copy = require('./do_copy');
ImageCLI.prototype.do_create = require('./do_create');
ImageCLI.prototype.do_delete = require('./do_delete');
ImageCLI.prototype.do_export = require('./do_export');
ImageCLI.prototype.do_share = require('./do_share');
ImageCLI.prototype.do_unshare = require('./do_unshare');
ImageCLI.prototype.do_wait = require('./do_wait');

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2019 Joyent, Inc.
*
* `triton instance create ...`
*/
@ -19,6 +19,8 @@ var common = require('../common');
var distractions = require('../distractions');
var errors = require('../errors');
var mat = require('../metadataandtags');
var NETWORK_OBJECT_FIELDS =
require('../constants').NETWORK_OBJECT_FIELDS;
function parseVolMount(volume) {
var components;
@ -83,6 +85,9 @@ function do_create(subcmd, opts, args, cb) {
return;
} else if (args.length !== 2) {
return cb(new errors.UsageError('incorrect number of args'));
} else if (opts.nic && opts.network) {
return cb(new errors.UsageError(
'--network and --nic cannot be specified together'));
}
var log = this.top.log;
@ -90,103 +95,6 @@ function do_create(subcmd, opts, args, cb) {
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
/* BEGIN JSSTYLED */
/*
* Parse --affinity options for validity to `ctx.affinities`.
* Later (in `resolveLocality`) we'll translate this to locality hints
* that CloudAPI speaks.
*
* Some examples. Inspired by
* <https://docs.docker.com/swarm/scheduler/filter/#how-to-write-filter-expressions>
*
* instance==vm1
* container==vm1 # alternative to 'instance'
* inst==vm1 # alternative to 'instance'
* inst=vm1 # '=' is shortcut for '=='
* inst!=vm1 # '!='
* inst==~vm1 # '~' for soft/non-strict
* inst!=~vm1
*
* inst==vm* # globbing (not yet supported)
* inst!=/vm\d/ # regex (not yet supported)
*
* some-tag!=db # tags (not yet supported)
*
* Limitations:
* - no support for tags yet
* - no globbing or regex yet
* - we resolve name -> instance id *client-side* for now (until
* CloudAPI supports that)
* - Triton doesn't support mixed strict and non-strict, so we error
* out on that. We *could* just drop the non-strict, but that is
* slightly different.
*/
/* END JSSTYLED */
function parseAffinity(ctx, next) {
if (!opts.affinity) {
next();
return;
}
var affinities = [];
// TODO: stricter rules on the value part
// JSSTYLED
var affinityRe = /((instance|inst|container)(==~|!=~|==|!=|=~|=))?(.*?)$/;
for (var i = 0; i < opts.affinity.length; i++) {
var raw = opts.affinity[i];
var match = affinityRe.exec(raw);
if (!match) {
next(new errors.UsageError(format('invalid affinity: "%s"',
raw)));
return;
}
var key = match[2];
if ([undefined, 'inst', 'container'].indexOf(key) !== -1) {
key = 'instance';
}
assert.equal(key, 'instance');
var op = match[3];
if ([undefined, '='].indexOf(op) !== -1) {
op = '==';
}
var strict = true;
if (op[op.length - 1] === '~') {
strict = false;
op = op.slice(0, op.length - 1);
}
var val = match[4];
// Guard against mixed strictness (Triton can't handle those).
if (affinities.length > 0) {
var lastAff = affinities[affinities.length - 1];
if (strict !== lastAff.strict) {
next(new errors.TritonError(format('mixed strict and '
+ 'non-strict affinities are not supported: '
+ '%j (%s) and %j (%s)',
lastAff.raw,
(lastAff.strict ? 'strict' : 'non-strict'),
raw, (strict ? 'strict' : 'non-strict'))));
return;
}
}
affinities.push({
raw: raw,
key: key,
op: op,
strict: strict,
val: val
});
}
if (affinities.length) {
log.trace({affinities: affinities}, 'affinities');
ctx.affinities = affinities;
}
next();
},
/*
* Make sure if volumes were passed, they're in the correct form.
@ -225,61 +133,44 @@ function do_create(subcmd, opts, args, cb) {
},
/*
* Determine `ctx.locality` according to what CloudAPI supports
* based on `ctx.affinities` parsed earlier.
* Parse any nics given via `--nic`
*/
function resolveLocality(ctx, next) {
if (!ctx.affinities) {
function parseNics(ctx, next) {
if (!opts.nic) {
next();
return;
}
var strict;
var near = [];
var far = [];
ctx.nics = [];
var i;
var networksSeen = {};
var nic;
var nics = opts.nic;
vasync.forEachPipeline({
inputs: ctx.affinities,
func: function resolveAffinity(aff, nextAff) {
assert.ok(['==', '!='].indexOf(aff.op) !== -1,
'unexpected op: ' + aff.op);
var nearFar = (aff.op == '==' ? near : far);
log.trace({nics: nics}, 'parsing nics');
strict = aff.strict;
if (common.isUUID(aff.val)) {
nearFar.push(aff.val);
nextAff();
} else {
tritonapi.getInstance({
id: aff.val,
fields: ['id']
}, function (err, inst) {
if (err) {
nextAff(err);
} else {
log.trace({val: aff.val, inst: inst.id},
'resolveAffinity');
nearFar.push(inst.id);
nextAff();
}
});
for (i = 0; i < nics.length; i++) {
nic = nics[i].split(',');
try {
nic = common.parseNicStr(nic);
if (networksSeen[nic.ipv4_uuid]) {
throw new errors.UsageError(format(
'only 1 ip on a network allowed '
+ '(network %s specified multiple times)',
nic.ipv4_uuid));
}
}
}, function (err) {
if (err) {
networksSeen[nic.ipv4_uuid] = true;
ctx.nics.push(nic);
} catch (err) {
next(err);
return;
}
}
ctx.locality = {
strict: strict
};
if (near.length > 0) ctx.locality.near = near;
if (far.length > 0) ctx.locality.far = far;
log.trace({locality: ctx.locality}, 'resolveLocality');
log.trace({nics: ctx.nics}, 'parsed nics');
next();
});
next();
},
function loadMetadata(ctx, next) {
@ -312,6 +203,7 @@ function do_create(subcmd, opts, args, cb) {
function getImg(ctx, next) {
var _opts = {
name: args[0],
excludeInactive: true,
useCache: true
};
tritonapi.getImage(_opts, function (err, img) {
@ -371,16 +263,22 @@ function do_create(subcmd, opts, args, cb) {
var createOpts = {
name: opts.name,
image: ctx.img.id,
'package': ctx.pkg && ctx.pkg.id,
networks: ctx.nets && ctx.nets.map(
function (net) { return net.id; })
'package': ctx.pkg && ctx.pkg.id
};
if (ctx.nets) {
createOpts.networks = ctx.nets.map(function (net) {
return net.id;
});
} else if (ctx.nics) {
createOpts.networks = ctx.nics;
}
if (ctx.volMounts) {
createOpts.volumes = ctx.volMounts;
}
if (ctx.locality) {
createOpts.locality = ctx.locality;
if (opts.affinity) {
createOpts.affinity = opts.affinity;
}
if (ctx.metadata) {
Object.keys(ctx.metadata).forEach(function (key) {
@ -392,11 +290,16 @@ function do_create(subcmd, opts, args, cb) {
createOpts['tag.'+key] = ctx.tags[key];
});
}
if (opts.allow_shared_images) {
createOpts.allow_shared_images = true;
}
for (var i = 0; i < opts._order.length; i++) {
var opt = opts._order[i];
if (opt.key === 'firewall') {
createOpts.firewall_enabled = opt.value;
} else if (opt.key === 'deletion_protection') {
createOpts.deletion_protection = opt.value;
}
}
@ -520,9 +423,7 @@ do_create.options = [
'INST), `instance==~INST` (*attempt* to place on the same server ' +
'as INST), or `instance!=~INST` (*attempt* to place on a server ' +
'other than INST\'s). `INST` is an existing instance name or ' +
'id. There are two shortcuts: `inst` may be used instead of ' +
'`instance` and `instance==INST` can be shortened to just ' +
'`INST`. Use this option more than once for multiple rules.',
'id. Use this option more than once for multiple rules.',
completionType: 'tritonaffinityrule'
},
@ -537,6 +438,15 @@ do_create.options = [
'This option can be used multiple times.',
completionType: 'tritonnetwork'
},
{
names: ['nic'],
type: 'arrayOfString',
helpArg: 'NICOPTS',
help: 'A network interface object containing comma separated ' +
'key=value pairs (Network object format). ' +
'This option can be used multiple times for multiple NICs. ' +
'Valid keys are: ' + Object.keys(NETWORK_OBJECT_FIELDS).join(', ')
},
{
// TODO: add boolNegationPrefix:'no-' when that cmdln pull is in
names: ['firewall'],
@ -544,6 +454,13 @@ do_create.options = [
help: 'Enable Cloud Firewall on this instance. See ' +
'<https://docs.spearhead.cloud/network/firewall>'
},
{
names: ['deletion-protection'],
type: 'bool',
help: 'Enable Deletion Protection on this instance. Such an instance ' +
'cannot be deleted until the protection is disabled. See ' +
'<https://apidocs.joyent.com/cloudapi/#deletion-protection>'
},
{
names: ['volume', 'v'],
type: 'arrayOfString',
@ -585,6 +502,11 @@ do_create.options = [
'Joyent-provided images, the user-script is run at every boot ' +
'of the instance. This is a shortcut for `-M user-script=FILE`.'
},
{
names: ['allow-shared-images'],
type: 'bool',
help: 'Allow instance creation to use a shared image.'
},
{
group: 'Other options'

View File

@ -0,0 +1,125 @@
/*
* 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 instance disable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_disable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing INST argument(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: false
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(!inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Disabled deletion protection for instance "%s"', name);
next();
});
}
function disableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.disableInstanceDeletionProtection({
id: name
}, function disableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Disabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: disableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_disable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be removed.'
}
];
do_disable_deletion_protection.synopses = [
'{{name}} disable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_disable_deletion_protection.help = [
'Disable deletion protection on one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_disable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_disable_deletion_protection;

View File

@ -0,0 +1,125 @@
/*
* 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 instance enable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_enable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing INST argument(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: true
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Enabled deletion protection for instance "%s"', name);
next();
});
}
function enableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.enableInstanceDeletionProtection({
id: name
}, function enableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Enabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: enableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_enable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be enabled.'
}
];
do_enable_deletion_protection.synopses = [
'{{name}} enable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_enable_deletion_protection.help = [
'Enable deletion protection for one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_enable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_enable_deletion_protection;

View File

@ -25,7 +25,10 @@ function do_get(subcmd, opts, args, cb) {
cb(setupErr);
return;
}
tritonapi.getInstance(args[0], function (err, inst) {
tritonapi.getInstance({
id: args[0],
credentials: opts.credentials
}, function onInst(err, inst) {
if (inst) {
if (opts.json) {
console.log(JSON.stringify(inst));
@ -44,6 +47,13 @@ do_get.options = [
type: 'bool',
help: 'Show this help.'
},
{
names: ['credentials'],
type: 'bool',
help: 'Include generated credentials, in the "metadata.credentials" ' +
'field, if any. Typically used with "-j", though one can show ' +
'values with "-o metadata.credentials".'
},
{
names: ['json', 'j'],
type: 'bool',
@ -58,7 +68,6 @@ do_get.help = [
'{{usage}}',
'',
'{{options}}',
'',
'Where "INST" is an instance name, id, or short id.',
'',
'A *deleted* instance may still respond with the instance object. In that',

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2017, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* `triton instance list ...`
*/
@ -150,9 +150,11 @@ function do_list(subcmd, opts, args, callback) {
common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0];
var flags = [];
if (inst.brand === 'bhyve') flags.push('B');
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined;
});
@ -208,9 +210,11 @@ do_list.help = [
'for convenience):',
' shortid* A short ID prefix.',
' flags* Single letter flags summarizing some fields:',
' "B" the brand is "bhyve"',
' "D" docker instance',
' "F" firewall is enabled',
' "K" the brand is "kvm"',
' "P" deletion protected',
' age* Approximate time since created, e.g. 1y, 2w.',
' img* The image "name@version", if available, else its',
' "shortid".',

View File

@ -0,0 +1,211 @@
/*
* 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 instance nic create ...`
*/
var assert = require('assert-plus');
var common = require('../../common');
var errors = require('../../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalBool(opts.wait, 'opts.wait');
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 < 2) {
cb(new errors.UsageError('missing INST and NETWORK or INST and' +
' NICOPT=VALUE arguments'));
return;
}
var cli = this.top;
var netObj;
var netObjArgs = [];
var regularArgs = [];
var createOpts = {};
args.forEach(function forEachArg(arg) {
if (arg.indexOf('=') !== -1) {
netObjArgs.push(arg);
return;
}
regularArgs.push(arg);
});
if (netObjArgs.length > 0) {
if (regularArgs.length > 1) {
cb(new errors.UsageError('cannot specify INST and NETWORK when'
+ ' passing in ipv4 arguments'));
return;
}
if (regularArgs.length !== 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
try {
netObj = common.parseNicStr(netObjArgs);
} catch (err) {
cb(err);
return;
}
}
if (netObj) {
assert.array(regularArgs, 'regularArgs');
assert.equal(regularArgs.length, 1, 'instance uuid');
createOpts.id = regularArgs[0];
createOpts.network = netObj;
} else {
assert.array(args, 'args');
assert.equal(args.length, 2, 'INST and NETWORK');
createOpts.id = args[0];
createOpts.network = args[1];
}
function wait(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
var waiter = cli.tritonapi.waitForNicStates.bind(cli.tritonapi);
/*
* We request state running|stopped because net-agent is doing work to
* keep a NICs state in sync with the VMs state. If a user adds a NIC
* to a stopped instance the final state of the NIC should also be
* stopped.
*/
waiter({
id: instId,
mac: mac,
states: ['running', 'stopped']
}, next);
}
// same signature as wait(), but is a nop
function waitNop(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
next();
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.addNic(createOpts, function onAddNic(err, nic) {
if (err) {
cb(err);
return;
}
// If a NIC exists on the network already we will receive a 302
if (!nic) {
var errMsg = 'Instance already has a NIC on that network';
cb(new errors.TritonError(errMsg));
return;
}
// either wait or invoke a nop stub
var func = opts.wait ? wait : waitNop;
if (opts.wait && !opts.json) {
console.log('Creating NIC %s', nic.mac);
}
func(createOpts.id, nic.mac, function onWait(err2, createdNic) {
if (err2) {
cb(err2);
return;
}
var nicInfo = createdNic || nic;
if (opts.json) {
console.log(JSON.stringify(nicInfo));
} else {
console.log('Created NIC %s', nic.mac);
}
cb();
});
});
});
}
do_create.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the creation to complete.'
}
];
do_create.synopses = [
'{{name}} {{cmd}} [OPTIONS] INST NETWORK',
'{{name}} {{cmd}} [OPTIONS] INST NICOPT=VALUE [NICOPT=VALUE ...]'
];
do_create.help = [
'Create a NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'INST is an instance id (full UUID), name, or short id,',
'and NETWORK is a network id (full UUID), name, or short id.',
'',
'NICOPTs are NIC options. The following NIC options are supported:',
'ipv4_uuid=<full network uuid> (required),' +
' and ipv4_ips=<a single IP string>.',
'',
'Be aware that adding NICs to an instance will cause that instance to',
'reboot.',
'',
'Example:',
' triton instance nic create --wait 22b75576 ca8aefb9',
' triton instance nic create 22b75576' +
' ipv4_uuid=651446a8-dab0-439e-a2c4-2c841ab07c51' +
' ipv4_ips=192.168.128.13'
].join('\n');
do_create.helpOpts = {
helpCol: 25
};
do_create.completionArgtypes = ['tritoninstance', 'tritonnic', 'none'];
module.exports = do_create;

View File

@ -0,0 +1,126 @@
/*
* 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 instance nic delete ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
function do_delete(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.optionalBool(opts.force, 'opts.force');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and MAC argument(s)'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
confirm({mac: mac, force: opts.force}, function onConfirm(confirmErr) {
if (confirmErr) {
console.error('Aborting');
cb();
return;
}
cli.tritonapi.removeNic({
id: inst,
mac: mac
}, function onRemove(err) {
if (err) {
cb(err);
return;
}
console.log('Deleted NIC %s', mac);
cb();
});
});
});
}
// Request confirmation before deleting, unless --force flag given.
// If user declines, terminate early.
function confirm(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
if (opts.force) {
cb();
return;
}
common.promptYesNo({
msg: 'Delete NIC "' + opts.mac + '"? [y/n] '
}, function (answer) {
if (answer !== 'y') {
cb(new Error('Aborted NIC deletion'));
} else {
cb();
}
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['force', 'f'],
type: 'bool',
help: 'Force removal.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} INST MAC'];
do_delete.help = [
'Remove a NIC from an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Be aware that removing NICs from an instance will cause that instance to',
'reboot.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_delete;

View File

@ -0,0 +1,89 @@
/*
* 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 instance nic 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 < 2) {
cb(new errors.UsageError('missing INST and MAC arguments'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.getNic({id: inst, mac: mac}, function onNic(err, nic) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(nic));
} else {
console.log(JSON.stringify(nic, 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}} INST MAC'];
do_get.help = [
'Show a specific NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.'
].join('\n');
do_get.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_get;

View File

@ -0,0 +1,154 @@
/*
* 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 instance nic list ...`
*/
var assert = require('assert-plus');
var tabula = require('tabula');
var common = require('../../common');
var errors = require('../../errors');
var VALID_FILTERS = ['ip', 'mac', 'state', 'network', 'primary', 'gateway'];
var COLUMNS_DEFAULT = 'ip,mac,state,network';
var COLUMNS_DEFAULT_LONG = 'ip,mac,state,network,primary,gateway';
var SORT_DEFAULT = 'ip';
function do_list(subcmd, opts, args, cb) {
assert.array(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
var inst = args.shift();
try {
var filters = common.objFromKeyValueArgs(args, {
validKeys: VALID_FILTERS,
disableDotted: true
});
} catch (e) {
cb(e);
return;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.listNics({id: inst}, function onNics(err, nics) {
if (err) {
cb(err);
return;
}
// do filtering
Object.keys(filters).forEach(function filterByKey(key) {
var val = filters[key];
nics = nics.filter(function filterByNic(nic) {
return nic[key] === val;
});
});
if (opts.json) {
common.jsonStream(nics);
} else {
nics.forEach(function onNic(nic) {
nic.network = nic.network.split('-')[0];
nic.ip = nic.ip + '/' + convertCidrSuffix(nic.netmask);
});
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_DEFAULT_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(nics, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
});
}
function convertCidrSuffix(netmask) {
var bitmask = netmask.split('.').map(function (octet) {
return (+octet).toString(2);
}).join('');
var i = 0;
for (i = 0; i < bitmask.length; i++) {
if (bitmask[i] === '0')
break;
}
return i;
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
do_list.help = [
'Show all NICs on an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Filters:',
' FIELD=<string> String filter. Supported fields: ip, mac, state,',
' network, netmask',
'',
'Filters are applied client-side (i.e. done by the triton command itself).'
].join('\n');
do_list.completionArgtypes = ['tritoninstance', 'none'];
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -0,0 +1,50 @@
/*
* 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 inst nic ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function NicCLI(top) {
this.top = top.top;
Cmdln.call(this, {
name: top.name + ' nic',
desc: 'List and manage instance NICs.',
helpSubcmds: [
'help',
'list',
'get',
'create',
'delete'
],
helpOpts: {
minHelpCol: 23
}
});
}
util.inherits(NicCLI, Cmdln);
NicCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
NicCLI.prototype.do_list = require('./do_list');
NicCLI.prototype.do_create = require('./do_create');
NicCLI.prototype.do_get = require('./do_get');
NicCLI.prototype.do_delete = require('./do_delete');
module.exports = NicCLI;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*
* `triton snapshot create ...`
*/
@ -133,7 +133,7 @@ do_create.help = [
'{{usage}}',
'',
'{{options}}',
'Snapshot do not work for instances of type "kvm".'
'Snapshots do not work for instances of type "bhyve" or "kvm".'
].join('\n');
do_create.completionArgtypes = ['tritoninstance', 'none'];

View File

@ -5,11 +5,12 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*
* `triton instance ssh ...`
*/
var assert = require('assert-plus');
var path = require('path');
var spawn = require('child_process').spawn;
var vasync = require('vasync');
@ -17,6 +18,30 @@ var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
/*
* The tag "tritoncli.ssh.ip" may be set to an IP address that belongs to the
* instance but which is not the primary IP. If set, we will use that IP
* address for the SSH connection instead of the primary IP.
*/
var TAG_SSH_IP = 'tritoncli.ssh.ip';
/*
* The tag "tritoncli.ssh.proxy" may be set to either the name or the UUID of
* another instance in this account. If set, we will use the "ProxyJump"
* feature of SSH to tunnel through the SSH server on that host. This is
* useful when exposing a single zone to the Internet while keeping the rest of
* your infrastructure on a private fabric.
*/
var TAG_SSH_PROXY = 'tritoncli.ssh.proxy';
/*
* The tag "tritoncli.ssh.proxyuser" may be set on the instance used as an SSH
* proxy. If set, we will use this value when making the proxy connection
* (i.e., it will be passed via the "ProxyJump" option). If not set, the
* default user selection behaviour applies.
*/
var TAG_SSH_PROXY_USER = 'tritoncli.ssh.proxyuser';
function do_ssh(subcmd, opts, args, callback) {
if (opts.help) {
@ -30,10 +55,12 @@ function do_ssh(subcmd, opts, args, callback) {
var id = args.shift();
var user;
var overrideUser = false;
var i = id.indexOf('@');
if (i >= 0) {
user = id.substr(0, i);
id = id.substr(i + 1);
overrideUser = true;
}
vasync.pipeline({arg: {cli: this.top}, funcs: [
@ -48,17 +75,112 @@ function do_ssh(subcmd, opts, args, callback) {
ctx.inst = inst;
ctx.ip = inst.primaryIp;
if (inst.tags && inst.tags[TAG_SSH_IP]) {
ctx.ip = inst.tags[TAG_SSH_IP];
if (!inst.ips || inst.ips.indexOf(ctx.ip) === -1) {
next(new Error('IP address ' + ctx.ip + ' not ' +
'attached to the instance'));
return;
}
} else {
ctx.ip = inst.primaryIp;
}
if (!ctx.ip) {
next(new Error('primaryIp not found for instance'));
next(new Error('IP address not found for instance'));
return;
}
next();
});
},
function getInstanceBastionIp(ctx, next) {
if (opts.no_proxy) {
setImmediate(next);
return;
}
if (!ctx.inst.tags || !ctx.inst.tags[TAG_SSH_PROXY]) {
setImmediate(next);
return;
}
ctx.cli.tritonapi.getInstance(ctx.inst.tags[TAG_SSH_PROXY],
function (err, proxy) {
if (err) {
next(err);
return;
}
if (proxy.tags && proxy.tags[TAG_SSH_IP]) {
ctx.proxyIp = proxy.tags[TAG_SSH_IP];
if (!proxy.ips || proxy.ips.indexOf(ctx.proxyIp) === -1) {
next(new Error('IP address ' + ctx.proxyIp + ' not ' +
'attached to the instance'));
return;
}
} else {
ctx.proxyIp = proxy.primaryIp;
}
ctx.proxyImage = proxy.image;
/*
* Selecting the right user to use for the proxy connection is
* somewhat nuanced, in order to allow for various useful
* configurations. We wish to enable the following cases:
*
* 1. The least sophisticated configuration; i.e., using two
* instances (the target instance and the proxy instnace)
* with the default "root" (or, e.g., "ubuntu") account
* and smartlogin or authorized_keys metadata for SSH key
* management.
*
* 2. The user has set up their own accounts (e.g., "roberta")
* in all of their instances and does their own SSH key
* management. They connect with:
*
* triton inst ssh roberta@instance
*
* In this case we will use "roberta" for both the proxy
* and the target instance. This means a user provided on
* the command line will override the per-image default
* user (e.g., "root" or "ubuntu") -- if the user wants to
* retain the default account for the proxy, they should
* use case 3 below.
*
* 3. The user has set up their own accounts in the target
* instance (e.g., "felicity"), but the proxy instance is
* using a single specific account that should be used by
* all users in the organisation (e.g., "partyline"). In
* this case, we want the user to be able to specify the
* global proxy account setting as a tag on the proxy
* instance, so that for:
*
* triton inst ssh felicity@instance
*
* ... we will use "-o ProxyJump partyline@proxy" but
* still use "felicity" for the target connection. This
* last case requires the proxy user tag (if set) to
* override a user provided on the command line.
*/
if (proxy.tags && proxy.tags[TAG_SSH_PROXY_USER]) {
ctx.proxyUser = proxy.tags[TAG_SSH_PROXY_USER];
}
if (!ctx.proxyIp) {
next(new Error('IP address not found for proxy instance'));
return;
}
next();
});
},
function getUser(ctx, next) {
if (user) {
if (overrideUser) {
assert.string(user, 'user');
next();
return;
}
@ -73,8 +195,8 @@ function do_ssh(subcmd, opts, args, callback) {
}
/*
* This is a convention as seen on Joyent's
* "ubuntu-certified" KVM images.
* This is a convention as seen on Joyent's "ubuntu-certified"
* KVM images.
*/
if (image.tags && image.tags.default_user) {
user = image.tags.default_user;
@ -86,9 +208,64 @@ function do_ssh(subcmd, opts, args, callback) {
});
},
function getBastionUser(ctx, next) {
if (!ctx.proxyImage || ctx.proxyUser) {
/*
* If there is no image for the proxy host, or an override user
* was already provided in the tags of the proxy instance
* itself, we don't need to look up the default user.
*/
next();
return;
}
if (overrideUser) {
/*
* A user was provided on the command line, but no user
* override tag was present on the proxy instance. To enable
* use case 2 (see comments above) we'll prefer this user over
* the image default.
*/
assert.string(user, 'user');
ctx.proxyUser = user;
next();
return;
}
ctx.cli.tritonapi.getImage({
name: ctx.proxyImage,
useCache: true
}, function (getImageErr, image) {
if (getImageErr) {
next(getImageErr);
return;
}
/*
* This is a convention as seen on Joyent's "ubuntu-certified"
* KVM images.
*/
assert.ok(!ctx.proxyUser, 'proxy user set twice');
if (image.tags && image.tags.default_user) {
ctx.proxyUser = image.tags.default_user;
} else {
ctx.proxyUser = 'root';
}
next();
});
},
function doSsh(ctx, next) {
args = ['-l', user, ctx.ip].concat(args);
if (ctx.proxyIp) {
assert.string(ctx.proxyUser, 'ctx.proxyUser');
args = [
'-o', 'ProxyJump=' + ctx.proxyUser + '@' + ctx.proxyIp
].concat(args);
}
/*
* By default we disable ControlMaster (aka mux, aka SSH
* connection multiplexing) because of
@ -133,6 +310,11 @@ do_ssh.options = [
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['no-proxy'],
type: 'bool',
help: 'Disable SSH proxy support (ignore "tritoncli.ssh.proxy" tag)'
}
];
do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]'];
@ -150,6 +332,26 @@ do_ssh.help = [
'If USER is not specified and the default_user tag is not set, the user',
'is assumed to be \"root\".',
'',
'The "tritoncli.ssh.proxy" tag on the target instance may be set to',
'the name or the UUID of another instance through which to proxy this',
'SSH connection. If set, the primary IP of the proxy instance will be',
'loaded and passed to SSH via the ProxyJump option. The --no-proxy',
'flag can be used to ignore the tag and force a direct connection.',
'',
'For example, to proxy connections to zone "narnia" through "wardrobe":',
' triton instance tag set narnia tritoncli.ssh.proxy=wardrobe',
'',
'The "tritoncli.ssh.ip" tag on the target instance may be set to the',
'IP address to use for SSH connections. This may be useful if the',
'primary IP address is not available for SSH connections. This address',
'must be set to one of the IP addresses attached to the instance.',
'',
'The "tritoncli.ssh.proxyuser" tag on the proxy instance may be set to',
'the user account that should be used for the proxy connection (i.e., via',
'the SSH ProxyJump option). This is useful when all users of the proxy',
'instance should use a special common account, and will override the USER',
'value (if one is provided) for the SSH connection to the target instance.',
'',
'There is a known issue with SSH connection multiplexing (a.k.a. ',
'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`',
'is spawned with options disabling ControlMaster. See ',

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2015 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*
* `triton instance ...`
*/
@ -45,10 +45,14 @@ function InstanceCLI(top) {
'enable-firewall',
'disable-firewall',
{ group: '' },
'enable-deletion-protection',
'disable-deletion-protection',
{ group: '' },
'ssh',
'ip',
'wait',
'audit',
'nic',
'snapshot',
'tag'
]
@ -77,10 +81,16 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall');
InstanceCLI.prototype.do_enable_deletion_protection =
require('./do_enable_deletion_protection');
InstanceCLI.prototype.do_disable_deletion_protection =
require('./do_disable_deletion_protection');
InstanceCLI.prototype.do_ssh = require('./do_ssh');
InstanceCLI.prototype.do_ip = require('./do_ip');
InstanceCLI.prototype.do_wait = require('./do_wait');
InstanceCLI.prototype.do_audit = require('./do_audit');
InstanceCLI.prototype.do_nic = require('./do_nic');
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
InstanceCLI.prototype.do_tag = require('./do_tag');

View File

@ -47,13 +47,7 @@ function do_add(subcmd, opts, args, cb) {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
ctx.data = stdin;
ctx.from = '<stdin>';
next();

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

@ -0,0 +1,273 @@
/*
* 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');
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.optionalArrayOfString(opts.resolver, 'opts.resolver');
assert.optionalArrayOfString(opts.route, 'opts.route');
assert.optionalBool(opts.no_nat, 'opts.no_nat');
assert.optionalBool(opts.json, 'opts.json');
assert.optionalBool(opts.help, 'opts.help');
assert.func(cb, 'cb');
var i;
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;
}
if (!opts.subnet) {
cb(new errors.UsageError('must specify --subnet (-s) option'));
return;
}
if (!opts.name) {
cb(new errors.UsageError('must specify --name (-n) option'));
return;
}
if (!opts.start_ip) {
cb(new errors.UsageError('must specify --start-ip (-S) option'));
return;
}
if (!opts.end_ip) {
cb(new errors.UsageError('must specify --end-ip (-E) option'));
return;
}
var createOpts = {
vlan_id: vlanId,
name: opts.name,
subnet: opts.subnet,
provision_start_ip: opts.start_ip,
provision_end_ip: opts.end_ip,
resolvers: [],
routes: {}
};
if (opts.resolver) {
for (i = 0; i < opts.resolver.length; i++) {
if (createOpts.resolvers.indexOf(opts.resolver[i]) === -1) {
createOpts.resolvers.push(opts.resolver[i]);
}
}
}
if (opts.route) {
for (i = 0; i < opts.route.length; i++) {
var m = opts.route[i].match(new RegExp('^([^=]+)=([^=]+)$'));
if (m === null) {
cb(new errors.UsageError('invalid route: ' + opts.route[i]));
return;
}
createOpts.routes[m[1]] = m[2];
}
}
if (opts.no_nat) {
createOpts.internet_nat = false;
}
if (opts.gateway) {
createOpts.gateway = opts.gateway;
} else {
if (!opts.no_nat) {
cb(new errors.UsageError('without a --gateway (-g), you must ' +
'specify --no-nat (-x)'));
return;
}
}
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.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.'
},
{
group: 'Create options'
},
{
names: ['name', 'n'],
type: 'string',
helpArg: 'NAME',
help: 'Name of the NETWORK.'
},
{
names: ['description', 'D'],
type: 'string',
helpArg: 'DESC',
help: 'Description of the NETWORK.'
},
{
group: ''
},
{
names: ['subnet', 's'],
type: 'string',
helpArg: 'SUBNET',
help: 'A CIDR string describing the NETWORK.'
},
{
names: ['start-ip', 'S', 'start_ip'],
type: 'string',
helpArg: 'START_IP',
help: 'First assignable IP address on NETWORK.'
},
{
names: ['end-ip', 'E', 'end_ip'],
type: 'string',
helpArg: 'END_IP',
help: 'Last assignable IP address on NETWORK.'
},
{
group: ''
},
{
names: ['gateway', 'g'],
type: 'string',
helpArg: 'IP',
help: 'Default gateway IP address.'
},
{
names: ['resolver', 'r'],
type: 'arrayOfString',
helpArg: 'RESOLVER',
help: 'DNS resolver IP address. Specify multiple -r options for ' +
'multiple resolvers.'
},
{
names: ['route', 'R'],
type: 'arrayOfString',
helpArg: 'SUBNET=IP',
help: [ 'Static route for network. Each route must include the',
'subnet (IP address with CIDR prefix length) and the router',
'address. Specify multiple -R options for multiple static',
'routes.' ].join(' ')
},
{
group: ''
},
{
names: ['no-nat', 'x', 'no_nat'],
type: 'bool',
helpArg: 'NO_NAT',
help: 'Disable creation of an Internet NAT zone on GATEWAY.'
},
{
group: 'Other options'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN'];
do_create.help = [
'Create a network on a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'',
'Examples:',
' Create the "accounting" network on VLAN 1000:',
' triton network create -n accounting --subnet 192.168.0.0/24 \\',
' --start-ip 192.168.0.1 --end-ip 192.168.0.254 --no-nat \\',
' 1000',
'',
' Create the "eng" network on VLAN 1001 with a pair of static routes:',
' triton network create -n eng -s 192.168.1.0/24 \\',
' -S 192.168.1.1 -E 192.168.1.249 --no-nat \\',
' --route 10.1.1.0/24=192.168.1.50 \\',
' --route 10.1.2.0/24=192.168.1.100 \\',
' 1001',
'',
' Create the "ops" network on VLAN 1002 with DNS resolvers and NAT:',
' triton network create -n ops -s 192.168.2.0/24 \\',
' -S 192.168.2.10 -E 192.168.2.249 \\',
' --resolver 8.8.8.8 --resolver 8.4.4.4 \\',
' --gateway 192.168.2.1 \\',
' 1002'
].join('\n');
do_create.helpOpts = {
helpCol: 16
};
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,
function searchNetworks(arg, next) {
self.top.tritonapi.cloudapi.listNetworks(function (err, networks) {
// since this command is also used by do_vlan/do_networks.js
if (opts.vlan_id) {
self.top.tritonapi.listFabricNetworks({
vlan_id: opts.vlan_id
}, listedNetworks);
} else {
self.top.tritonapi.cloudapi.listNetworks({}, listedNetworks);
}
function listedNetworks(err, networks) {
if (err) {
next(err);
return;
}
arg.networks = networks;
next();
});
}
},
function filterNetworks(arg, next) {

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',
'list',
'get',
'ip'
'ip',
'create',
'delete',
'get-default',
'set-default'
]
});
}
@ -47,6 +51,10 @@ NetworkCLI.prototype.init = function init(opts, args, cb) {
NetworkCLI.prototype.do_list = require('./do_list');
NetworkCLI.prototype.do_get = require('./do_get');
NetworkCLI.prototype.do_ip = require('./do_ip');
NetworkCLI.prototype.do_create = require('./do_create');
NetworkCLI.prototype.do_delete = require('./do_delete');
NetworkCLI.prototype.do_get_default = require('./do_get_default');
NetworkCLI.prototype.do_set_default = require('./do_set_default');
module.exports = NetworkCLI;

View File

@ -93,12 +93,8 @@ function _createProfile(opts, cb) {
next();
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {
@ -171,7 +167,7 @@ function _createProfile(opts, cb) {
defaults = ctx.copy;
delete defaults.name; // we don't copy a profile name
} else {
defaults.url = 'https://us-sw-1.api.joyent.com';
defaults.url = 'https://eu-ro-1.api.spearhead.cloud';
}
/*

View File

@ -23,7 +23,8 @@ function do_docker_setup(subcmd, opts, args, cb) {
cli: this.top,
name: profileName,
implicit: false,
yes: opts.yes
yes: opts.yes,
lifetime: opts.lifetime
}, cb);
}
@ -33,6 +34,11 @@ do_docker_setup.options = [
type: 'bool',
help: 'Show this help.'
},
{
names: ['lifetime', 't'],
type: 'number',
help: 'Lifetime of the generated docker certificate, in days'
},
{
names: ['yes', 'y'],
type: 'bool',
@ -56,7 +62,7 @@ do_docker_setup.help = [
'the DOCKER_HOST and related environment variables.',
'',
'After running this, you can setup your shell environment for `docker` via:',
' eval "$(triton env --docker)"',
' eval "$(spearhead env --docker)"',
'or the equivalent. See `spearhead env --help` for details.'
/* END JSSTYLED */
].join('\n');

View File

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

View File

@ -24,6 +24,7 @@ var rimraf = require('rimraf');
var semver = require('semver');
var sshpk = require('sshpk');
var mod_url = require('url');
var crypto = require('crypto');
var vasync = require('vasync');
var which = require('which');
var wordwrap = require('wordwrap')(78);
@ -128,7 +129,6 @@ function setCurrentProfile(opts, cb) {
});
}
/**
* Setup the given profile for Docker usage. This means checking the cloudapi
* has a Docker service (ListServices), finding the user's SSH *private* key,
@ -143,14 +143,21 @@ function setCurrentProfile(opts, cb) {
* implicit, we silently skip if ListServices shows no Docker service.
* - {Boolean} yes: Optional. Boolean indicating if confirmation prompts
* should be skipped, assuming a "yes" answer.
* - {Number} lifetime: Optional. Number of days to make the Docker
* certificate valid for. Defaults to 3650 (10 years).
*/
function profileDockerSetup(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.optionalBool(opts.implicit, 'opts.implicit');
assert.optionalBool(opts.yes, 'opts.yes');
assert.optionalNumber(opts.lifetime, 'opts.lifetime');
assert.func(cb, 'cb');
/* Default to a 10 year certificate. */
if (!opts.lifetime)
opts.lifetime = 3650;
var cli = opts.cli;
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
@ -165,13 +172,17 @@ function profileDockerSetup(opts, cb) {
function dockerKeyWarning(arg, next) {
console.log(wordwrap('WARNING: Docker uses authentication via ' +
'client TLS certificates that do not support encrypted ' +
'(passphrase protected) keys or SSH agents. If you continue, ' +
'this profile setup will attempt to write a copy of your ' +
'SSH private key formatted as an unencrypted TLS certificate ' +
'in "~/.spearhead/docker" for use by the Docker client.\n'));
'(passphrase protected) keys or SSH agents.\n'));
console.log(wordwrap('If you continue, this profile setup will ' +
'create a fresh private key to be written unencrypted to ' +
'disk in "~/.spearhead/docker" for use by the Docker client. ' +
'This key will be useable only for Docker.\n'));
if (yes) {
next();
return;
} else {
console.log(wordwrap('If you do not specifically want to use ' +
'Docker, you can answer "no" here.\n'));
}
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
if (answer !== 'y') {
@ -311,79 +322,143 @@ function profileDockerSetup(opts, cb) {
});
},
/*
* We need the private key to format as a client cert. If this profile's
* key was found in the SSH agent (and by default it prefers to take
* it from there), then we can't use `tritonapi.keyPair`, because
* the SSH agent protocol will not allow us access to the private key
* data (by design).
*
* As a fallback we'll look (via KeyRing) for a local copy of the
* private key to use, and then unlock it if necessary.
*/
function getPrivKey(arg, next) {
// If the key pair already works, then use that...
try {
arg.privKey = tritonapi.keyPair.getPrivateKey();
next();
return;
} catch (_) {
// ... else fall through.
}
function getSigningKey(arg, next) {
var kr = new auth.KeyRing();
var profileFp = sshpk.parseFingerprint(tritonapi.profile.keyId);
kr.find(profileFp, function (findErr, keyPairs) {
var profileFp = sshpk.parseFingerprint(profile.keyId);
kr.findSigningKeyPair(profileFp,
function unlockAndStash(findErr, keyPair) {
if (findErr) {
next(findErr);
return;
}
/*
* If our keyId was found, and with the 'homedir' plugin, then
* we should have access to the private key (modulo unlocking).
*/
var homedirKeyPair;
for (var i = 0; i < keyPairs.length; i++) {
if (keyPairs[i].plugin === 'homedir') {
homedirKeyPair = keyPairs[i];
break;
}
}
if (homedirKeyPair) {
common.promptPassphraseUnlockKey({
// Fake the `tritonapi` object, only `.keyPair` is used.
tritonapi: {keyPair: homedirKeyPair}
}, function (unlockErr) {
if (unlockErr) {
next(unlockErr);
return;
}
try {
arg.privKey = homedirKeyPair.getPrivateKey();
} catch (homedirErr) {
next(new errors.SetupError(homedirErr, format(
'could not obtain SSH private key for keyId ' +
'"%s" to create Docker certificate',
profile.keyId)));
return;
}
next();
});
} else {
next(new errors.SetupError(format('could not obtain SSH ' +
'private key for keyId "%s" to create Docker ' +
'certificate', profile.keyId)));
arg.signKeyPair = keyPair;
if (!keyPair.isLocked()) {
next();
return;
}
common.promptPassphraseUnlockKey({
/* Fake the `tritonapi` object, only `.keyPair` is used. */
tritonapi: { keyPair: keyPair }
}, next);
});
},
function generateAndSignCert(arg, next) {
var key = arg.signKeyPair;
var pubKey = key.getPublicKey();
function genClientCert_dir(arg, next) {
/*
* There isn't a particular reason this has to be ECDSA, but
* Docker supports it, and ECDSA keys are much easier to
* generate from inside node than RSA ones (since sshpk will
* do them for us instead of us shelling out and mucking with
* temporary files).
*/
arg.privKey = sshpk.generatePrivateKey('ecdsa');
var id = sshpk.identityFromDN('CN=' + profile.account);
var parentId = sshpk.identityFromDN('CN=' +
pubKey.fingerprint('md5').toString('base64'));
var serial = crypto.randomBytes(8);
/*
* Backdate the certificate by 5 minutes to account for clock
* sync -- we only allow 5 mins drift in cloudapi generally so
* using the same amount here seems fine.
*/
var validFrom = new Date();
validFrom.setTime(validFrom.getTime() - 300*1000);
var validUntil = new Date();
validUntil.setTime(validFrom.getTime() +
24*3600*1000*opts.lifetime);
/*
* Generate it self-signed for now -- we will clear this
* signature out and replace it with the real one below.
*/
var cert = sshpk.createCertificate(id, arg.privKey, parentId,
arg.privKey, { validFrom: validFrom, validUntil: validUntil,
purposes: ['clientAuth', 'joyentDocker'], serial: serial });
var algo = pubKey.type + '-' + pubKey.defaultHashAlgorithm();
/*
* This code is using private API in sshpk because there is
* no public API as of 1.14.x for async signing of certificates.
*
* If the sshpk version in package.json is updated (even a
* patch bump) this code could break! This will be fixed up
* eventually, but for now we just have to be careful.
*/
var x509 = require('sshpk/lib/formats/x509');
cert.signatures = {};
cert.signatures.x509 = {};
cert.signatures.x509.algo = algo;
var signer = key.createSign({
user: profile.account,
algorithm: algo
});
/*
* The smartdc-auth KeyPair signer produces an object with
* strings on it intended for http-signature instead of just a
* Signature instance (which is what the x509 format module
* expects). We wrap it up here to convert it.
*/
var signerConv = function (buf, ccb) {
signer(buf, function convertSignature(signErr, sigData) {
if (signErr) {
ccb(signErr);
return;
}
var algparts = sigData.algorithm.split('-');
var sig = sshpk.parseSignature(sigData.signature,
algparts[0], 'asn1');
sig.hashAlgorithm = algparts[1];
sig.curve = pubKey.curve;
ccb(null, sig);
});
};
/*
* Sign a "test" string first to double-check the hash algo
* it's going to use. The SSH agent may not support SHA256
* signatures, for example, and we will only find out by
* testing like this.
*/
signer('test', function afterTestSig(testErr, testSigData) {
if (testErr) {
next(new errors.SetupError(testErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.signatures.x509.algo = testSigData.algorithm;
x509.signAsync(cert, signerConv,
function afterCertSign(signErr) {
if (signErr) {
next(new errors.SetupError(signErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.issuerKey = undefined;
/* Double-check that it came out ok. */
assert.ok(cert.isSignedByKey(pubKey));
arg.cert = cert;
next();
});
});
},
function makeClientCertDir(arg, next) {
arg.dockerCertPath = path.resolve(cli.configDir,
'docker', common.profileSlug(profile));
mkdirp(arg.dockerCertPath, next);
},
function genClientCert_key(arg, next) {
function writeClientCertKey(arg, next) {
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
var data = arg.privKey.toBuffer('pkcs1');
fs.writeFile(arg.keyPath, data, function (err) {
@ -395,12 +470,9 @@ function profileDockerSetup(opts, cb) {
}
});
},
function genClientCert_cert(arg, next) {
function writeClientCert(arg, next) {
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
var id = sshpk.identityFromDN('CN=' + profile.account);
var cert = sshpk.createSelfSignedCertificate(id, arg.privKey);
var data = cert.toBuffer('pem');
var data = arg.cert.toBuffer('pem');
fs.writeFile(arg.certPath, data, function (err) {
if (err) {

View File

@ -125,12 +125,8 @@ function _addUserKey(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
ctx.data = stdin;
ctx.from = '<stdin>';
next();

View File

@ -291,12 +291,8 @@ function _addPolicy(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -287,12 +287,8 @@ function _addRole(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

View File

@ -282,12 +282,8 @@ function _addUser(opts, cb) {
if (opts.file !== '-') {
return next();
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
common.readStdin(function gotStdin(stdin) {
try {
data = JSON.parse(stdin);
} catch (err) {

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

@ -0,0 +1,140 @@
/*
* 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 vlan create ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var jsprim = require('jsprim');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalString(opts.name, 'opts.name');
assert.optionalString(opts.description, 'opts.description');
assert.optionalBool(opts.json, 'opts.json');
assert.optionalBool(opts.help, 'opts.help');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing 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;
}
if (!opts.name) {
cb(new errors.UsageError('must provide a --name (-n)'));
return;
}
var createOpts = {
vlan_id: vlanId,
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.'
},
{
group: 'Create options'
},
{
names: ['name', 'n'],
type: 'string',
helpArg: 'NAME',
help: 'Name of the VLAN.'
},
{
names: ['description', 'D'],
type: 'string',
helpArg: 'DESC',
help: 'Description of the VLAN.'
},
{
group: 'Other options'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
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: 16
};
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

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2019 Joyent, Inc.
*/
/* BEGIN JSSTYLED */
@ -133,7 +133,7 @@ var errors = require('./errors');
// ---- globals
var CLOUDAPI_ACCEPT_VERSION = '~8||~7';
var CLOUDAPI_ACCEPT_VERSION = '~9||~8';
@ -233,10 +233,10 @@ function _stepPkgId(arg, next) {
/**
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.image`
* image name, shortid, or uuid, and determines the image id (setting it
* as arg.imgId).
* image name, shortid, or uuid, and determines the image object (setting it
* as arg.img).
*/
function _stepImgId(arg, next) {
function _stepImg(arg, next) {
assert.object(arg.client, 'arg.client');
assert.string(arg.image, 'arg.image');
@ -244,7 +244,7 @@ function _stepImgId(arg, next) {
if (err) {
next(err);
} else {
arg.imgId = img.id;
arg.img = img;
next();
}
});
@ -282,7 +282,9 @@ function _stepFwRuleId(arg, next) {
* and determines the network id (setting it as `arg.netId`).
*/
function _stepNetId(arg, next) {
assert.object(arg, 'arg');
assert.object(arg.client, 'arg.client');
assert.func(next, 'next');
var id = arg.network || arg.id;
assert.string(id, 'arg.network || arg.id');
@ -291,7 +293,7 @@ function _stepNetId(arg, next) {
arg.netId = id;
next();
} else {
arg.client.getNetwork(id, function (err, net) {
arg.client.getNetwork(id, function onGet(err, net) {
if (err) {
next(err);
} else {
@ -302,6 +304,98 @@ function _stepNetId(arg, next) {
}
}
/**
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id` and
* optionally a `arg.vlan_id`, where `arg.id` is a network name, shortid or
* uuid, and `arg.vlan_id` is a VLAN's id or name. Sets the network id as
* `arg.netId` and the VLAN id as `arg.vlanId`.
*/
function _stepFabricNetId(arg, next) {
assert.object(arg, 'arg');
assert.object(arg.client, 'arg.client');
assert.string(arg.id, 'arg.id');
assert.func(next, 'next');
var id = arg.id;
var vlanId = arg.vlan_id;
var vlanIdType = typeof (vlanId);
if (common.isUUID(id) && vlanIdType === 'number') {
arg.netId = id;
arg.vlanId = vlanId;
next();
return;
}
arg.client.getNetwork(id, function onGetNetwork(err, net) {
if (err) {
next(err);
return;
}
if (vlanIdType === 'number') {
assert.equal(net.vlan_id, vlanId, 'VLAN belongs to network');
}
if (vlanIdType === 'number' || vlanIdType === 'undefined') {
arg.netId = net.id;
arg.vlanId = net.vlan_id;
next();
return;
}
// at this point the only type left we support are strings
assert.string(vlanId, 'arg.vlan_id');
arg.client.getFabricVlan(vlanId, function onGetFabric(err2, vlan) {
if (err2) {
next(err2);
return;
}
assert.equal(net.vlan_id, vlan.vlan_id, 'VLAN belongs to network');
arg.netId = net.id;
arg.vlanId = net.vlan_id;
next();
});
});
}
/**
* A function appropriate for `vasync.pipeline` funcs that takes a
* `arg.vlan_id`, where that is either a VLAN's id or name. Sets the
* VLAN id as `arg.vlanId`.
*/
function _stepFabricVlanId(arg, next) {
assert.object(arg, 'arg');
assert.object(arg.client, 'arg.client');
assert.ok(typeof (arg.vlan_id) === 'string' ||
typeof (arg.vlan_id) === 'number', 'arg.vlan_id');
assert.func(next, 'next');
var vlanId = arg.vlan_id;
if (typeof (vlanId) === 'number') {
arg.vlanId = vlanId;
next();
return;
}
arg.client.getFabricVlan(vlanId, function onGet(err, vlan) {
if (err) {
next(err);
return;
}
arg.vlanId = vlan.vlan_id;
next();
});
}
//---- TritonApi class
/**
@ -399,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
? true : !profile.insecure);
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
var opts = {
self._cloudapiOpts = {
url: profile.url,
account: profile.actAsAccount || profile.account,
principal: {
@ -415,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
if (profile.privKey) {
var key = sshpk.parsePrivateKey(profile.privKey);
this.keyPair =
opts.principal.keyPair =
self._cloudapiOpts.principal.keyPair =
auth.KeyPair.fromPrivateKey(key);
this.cloudapi = cloudapi.createClient(opts);
this.cloudapi = cloudapi.createClient(self._cloudapiOpts);
cb(null);
} else {
var kr = new auth.KeyRing();
@ -427,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
cb(err);
return;
}
self.keyPair = opts.principal.keyPair = kp;
self.cloudapi = cloudapi.createClient(opts);
self.keyPair = self._cloudapiOpts.principal.keyPair = kp;
self.cloudapi = cloudapi.createClient(self._cloudapiOpts);
cb(null);
});
}
@ -599,6 +693,10 @@ TritonApi.prototype.listImages = function listImages(opts, cb) {
*
* If there is more than one image with that name, then the latest
* (by published_at) is returned.
*
* @param {Boolean} opts.excludeInactive - Exclude inactive images when
* matching. By default inactive images are included. This param is *not*
* used when a full image ID (a UUID) is given.
*/
TritonApi.prototype.getImage = function getImage(opts, cb) {
var self = this;
@ -606,10 +704,13 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
opts = {name: opts};
assert.object(opts, 'opts');
assert.string(opts.name, 'opts.name');
assert.optionalBool(opts.excludeInactive, 'opts.excludeInactive');
assert.optionalBool(opts.useCache, 'opts.useCache');
assert.func(cb, 'cb');
var excludeInactive = Boolean(opts.excludeInactive);
var img;
if (common.isUUID(opts.name)) {
vasync.pipeline({funcs: [
function tryCache(_, next) {
@ -661,10 +762,8 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
var version = s[1];
var nameSelector;
var listOpts = {
// Explicitly include inactive images.
state: 'all'
};
var listOpts = {};
listOpts.state = (excludeInactive ? 'active' : 'all');
if (version) {
nameSelector = name + '@' + version;
listOpts.name = name;
@ -745,10 +844,10 @@ TritonApi.prototype.exportImage = function exportImage(opts, cb)
};
vasync.pipeline({arg: arg, funcs: [
_stepImgId,
_stepImg,
function cloudApiExportImage(ctx, next) {
self.cloudapi.exportImage({
id: ctx.imgId, manta_path: opts.manta_path },
id: ctx.img.id, manta_path: opts.manta_path },
function (err, exportInfo_, res_) {
if (err) {
next(err);
@ -769,6 +868,227 @@ TritonApi.prototype.exportImage = function exportImage(opts, cb)
});
};
/**
* Share an image with another account.
*
* @param {Object} opts
* - {String} image The image UUID, name, or short ID. Required.
* - {String} account The account UUID. Required.
* @param {Function} cb `function (err, img)`
* On failure `err` is an error instance, else it is null.
* On success: `img` is an image object.
*/
TritonApi.prototype.shareImage = function shareImage(opts, cb)
{
var self = this;
assert.object(opts, 'opts');
assert.string(opts.image, 'opts.image');
assert.string(opts.account, 'opts.account');
assert.func(cb, 'cb');
var arg = {
image: opts.image,
client: self
};
var res;
vasync.pipeline({arg: arg, funcs: [
_stepImg,
function validateAcl(ctx, next) {
ctx.acl = ctx.img.acl && ctx.img.acl.slice() || [];
if (ctx.acl.indexOf(opts.account) === -1) {
ctx.acl.push(opts.account);
}
next();
},
function cloudApiShareImage(ctx, next) {
self.cloudapi.updateImage({id: ctx.img.id, fields: {acl: ctx.acl}},
function _updateImageCb(err, img) {
res = img;
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* Unshare an image with another account.
*
* @param {Object} opts
* - {String} image The image UUID, name, or short ID. Required.
* - {String} account The account UUID. Required.
* @param {Function} cb `function (err, img)`
* On failure `err` is an error instance, else it is null.
* On success: `img` is an image object.
*/
TritonApi.prototype.unshareImage = function unshareImage(opts, cb)
{
var self = this;
assert.object(opts, 'opts');
assert.string(opts.image, 'opts.image');
assert.string(opts.account, 'opts.account');
assert.func(cb, 'cb');
var arg = {
image: opts.image,
client: self
};
var res;
vasync.pipeline({arg: arg, funcs: [
_stepImg,
function validateAcl(ctx, next) {
assert.object(ctx.img, 'img');
ctx.acl = ctx.img.acl && ctx.img.acl.slice() || [];
var aclIdx = ctx.acl.indexOf(opts.account);
if (aclIdx === -1) {
cb(new errors.TritonError(format('image is not shared with %s',
opts.account)));
return;
}
ctx.acl.splice(aclIdx, 1);
next();
},
function cloudApiUnshareImage(ctx, next) {
self.cloudapi.updateImage({id: ctx.img.id, fields: {acl: ctx.acl}},
function _updateImageCb(err, img) {
res = img;
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* 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.
*
@ -839,7 +1159,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
assert.func(cb, 'cb');
if (common.isUUID(name)) {
this.cloudapi.getNetwork(name, function (err, net) {
this.cloudapi.getNetwork(name, function onGet(err, net) {
if (err) {
if (err.restCode === 'ResourceNotFound') {
// Wrap with *our* ResourceNotFound for exitStatus=3.
@ -852,7 +1172,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
}
});
} else {
this.cloudapi.listNetworks(function (err, nets) {
this.cloudapi.listNetworks({}, function onList(err, nets) {
if (err) {
return cb(err);
}
@ -889,6 +1209,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.
*
@ -1028,6 +1414,8 @@ TritonApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) {
* - {Array} fields: Optional. An array of instance field names that are
* wanted by the caller. This *can* allow the implementation to avoid
* extra API calls. E.g. `['id', 'name']`.
* - {Boolean} credentials: Optional. Set to true to include generated
* credentials for this instance in `inst.metadata.credentials`.
* @param {Function} cb `function (err, inst, res)`
* Note that deleted instances will result in `err` being a
* `InstanceDeletedError` and `inst` being defined. On success, `res` is
@ -1042,6 +1430,7 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.optionalArrayOfString(opts.fields, 'opts.fields');
assert.optionalBool(opts.credentials, 'opts.credentials');
assert.func(cb, 'cb');
/*
@ -1081,7 +1470,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
return next();
}
}
self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
self.cloudapi.getMachine({
id: uuid,
credentials: opts.credentials
}, function onMachine(err, inst_, res_) {
res = res_;
inst = inst_;
err = errFromGetMachineErr(err);
@ -1169,7 +1561,10 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
}
var uuid = instFromList.id;
self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
self.cloudapi.getMachine({
id: uuid,
credentials: opts.credentials
}, function onMachine(err, inst_, res_) {
res = res_;
inst = inst_;
err = errFromGetMachineErr(err);
@ -1269,6 +1664,92 @@ function disableInstanceFirewall(opts, cb) {
};
// ---- instance enable/disable deletion protection
/**
* Enable deletion protection on an instance.
*
* @param {Object} opts
* - {String} id: Required. The instance ID, name, or short ID.
* @param {Function} callback `function (err, fauxInst, res)`
* On failure `err` is an error instance, else it is null.
* On success: `fauxInst` is an object with just the instance id,
* `{id: <instance UUID>}` and `res` is the CloudAPI
* `EnableMachineDeletionProtection` response.
* The API call does not return the instance/machine object, hence we
* are limited to just the id for `fauxInst`.
*/
TritonApi.prototype.enableInstanceDeletionProtection =
function enableInstanceDeletionProtection(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var res;
var fauxInst;
function enableDeletionProtection(arg, next) {
fauxInst = {id: arg.instId};
self.cloudapi.enableMachineDeletionProtection(arg.instId,
function enableCb(err, _, _res) {
res = _res;
next(err);
});
}
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
enableDeletionProtection
]}, function vasyncCb(err) {
cb(err, fauxInst, res);
});
};
/**
* Disable deletion protection on an instance.
*
* @param {Object} opts
* - {String} id: Required. The instance ID, name, or short ID.
* @param {Function} callback `function (err, fauxInst, res)`
* On failure `err` is an error instance, else it is null.
* On success: `fauxInst` is an object with just the instance id,
* `{id: <instance UUID>}` and `res` is the CloudAPI
* `DisableMachineDeletionProtectiomn` response.
* The API call does not return the instance/machine object, hence we
* are limited to just the id for `fauxInst`.
*/
TritonApi.prototype.disableInstanceDeletionProtection =
function disableInstanceDeletionProtection(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var res;
var fauxInst;
function disableDeletionProtection(arg, next) {
fauxInst = {id: arg.instId};
self.cloudapi.disableMachineDeletionProtection(arg.instId,
function (err, _, _res) {
res = _res;
next(err);
});
}
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
disableDeletionProtection
]}, function vasyncCb(err) {
cb(err, fauxInst, res);
});
};
// ---- instance snapshots
/**
@ -1934,6 +2415,231 @@ function deleteAllInstanceTags(opts, cb) {
};
// ---- nics
/**
* Add a NIC on a network to an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {Object|String} network: The network object or ID, name, or short ID.
* Required.
* @param {Function} callback `function (err, nic, res)`
*/
TritonApi.prototype.addNic =
function addNic(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network');
assert.func(cb, 'cb');
var self = this;
var nic;
var pipeline = [];
var res;
switch (typeof (opts.network)) {
case 'string':
pipeline.push(_stepNetId);
break;
case 'object':
break;
default:
throw new Error('unexpected opts.network: ' + opts.network);
}
pipeline.push(_stepInstId);
pipeline.push(function createNic(arg, next) {
self.cloudapi.addNic({
id: arg.instId,
network: arg.netId || arg.network
}, function onCreateNic(err, _nic, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
res.netId = arg.netId; // ditto
nic = _nic;
next(err);
});
});
var pipelineArg = {
client: self,
id: opts.id,
network: opts.network
};
vasync.pipeline({
arg: pipelineArg,
funcs: pipeline
}, function (err) {
cb(err, nic, res);
});
};
/**
* List an instance's NICs.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* @param {Function} callback `function (err, nics, res)`
*/
TritonApi.prototype.listNics =
function listNics(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.func(cb, 'cb');
var self = this;
var nics;
var res;
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
_stepInstId,
function list(arg, next) {
self.cloudapi.listNics({
id: arg.instId
}, function onList(err, _nics, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
nics = _nics;
next(err);
});
}
]}, function (err) {
cb(err, nics, res);
});
};
/**
* Get a NIC belonging to an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {String} mac: The NIC's MAC address. Required.
* @param {Function} callback `function (err, nic, res)`
*/
TritonApi.prototype.getNic =
function getNic(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var self = this;
var res;
var nic;
vasync.pipeline({arg: {client: self, id: opts.id, mac: opts.mac}, funcs: [
_stepInstId,
function get(arg, next) {
self.cloudapi.getNic({
id: arg.instId,
mac: arg.mac
}, function onGet(err, _nic, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
nic = _nic;
next(err);
});
}
]}, function (err) {
cb(err, nic, res);
});
};
/**
* Remove a NIC from an instance.
*
* @param {Object} opts
* - {String} id: The instance ID, name, or short ID. Required.
* - {String} mac: The NIC's MAC address. Required.
* @param {Function} callback `function (err, res)`
*
*/
TritonApi.prototype.removeNic =
function removeNic(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var self = this;
var res;
vasync.pipeline({arg: {client: self, id: opts.id, mac: opts.mac}, funcs: [
_stepInstId,
function deleteNic(arg, next) {
self.cloudapi.removeNic({
id: arg.instId,
mac: arg.mac
}, function onRemove(err, _res) {
res = _res;
res.instId = arg.instId; // gross hack, in case caller needs it
next(err);
});
}
]}, function (err) {
cb(err, res);
});
};
/**
* Wrapper for cloudapi2's waitForNicStates that will first translate
* opts.id into the proper uuid from shortid/name.
*
* @param {Object} options
* - {String} id {required} machine id
* - {String} mac {required} mac for new nic
* - {Array of String} states - desired state
* @param {Function} callback of the form f(err, nic, res).
*/
TritonApi.prototype.waitForNicStates = function waitForNicStates(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.arrayOfString(opts.states, 'opts.states');
var self = this;
var nic, res;
function waitForNic(arg, next) {
var _opts = {
id: arg.instId,
mac: arg.mac,
states: arg.states
};
self.cloudapi.waitForNicStates(_opts,
function onWaitForNicState(err, _nic, _res) {
res = _res;
nic = _nic;
next(err);
});
}
var pipelineArgs = {
client: self,
id: opts.id,
mac: opts.mac,
states: opts.states
};
vasync.pipeline({
arg: pipelineArgs,
funcs: [
_stepInstId,
waitForNic
]
}, function onWaitForNicPipeline(err) {
cb(err, nic, res);
});
};
// ---- Firewall Rules
/**
@ -2221,6 +2927,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
/**

View File

@ -1,7 +1,7 @@
{
"name": "spearhead",
"description": "Spearhead Cloud CLI and client (https://spearhead.cloud)",
"version": "5.6.4",
"version": "7.0.0",
"author": "Spearhead Systems (spearhead.systems)",
"homepage": "https://code.spearhead.cloud/Spearhead/node-spearhead",
"dependencies": {
@ -21,9 +21,9 @@
"restify-errors": "3.0.0",
"rimraf": "2.4.4",
"semver": "5.1.0",
"smartdc-auth": "2.5.6",
"sshpk": "1.10.2",
"sshpk-agent": "1.4.2",
"smartdc-auth": "2.5.7",
"sshpk": "1.14.1",
"sshpk-agent": "1.7.0",
"strsplit": "1.0.0",
"tabula": "1.10.0",
"vasync": "1.6.3",
@ -33,7 +33,8 @@
},
"devDependencies": {
"tape": "4.2.0",
"tap-summary": "3.0.2"
"tap-summary": "3.0.2",
"uuid": "3.2.1"
},
"main": "./lib",
"scripts": {

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*/
/*
@ -18,6 +18,8 @@ var test = require('tape');
// --- Globals
var NET_NAME = 'node-triton-testnet967';
var CLIENT;
var NET;
@ -33,11 +35,14 @@ test('TritonApi networks', function (tt) {
});
});
tt.test(' cleanup: rm network ' + NET_NAME + ' if exists', function (t) {
CLIENT.deleteFabricNetwork({id: NET_NAME}, function () {
t.end();
});
});
tt.test(' setup: net', function (t) {
var opts = {
account: CLIENT.profile.account
};
CLIENT.cloudapi.listNetworks(opts, function (err, nets) {
CLIENT.cloudapi.listNetworks({}, function (err, nets) {
if (h.ifErr(t, err))
return t.end();
@ -78,6 +83,61 @@ test('TritonApi networks', function (tt) {
});
tt.test(' TritonApi deleteFabricNetwork', function (t) {
function check(genId, idType, vlanId, cb) {
CLIENT.cloudapi.createFabricNetwork({
name: NET_NAME,
subnet: '192.168.97.0/24',
provision_start_ip: '192.168.97.1',
provision_end_ip: '192.168.97.254',
vlan_id: vlanId
}, function onCreate(err, net) {
if (h.ifErr(t, err, 'Error creating network')) {
t.end();
return;
}
var id = genId(net);
CLIENT.deleteFabricNetwork({id: id}, function onDelete(err2) {
if (h.ifErr(t, err, 'Error deleting net by ' + idType)) {
t.end();
return;
}
CLIENT.cloudapi.getNetwork(net.id, function onGet(err3) {
t.ok(err3, 'Network should be gone');
cb();
});
});
});
}
// get a VLAN, then create and delete a set of fabrics to check it's
// possible to delete by id, shortId and name
CLIENT.cloudapi.listFabricVlans({}, function onList(err, vlans) {
if (vlans.length === 0) {
t.end();
return;
}
function getId(net) { return net.id; }
function getName(net) { return net.name; }
function getShort(net) { return net.id.split('-')[0]; }
var vlanId = +vlans[0].vlan_id;
check(getId, 'id', vlanId, function onId() {
check(getName, 'name', vlanId, function onName() {
check(getShort, 'shortId', vlanId, function onShort() {
t.end();
});
});
});
});
});
tt.test(' teardown: client', function (t) {
CLIENT.close();
t.end();

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 NIC-related APIs as a module.
*/
var h = require('./helpers');
var test = require('tape');
// --- Globals
var CLIENT;
var INST;
var NIC;
// --- Tests
test('TritonApi nics', function (tt) {
tt.test(' setup', function (t) {
h.createClient(function onCreate(err, client_) {
t.error(err);
CLIENT = client_;
t.end();
});
});
tt.test(' setup: inst', function (t) {
CLIENT.cloudapi.listMachines({}, function onList(err, vms) {
if (vms.length === 0) {
t.end();
return;
}
t.ok(Array.isArray(vms), 'vms array');
INST = vms[0];
t.end();
});
});
tt.test(' TritonApi listNics', function (t) {
if (!INST) {
t.end();
return;
}
function check(val, valName, next) {
CLIENT.listNics({id: val}, function onList(err, nics) {
if (h.ifErr(t, err, 'no err ' + valName)) {
t.end();
return;
}
t.ok(Array.isArray(nics), 'nics array');
NIC = nics[0];
next();
});
}
var shortId = INST.id.split('-')[0];
check(INST.id, 'id', function doId() {
check(INST.name, 'name', function doName() {
check(shortId, 'shortId', function doShort() {
t.end();
});
});
});
});
tt.test(' TritonApi getNic', function (t) {
if (!NIC) {
t.end();
return;
}
function check(inst, mac, instValName, next) {
CLIENT.getNic({id: inst, mac: mac}, function onGet(err, nic) {
if (h.ifErr(t, err, 'no err for ' + instValName)) {
t.end();
return;
}
t.deepEqual(nic, NIC, instValName);
next();
});
}
var shortId = INST.id.split('-')[0];
check(INST.id, NIC.mac, 'id', function doId() {
check(INST.name, NIC.mac, 'name', function doName() {
check(shortId, NIC.mac, 'shortId', function doShort() {
t.end();
});
});
});
});
tt.test(' teardown: client', function (t) {
CLIENT.close();
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

@ -81,7 +81,8 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
var db0Alias = ALIAS_PREFIX + '-db0';
var db0;
tt.test(' setup: triton create -n db0', function (t) {
var argv = ['create', '-wj', '-n', db0Alias, imgId, pkgId];
var argv = ['create', '-wj', '-n', db0Alias, '-t', 'role=database',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db0 = lines[1];
@ -92,9 +93,9 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
// Test db1 being put on same server as db0.
var db1Alias = ALIAS_PREFIX + '-db1';
var db1;
tt.test(' setup: triton create -n db1 -a db0', function (t) {
var argv = ['create', '-wj', '-n', db1Alias, '-a', db0Alias,
imgId, pkgId];
tt.test(' triton create -n db1 -a instance==db0', function (t) {
var argv = ['create', '-wj', '-n', db1Alias, '-a',
'instance==' + db0Alias, imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db1 = lines[1];
@ -105,11 +106,12 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
});
});
// Test db2 being put on server *other* than db0.
// Test db2 being put on a server without a db.
var db2Alias = ALIAS_PREFIX + '-db2';
var db2;
tt.test(' setup: triton create -n db2 -a \'inst!=db0\'', function (t) {
var argv = ['create', '-wj', '-n', db2Alias, '-a', 'inst!='+db0Alias,
tt.test(' triton create -n db2 -a \'instance!=db*\'', function (t) {
var argv = ['create', '-wj', '-n', db2Alias, '-a',
'instance!=' + ALIAS_PREFIX + '-db*',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
@ -121,11 +123,45 @@ test('affinity (triton create -a RULE ...)', testOpts, function (tt) {
});
});
// Test db3 being put on server *other* than db0.
var db3Alias = ALIAS_PREFIX + '-db3';
var db3;
tt.test(' triton create -n db3 -a \'instance!=db0\'', function (t) {
var argv = ['create', '-wj', '-n', db3Alias, '-a',
'instance!='+db0Alias, imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db3 = lines[1];
t.notEqual(db0.compute_node, db3.compute_node,
format('inst %s landed on different CN (%s) as inst %s (%s)',
db3Alias, db3.compute_node, db0Alias, db0.compute_node));
t.end();
});
});
// Test db4 being put on server *other* than db0 (due ot db0's tag).
var db4Alias = ALIAS_PREFIX + '-db4';
var db4;
tt.test(' triton create -n db4 -a \'role!=database\'', function (t) {
var argv = ['create', '-wj', '-n', db4Alias, '-a', 'role!=database',
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
db4 = lines[1];
t.notEqual(db0.compute_node, db4.compute_node,
format('inst %s landed on different CN (%s) as inst %s (%s)',
db4Alias, db4.compute_node, db0Alias, db0.compute_node));
t.end();
});
});
// Remove instances. Add a test timeout, because '-w' on delete doesn't
// have a way to know if the attempt failed or if it is just taking a
// really long time.
tt.test(' cleanup: triton rm', {timeout: 10 * 60 * 1000}, function (t) {
h.safeTriton(t, ['rm', '-w', db0.id, db1.id, db2.id], function () {
h.safeTriton(t, ['rm', '-w', db0.id, db1.id, db2.id, db3.id, db4.id],
function () {
t.end();
});
});

View File

@ -0,0 +1,191 @@
/*
* 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.
*/
/*
* Integration tests for `triton instance enable-deletion-protection ...` and
* `triton instance disable-deletion-protection ...`
*/
var h = require('./helpers');
var f = require('util').format;
var os = require('os');
var test = require('tape');
// --- Globals
var INST_ALIAS = f('nodetritontest-deletion-protection-%s', os.hostname());
var INST;
var OPTS = {
skip: !h.CONFIG.allowWriteActions
};
// --- Helpers
function cleanup(t) {
var cmd = 'instance disable-deletion-protection ' + INST_ALIAS + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (err)
return t.end();
h.deleteTestInst(t, INST_ALIAS, function (err2) {
t.ifErr(err2, 'delete inst err');
t.end();
});
});
}
// --- Tests
if (OPTS.skip) {
console.error('** skipping %s tests', __filename);
console.error('** set "allowWriteActions" in test config to enable');
}
test('triton instance', OPTS, function (tt) {
h.printConfig(tt);
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, cleanup);
tt.test(' triton create --deletion-protection', function (t) {
h.createTestInst(t, INST_ALIAS, {
extraFlags: ['--deletion-protection']
}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();
INST = instId;
h.triton('instance get -j ' + INST, function (err3, stdout) {
if (h.ifErr(t, err3, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout);
t.ok(inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' attempt to delete deletion-protected instance', function (t) {
var cmd = 'instance rm ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
t.ok(err, 'err expected');
/* JSSTYLED */
t.ok(stderr.match(/Instance has "deletion_protection" enabled/));
t.end();
});
});
tt.test(' triton instance disable-deletion-protection', function (t) {
var cmd = 'instance disable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance disable-deletion-protection'))
return t.end();
t.ok(stdout.match('Disabled deletion protection for instance "' +
INST + '"'), 'deletion protection disabled');
h.triton('instance get -j ' + INST, function (err2, stdout2) {
if (h.ifErr(t, err2, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout2);
t.ok(!inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance disable-deletion-protection (already enabled)',
function (t) {
var cmd = 'instance disable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance disable-deletion-protection'))
return t.end();
t.ok(stdout.match('Disabled deletion protection for instance "' +
INST + '"'), 'deletion protection disabled');
h.triton('instance get -j ' + INST, function (err2, stdout2) {
if (h.ifErr(t, err2, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout2);
t.ok(!inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance enable-deletion-protection', function (t) {
var cmd = 'instance enable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance enable-deletion-protection'))
return t.end();
t.ok(stdout.match('Enabled deletion protection for instance "' +
INST + '"'), 'deletion protection enabled');
h.triton('instance get -j ' + INST, function (err2, stdout2) {
if (h.ifErr(t, err2, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout2);
t.ok(inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
tt.test(' triton instance enable-deletion-protection (already enabled)',
function (t) {
var cmd = 'instance enable-deletion-protection ' + INST + ' -w';
h.triton(cmd, function (err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance enable-deletion-protection'))
return t.end();
t.ok(stdout.match('Enabled deletion protection for instance "' +
INST + '"'), 'deletion protection enabled');
h.triton('instance get -j ' + INST, function (err2, stdout2) {
if (h.ifErr(t, err2, 'triton instance get'))
return t.end();
var inst = JSON.parse(stdout2);
t.ok(inst.deletion_protection, 'deletion_protection');
t.end();
});
});
});
/*
* Use a timeout, because '-w' on delete doesn't have a way to know if the
* attempt failed or if it is just taking a really long time.
*/
tt.test(' cleanup: triton rm INST', {timeout: 10 * 60 * 1000}, cleanup);
});

View File

@ -47,7 +47,7 @@ test('triton fwrule', OPTS, function (tt) {
});
tt.test(' setup: triton create', function (t) {
h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
/*
@ -15,6 +15,7 @@
var format = require('util').format;
var os = require('os');
var test = require('tape');
var uuid = require('uuid');
var vasync = require('vasync');
var common = require('../../lib/common');
@ -37,7 +38,7 @@ var testOpts = {
// --- Tests
test('triton image ...', testOpts, function (tt) {
test('spearhead image ...', testOpts, function (tt) {
var imgNameVer = IMAGE_DATA.name + '@' + IMAGE_DATA.version;
var originInst;
var img;
@ -48,7 +49,7 @@ test('triton image ...', testOpts, function (tt) {
tt.comment(format('- %s: %j', key, value));
});
// TODO: `triton rm -f` would be helpful for this
// TODO: `spearhead rm -f` would be helpful for this
tt.test(' setup: rm existing origin inst ' + ORIGIN_ALIAS, function (t) {
h.triton(['inst', 'get', '-j', ORIGIN_ALIAS],
function (err, stdout, stderr) {
@ -199,6 +200,62 @@ test('triton image ...', testOpts, function (tt) {
}
});
tt.test(' triton image share ...', function (t) {
var dummyUuid = uuid.v4();
var argv = ['image', 'share', img.id, dummyUuid];
h.safeTriton(t, argv, function (err) {
if (err) {
t.end();
return;
}
argv = ['image', 'get', '-j', img.id];
h.safeTriton(t, argv, function (err2, stdout2) {
t.ifErr(err2, 'image get response');
if (err2) {
t.end();
return;
}
var result = JSON.parse(stdout2);
t.ok(result, 'image share result');
t.ok(result.acl, 'image share result.acl');
if (result.acl && Array.isArray(result.acl)) {
t.notEqual(result.acl.indexOf(dummyUuid), -1,
'image share result.acl contains uuid');
} else {
t.fail('image share result does not contain acl array');
}
unshareImage();
});
});
function unshareImage() {
argv = ['image', 'unshare', img.id, dummyUuid];
h.safeTriton(t, argv, function (err) {
if (err) {
t.end();
return;
}
argv = ['image', 'get', '-j', img.id];
h.safeTriton(t, argv, function (err2, stdout2) {
t.ifErr(err2, 'image get response');
if (err2) {
t.end();
return;
}
var result = JSON.parse(stdout2);
t.ok(result, 'image unshare result');
if (result.acl && Array.isArray(result.acl)) {
t.equal(result.acl.indexOf(dummyUuid), -1,
'image unshare result.acl should not contain uuid');
} else {
t.equal(result.acl, undefined, 'image has no acl');
}
t.end();
});
});
}
});
// TODO: Once have `triton ssh ...` working in test suite without hangs,
// then want to check that the created VM has the markerFile.

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 2018, Joyent, Inc.
*/
/*
* Test creating a bhyve VM.
*/
var os = require('os');
var format = require('util').format;
var test = require('tape');
var h = require('./helpers');
// --- globals
var INST_ALIAS = 'nodetritontest-instance-create-bhyve-' +
os.hostname();
var testOpts = {
skip: !h.CONFIG.allowWriteActions || h.CONFIG.skipBhyveTests
};
// --- Tests
test('triton image ...', testOpts, function (tt) {
var imgId;
var inst;
var pkgId;
tt.comment('Test config:');
Object.keys(h.CONFIG).forEach(function (key) {
var value = h.CONFIG[key];
tt.comment(format('- %s: %j', key, value));
});
// TODO: `triton rm -f` would be helpful for this
tt.test(' setup: rm existing inst ' + INST_ALIAS, function (t) {
h.deleteTestInst(t, INST_ALIAS, function onDel() {
t.end();
});
});
tt.test(' setup: find image', function (t) {
h.getTestBhyveImg(t, function (err, _imgId) {
t.ifError(err, 'getTestImg' + (err ? ': ' + err : ''));
imgId = _imgId;
t.end();
});
});
tt.test(' setup: find test package', function (t) {
h.getTestBhyvePkg(t, function (err, _pkgId) {
t.ifError(err, 'getTestPkg' + (err ? ': ' + err : ''));
pkgId = _pkgId;
t.end();
});
});
tt.test(' setup: triton create ... -n ' + INST_ALIAS, function (t) {
var argv = ['create', '-wj', '--brand=bhyve', '-n', INST_ALIAS,
imgId, pkgId];
h.safeTriton(t, argv, function (err, stdout) {
var lines = h.jsonStreamParse(stdout);
inst = lines[1];
t.ok(inst.id, 'inst.id: ' + inst.id);
t.equal(lines[1].state, 'running', 'inst is running');
t.end();
});
});
// TODO: Once have `triton ssh ...` working in test suite without hangs,
// then want to check that the created VM works.
// Remove instance. Add a test timeout, because '-w' on delete doesn't
// have a way to know if the attempt failed or if it is just taking a
// really long time.
tt.test(' cleanup: spearhead rm', {timeout: 10 * 60 * 1000}, function (t) {
h.safeTriton(t, ['rm', '-w', inst.id], function () {
t.end();
});
});
});

View File

@ -5,28 +5,53 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2018 Joyent, Inc.
*/
/*
* Integration tests for `triton network(s)`
*/
var h = require('./helpers');
var f = require('util').format;
var os = require('os');
var test = require('tape');
var h = require('./helpers');
var common = require('../../lib/common');
// --- Globals
var NET_NAME = f('nodetritontest-network-%s', os.hostname());
var networks;
var vlan;
var OPTS = {
skip: !h.CONFIG.allowWriteActions
};
// --- Tests
if (OPTS.skip) {
console.error('** skipping some %s tests', __filename);
console.error('** set "allowWriteActions" in test config to enable');
}
test('triton networks', function (tt) {
tt.test(' setup: find a test VLAN', function (t) {
h.triton('vlan list -j', function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
vlan = JSON.parse(stdout.trim().split('\n')[0]);
t.ok(vlan, 'vlan for testing found');
t.end();
});
});
tt.test(' triton network list -h', function (t) {
h.triton('networks -h', function (err, stdout, stderr) {
if (h.ifErr(t, err))
@ -209,3 +234,135 @@ test('triton network get', function (tt) {
});
});
test('triton network create', OPTS, function (tt) {
tt.test(' cleanup: rm network ' + NET_NAME + ' if exists', function (t) {
h.triton('network delete ' + NET_NAME, function (err, stdout) {
t.end();
});
});
tt.test(' triton network create -h', function (t) {
h.triton('network create -h', function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
t.ok(/Usage:\s+triton network\b/.test(stdout));
t.end();
});
});
tt.test(' triton network help create', function (t) {
h.triton('network help create', function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
t.ok(/Usage:\s+triton network create\b/.test(stdout));
t.end();
});
});
tt.test(' triton network create', function (t) {
h.triton('network create', function (err, stdout, stderr) {
t.ok(err);
t.ok(/error \(Usage\)/.test(stderr));
t.end();
});
});
tt.test(' triton network create VLAN', function (t) {
h.triton('network create --name=' + NET_NAME +
' --subnet=192.168.97.0/24 --start_ip=192.168.97.1' +
' --end_ip=192.168.97.254 -j ' + vlan.vlan_id,
function (err, stdout) {
if (h.ifErr(t, err))
return t.end();
var network = JSON.parse(stdout.trim().split('\n')[0]);
t.equal(network.name, NET_NAME);
t.equal(network.subnet, '192.168.97.0/24');
t.equal(network.provision_start_ip, '192.168.97.1');
t.equal(network.provision_end_ip, '192.168.97.254');
t.equal(network.vlan_id, vlan.vlan_id);
h.triton('network delete ' + network.id, function (err2) {
h.ifErr(t, err2);
t.end();
});
});
});
});
test('triton network delete', OPTS, function (tt) {
tt.test(' triton network delete -h', function (t) {
h.triton('network delete -h', function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
t.ok(/Usage:\s+triton network\b/.test(stdout));
t.end();
});
});
tt.test(' triton network help delete', function (t) {
h.triton('network help delete', function (err, stdout, stderr) {
if (h.ifErr(t, err))
return t.end();
t.ok(/Usage:\s+triton network delete\b/.test(stdout));
t.end();
});
});
tt.test(' triton network delete', function (t) {
h.triton('network delete', function (err, stdout, stderr) {
t.ok(err);
t.ok(/error \(Usage\)/.test(stderr));
t.end();
});
});
function deleteNetworkTester(t, deleter) {
h.triton('network create --name=' + NET_NAME +
' --subnet=192.168.97.0/24 --start_ip=192.168.97.1' +
' --end_ip=192.168.97.254 -j ' + vlan.vlan_id,
function (err, stdout) {
if (h.ifErr(t, err, 'create test network'))
return t.end();
var network = JSON.parse(stdout.trim().split('\n')[0]);
deleter(null, network, function (err2) {
if (h.ifErr(t, err2, 'deleting test network'))
return t.end();
h.triton('network get ' + network.id, function (err3) {
t.ok(err3, 'network should be gone');
t.end();
});
});
});
}
tt.test(' triton network delete ID', function (t) {
deleteNetworkTester(t, function (err, network, cb) {
h.triton('network delete ' + network.id, cb);
});
});
tt.test(' triton network delete NAME', function (t) {
deleteNetworkTester(t, function (err, network, cb) {
h.triton('network delete ' + network.name, cb);
});
});
tt.test(' triton network delete SHORTID', function (t) {
deleteNetworkTester(t, function (err, network, cb) {
var shortid = network.id.split('-')[0];
h.triton('network delete ' + shortid, cb);
});
});
});

View File

@ -0,0 +1,278 @@
/*
* 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.
*/
/*
* Integration tests for `triton instance nics ...`
*/
var h = require('./helpers');
var f = require('util').format;
var os = require('os');
var test = require('tape');
// --- Globals
var INST_ALIAS = f('nodetritontest-nics-%s', os.hostname());
var NETWORK;
var INST;
var NIC;
var NIC2;
var OPTS = {
skip: !h.CONFIG.allowWriteActions
};
// --- Tests
if (OPTS.skip) {
console.error('** skipping %s tests', __filename);
console.error('** set "allowWriteActions" in test config to enable');
}
test('triton instance nics', OPTS, function (tt) {
h.printConfig(tt);
tt.test(' cleanup existing inst with alias ' + INST_ALIAS, function (t) {
h.deleteTestInst(t, INST_ALIAS, function onDelete(err) {
t.ifErr(err);
t.end();
});
});
tt.test(' setup: triton instance create', function (t) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err, instId) {
if (h.ifErr(t, err, 'triton instance create')) {
t.end();
return;
}
t.ok(instId, 'created instance ' + instId);
INST = instId;
t.end();
});
});
tt.test(' setup: find network for tests', function (t) {
h.triton('network list -j', function onNetworks(err, stdout) {
if (h.ifErr(t, err, 'triton network list')) {
t.end();
return;
}
NETWORK = JSON.parse(stdout.trim().split('\n')[0]);
t.ok(NETWORK, 'NETWORK');
t.end();
});
});
tt.test(' triton instance nic create', function (t) {
var cmd = 'instance nic create -j -w ' + INST + ' ' + NETWORK.id;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create')) {
t.end();
return;
}
NIC = JSON.parse(stdout);
t.ok(NIC, 'created NIC: ' + stdout.trim());
t.end();
});
});
tt.test(' triton instance nic get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC.mac;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get')) {
t.end();
return;
}
var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC.mac, 'nic MAC is correct');
t.equal(obj.ip, NIC.ip, 'nic IP is correct');
t.equal(obj.network, NIC.network, 'nic network is correct');
t.end();
});
});
tt.test(' triton instance nic list', function (t) {
var cmd = 'instance nic list ' + INST;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list')) {
t.end();
return;
}
var nics = stdout.trim().split('\n');
t.ok(nics[0].match(/IP\s+MAC\s+STATE\s+NETWORK/), 'nic list' +
' header correct');
nics.shift();
t.ok(nics.length >= 1, 'triton nic list expected nic num');
var testNics = nics.filter(function doFilter(nic) {
return nic.match(NIC.mac);
});
t.equal(testNics.length, 1, 'triton nic list test nic found');
t.end();
});
});
tt.test(' triton instance nic list -j', function (t) {
var cmd = 'instance nic list -j ' + INST;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic list')) {
t.end();
return;
}
var nics = stdout.trim().split('\n').map(function doParse(line) {
return JSON.parse(line);
});
t.ok(nics.length >= 1, 'triton nic list expected nic num');
var testNics = nics.filter(function doFilter(nic) {
return nic.mac === NIC.mac;
});
t.equal(testNics.length, 1, 'triton nic list test nic found');
t.end();
});
});
tt.test(' triton instance nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err)) {
t.end();
return;
}
var nics = stdout.trim().split('\n').map(function doParse(str) {
return JSON.parse(str);
});
t.equal(nics.length, 1);
t.equal(nics[0].ip, NIC.ip);
t.equal(nics[0].network, NIC.network);
t.end();
});
});
tt.test(' triton nic list mac=<...>', function (t) {
var cmd = 'instance nic list -j ' + INST + ' mac=' + NIC.mac;
h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err)) {
t.end();
return;
}
var nics = stdout.trim().split('\n').map(function doParse(str) {
return JSON.parse(str);
});
t.equal(nics.length, 1);
t.equal(nics[0].ip, NIC.ip);
t.equal(nics[0].network, NIC.network);
t.end();
});
});
tt.test(' triton instance nic delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC.mac;
h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic delete')) {
t.end();
return;
}
t.ok(stdout.match('Deleted NIC ' + NIC.mac, 'deleted nic'));
t.end();
});
});
tt.test(' triton instance nic create (with NICOPTS)', function (t) {
var cmd = 'instance nic create -j -w ' + INST + ' ipv4_uuid=' +
NETWORK.id;
h.triton(cmd, function doTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic create')) {
t.end();
return;
}
NIC2 = JSON.parse(stdout);
t.end();
});
});
tt.test(' triton instance nic with ip get', function (t) {
var cmd = 'instance nic get ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic get')) {
t.end();
return;
}
var obj = JSON.parse(stdout);
t.equal(obj.mac, NIC2.mac, 'nic MAC is correct');
t.equal(obj.ip, NIC2.ip, 'nic IP is correct');
t.equal(obj.network, NIC2.network, 'nic network is correct');
t.end();
});
});
tt.test(' triton instance nic with ip delete', function (t) {
var cmd = 'instance nic delete --force ' + INST + ' ' + NIC2.mac;
h.triton(cmd, function onTriton(err, stdout, stderr) {
if (h.ifErr(t, err, 'triton instance nic with ip delete')) {
t.end();
return;
}
t.ok(stdout.match('Deleted NIC ' + NIC2.mac, 'deleted nic'));
t.end();
});
});
/*
* Use a timeout, because '-w' on delete doesn't have a way to know if the
* attempt failed or if it is just taking a really long time.
*/
tt.test(' cleanup: triton instance rm INST', {timeout: 10 * 60 * 1000},
function (t) {
h.deleteTestInst(t, INST_ALIAS, function () {
t.end();
});
});
});

View File

@ -44,7 +44,7 @@ test('triton instance snapshot', OPTS, function (tt) {
});
tt.test(' setup: triton instance create', function (t) {
h.createTestInst(t, INST_ALIAS, function onInst(err2, instId) {
h.createTestInst(t, INST_ALIAS, {}, function onInst(err2, instId) {
if (h.ifErr(t, err2, 'triton instance create'))
return t.end();

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2015, Joyent, Inc.
* Copyright (c) 2018, Joyent, Inc.
*/
/*
@ -45,6 +45,8 @@ var subs = [
['instance delete', 'instance rm', 'delete', 'rm'],
['instance enable-firewall'],
['instance disable-firewall'],
['instance enable-deletion-protection'],
['instance disable-deletion-protection'],
['instance rename'],
['instance ssh'],
['instance ip'],
@ -56,11 +58,25 @@ var subs = [
['instance snapshot list', 'instance snapshot ls', 'instance snapshots'],
['instance snapshot get'],
['instance snapshot delete', 'instance snapshot rm'],
['instance nic create'],
['instance nic list', 'instance nic ls'],
['instance nic get'],
['instance nic delete', 'instance nic rm'],
['ip'],
['ssh'],
['network'],
['network list', 'networks'],
['network create'],
['network list', 'network ls', 'networks'],
['network get'],
['network get-default'],
['network set-default'],
['network delete', 'network rm'],
['vlan'],
['vlan create'],
['vlan list', 'vlan ls'],
['vlan get'],
['vlan update'],
['vlan delete', 'vlan rm'],
['key'],
['key add'],
['key list', 'key ls', 'keys'],

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

@ -26,8 +26,8 @@ var testcommon = require('../lib/testcommon');
var CONFIG;
var configPath = process.env.TRITON_TEST_CONFIG
? path.resolve(process.cwd(), process.env.TRITON_TEST_CONFIG)
var configPath = process.env.TSC_TEST_CONFIG
? path.resolve(process.cwd(), process.env.SC_TEST_CONFIG)
: path.resolve(__dirname, '..', 'config.json');
try {
CONFIG = require(configPath);
@ -44,7 +44,7 @@ try {
'CONFIG.profile.insecure');
} else if (CONFIG.profileName) {
CONFIG.profile = mod_triton.loadProfile({
configDir: path.join(process.env.HOME, '.triton'),
configDir: path.join(process.env.HOME, '.spearhead'),
name: CONFIG.profileName
});
} else {
@ -55,11 +55,11 @@ try {
'test/config.json#allowWriteActions');
} catch (e) {
error('* * *');
error('node-triton integration tests require a config file. By default');
error('node-spearhead integration tests require a config file. By default');
error('it looks for "test/config.json". Or you can set the');
error('TRITON_TEST_CONFIG envvar. E.g.:');
error('SC_TEST_CONFIG envvar. E.g.:');
error('');
error(' TRITON_TEST_CONFIG=test/coal.json make test');
error(' SC_TEST_CONFIG=test/coal.json make test');
error('');
error('See "test/config.json.sample" for a starting point for a config.');
error('');
@ -75,7 +75,7 @@ if (CONFIG.profile.insecure === undefined)
if (CONFIG.allowWriteActions === undefined)
CONFIG.allowWriteActions = false;
var TRITON = [process.execPath, path.resolve(__dirname, '../../bin/triton')];
var TRITON = [process.execPath, path.resolve(__dirname, '../../bin/spearhead')];
var UA = 'node-triton-test';
var LOG = require('../lib/log');
@ -83,10 +83,10 @@ var LOG = require('../lib/log');
/*
* Call the `triton` CLI with the given args.
* Call the `spearhead` CLI with the given args.
*
* @param args {String|Array} Required. CLI arguments to `triton ...` (without
* the "triton"). This can be an array of args, or a string.
* @param args {String|Array} Required. CLI arguments to `spearhead ...` (without
* the "spearhead"). This can be an array of args, or a string.
* @param opts {Object} Optional.
* - opts.cwd {String} cwd option to exec.
* @param cb {Function}
@ -111,11 +111,11 @@ function triton(args, opts, cb) {
PATH: process.env.PATH,
HOME: process.env.HOME,
SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK,
TRITON_PROFILE: 'env',
TRITON_URL: CONFIG.profile.url,
TRITON_ACCOUNT: CONFIG.profile.account,
TRITON_KEY_ID: CONFIG.profile.keyId,
TRITON_TLS_INSECURE: CONFIG.profile.insecure
SC_PROFILE: 'env',
SC_URL: CONFIG.profile.url,
SC_ACCOUNT: CONFIG.profile.account,
SC_KEY_ID: CONFIG.profile.keyId,
SC_TLS_INSECURE: CONFIG.profile.insecure
},
cwd: opts.cwd
},
@ -126,12 +126,12 @@ function triton(args, opts, cb) {
/*
* `triton ...` wrapper that:
* `spearhead ...` wrapper that:
* - tests non-error exit
* - tests stderr is empty
*
* @param {Tape} t - tape test object
* @param {Object|Array} opts - options object, or just the `triton` args
* @param {Object|Array} opts - options object, or just the `spearhead` args
* @param {Function} cb - `function (err, stdout)`
* Note that `err` will already have been tested to be falsey via
* `t.error(err, ...)`, so it may be fine for the calling test case
@ -149,7 +149,7 @@ function safeTriton(t, opts, cb) {
// t.comment(f('running: triton %s', opts.args.join(' ')));
triton(opts.args, function (err, stdout, stderr) {
t.error(err, f('ran "triton %s", err=%s', opts.args.join(' '), err));
t.error(err, f('ran "spearhead %s", err=%s', opts.args.join(' '), err));
t.equal(stderr, '', 'empty stderr');
if (opts.json) {
try {
@ -183,6 +183,7 @@ function getTestImg(t, cb) {
var candidateImageNames = {
'base-64-lts': true,
'base-64': true,
'minimal-64-lts': true,
'minimal-64': true,
'base-32-lts': true,
'base-32': true,
@ -361,7 +362,7 @@ function createClient(cb) {
mod_triton.createClient({
log: LOG,
profile: CONFIG.profile,
configDir: '~/.triton' // piggy-back on Triton CLI config dir
configDir: '~/.spearhead' // piggy-back on Spearhead CLI config dir
}, cb);
}
@ -369,7 +370,13 @@ function createClient(cb) {
/*
* Create a small test instance.
*/
function createTestInst(t, name, cb) {
function createTestInst(t, name, opts, cb) {
assert.object(t, 't');
assert.string(name, 'name');
assert.object(opts, 'opts');
assert.optionalArrayOfString(opts.extraFlags, 'opts.extraFlags');
assert.func(cb, 'cb');
getTestPkg(t, function (err, pkgId) {
t.ifErr(err);
if (err) {
@ -385,6 +392,10 @@ function createTestInst(t, name, cb) {
}
var cmd = f('instance create -w -n %s %s %s', name, imgId, pkgId);
if (opts.extraFlags) {
cmd += ' ' + opts.extraFlags.join(' ');
}
triton(cmd, function (err3, stdout) {
t.ifErr(err3, 'create test instance');
if (err3) {