@@ -6,6 +6,34 @@ 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 |
@@ -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 |
@@ -504,7 +504,7 @@ CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = { | |||
// --- Fabric VLANs | |||
/** | |||
* Creates a network on a fabric (specifically: a fabric VLAN). | |||
* Creates a network on a fabric VLAN. | |||
* | |||
* @param {Object} options object containing: | |||
* - {Integer} vlan_id (required) VLAN's id, between 0-4095. | |||
@@ -513,7 +513,8 @@ CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = { | |||
* - {String} provision_start_ip (required) First assignable IP addr. | |||
* - {String} provision_end_ip (required) Last assignable IP addr. | |||
* - {String} gateway (optional) Gateway IP address. | |||
* - {String} resolvers (optional) Static routes for hosts on network. | |||
* - {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). | |||
@@ -528,8 +529,8 @@ function createFabricNetwork(opts, cb) { | |||
assert.string(opts.provision_start_ip, 'opts.provision_start_ip'); | |||
assert.string(opts.provision_end_ip, 'opts.provision_end_ip'); | |||
assert.optionalString(opts.gateway, 'opts.gateway'); | |||
assert.optionalString(opts.resolvers, 'opts.resolvers'); | |||
assert.optionalString(opts.routes, 'opts.routes'); | |||
assert.optionalArrayOfString(opts.resolvers, 'opts.resolvers'); | |||
assert.optionalObject(opts.routes, 'opts.routes'); | |||
assert.optionalBool(opts.internet_nat, 'opts.internet_nat'); | |||
var data = common.objCopy(opts); |
@@ -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.' | |||
} | |||
]; | |||
@@ -5,7 +5,7 @@ | |||
*/ | |||
/* | |||
* Copyright 2018 Joyent, Inc. | |||
* Copyright 2019 Joyent, Inc. | |||
* | |||
* `triton instance create ...` | |||
*/ | |||
@@ -203,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) { |
@@ -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 ', |
@@ -18,8 +18,6 @@ var common = require('../common'); | |||
var errors = require('../errors'); | |||
var OPTIONAL_OPTS = ['description', 'gateway', 'resolvers', 'routes']; | |||
function do_create(subcmd, opts, args, cb) { | |||
assert.optionalString(opts.name, 'opts.name'); | |||
@@ -28,13 +26,15 @@ function do_create(subcmd, opts, args, cb) { | |||
assert.optionalString(opts.end_ip, 'opts.end_ip'); | |||
assert.optionalString(opts.description, 'opts.description'); | |||
assert.optionalString(opts.gateway, 'opts.gateway'); | |||
assert.optionalString(opts.resolvers, 'opts.resolvers'); | |||
assert.optionalString(opts.routes, 'opts.routes'); | |||
assert.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; | |||
@@ -53,23 +53,74 @@ function do_create(subcmd, opts, args, cb) { | |||
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, | |||
name: opts.name, | |||
subnet: opts.subnet, | |||
provision_start_ip: opts.start_ip, | |||
provision_end_ip: opts.end_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; | |||
} | |||
OPTIONAL_OPTS.forEach(function (attr) { | |||
if (opts[attr]) { | |||
createOpts[attr] = opts[attr]; | |||
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; | |||
@@ -106,9 +157,7 @@ do_create.options = [ | |||
help: 'Show this help.' | |||
}, | |||
{ | |||
names: ['json', 'j'], | |||
type: 'bool', | |||
help: 'JSON stream output.' | |||
group: 'Create options' | |||
}, | |||
{ | |||
names: ['name', 'n'], | |||
@@ -123,46 +172,67 @@ do_create.options = [ | |||
help: 'Description of the NETWORK.' | |||
}, | |||
{ | |||
names: ['subnet'], | |||
group: '' | |||
}, | |||
{ | |||
names: ['subnet', 's'], | |||
type: 'string', | |||
helpArg: 'SUBNET', | |||
help: 'A CIDR string describing the NETWORK.' | |||
}, | |||
{ | |||
names: ['start_ip'], | |||
names: ['start-ip', 'S', 'start_ip'], | |||
type: 'string', | |||
helpArg: 'START_IP', | |||
help: 'First assignable IP address on NETWORK.' | |||
}, | |||
{ | |||
names: ['end_ip'], | |||
names: ['end-ip', 'E', 'end_ip'], | |||
type: 'string', | |||
helpArg: 'END_IP', | |||
help: 'Last assignable IP address on NETWORK.' | |||
}, | |||
{ | |||
names: ['gateway'], | |||
type: 'string', | |||
helpArg: 'GATEWAY', | |||
help: 'Gateway IP address.' | |||
group: '' | |||
}, | |||
{ | |||
names: ['resolvers'], | |||
names: ['gateway', 'g'], | |||
type: 'string', | |||
helpArg: 'RESOLVERS', | |||
help: 'Resolver IP addresses.' | |||
helpArg: 'IP', | |||
help: 'Default gateway IP address.' | |||
}, | |||
{ | |||
names: ['routes'], | |||
type: 'string', | |||
helpArg: 'ROUTES', | |||
help: 'Static routes for hosts on NETWORK.' | |||
names: ['resolver', 'r'], | |||
type: 'arrayOfString', | |||
helpArg: 'RESOLVER', | |||
help: 'DNS resolver IP address. Specify multiple -r options for ' + | |||
'multiple resolvers.' | |||
}, | |||
{ | |||
names: ['no_nat'], | |||
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.' | |||
} | |||
]; | |||
@@ -175,13 +245,29 @@ do_create.help = [ | |||
'', | |||
'{{options}}', | |||
'', | |||
'Example:', | |||
' triton network create -n accounting --subnet=192.168.0.0/24', | |||
' --start_ip=192.168.0.1 --end_ip=192.168.0.254 2' | |||
'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: 25 | |||
helpCol: 16 | |||
}; | |||
module.exports = do_create; |
@@ -5,13 +5,14 @@ | |||
*/ | |||
/* | |||
* Copyright 2017 Joyent, Inc. | |||
* 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'); | |||
@@ -37,12 +38,22 @@ function do_create(subcmd, opts, args, cb) { | |||
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: +args[0] | |||
vlan_id: vlanId, | |||
name: opts.name | |||
}; | |||
if (opts.name) { | |||
createOpts.name = opts.name; | |||
} | |||
if (opts.description) { | |||
createOpts.description = opts.description; | |||
} | |||
@@ -86,9 +97,7 @@ do_create.options = [ | |||
help: 'Show this help.' | |||
}, | |||
{ | |||
names: ['json', 'j'], | |||
type: 'bool', | |||
help: 'JSON stream output.' | |||
group: 'Create options' | |||
}, | |||
{ | |||
names: ['name', 'n'], | |||
@@ -101,6 +110,14 @@ do_create.options = [ | |||
type: 'string', | |||
helpArg: 'DESC', | |||
help: 'Description of the VLAN.' | |||
}, | |||
{ | |||
group: 'Other options' | |||
}, | |||
{ | |||
names: ['json', 'j'], | |||
type: 'bool', | |||
help: 'JSON stream output.' | |||
} | |||
]; | |||
@@ -117,7 +134,7 @@ do_create.help = [ | |||
].join('\n'); | |||
do_create.helpOpts = { | |||
helpCol: 25 | |||
helpCol: 16 | |||
}; | |||
module.exports = do_create; |
@@ -5,7 +5,7 @@ | |||
*/ | |||
/* | |||
* Copyright (c) 2018, Joyent, Inc. | |||
* Copyright 2019 Joyent, Inc. | |||
*/ | |||
/* BEGIN JSSTYLED */ | |||
@@ -693,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; | |||
@@ -700,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) { | |||
@@ -755,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; |
@@ -1,7 +1,7 @@ | |||
{ | |||
"name": "spearhead", | |||
"description": "Spearhead Cloud CLI and client (https://spearhead.cloud)", | |||
"version": "6.1.4", | |||
"version": "7.0.0", | |||
"author": "Spearhead Systems (spearhead.systems)", | |||
"homepage": "https://code.spearhead.cloud/Spearhead/node-spearhead", | |||
"dependencies": { |