merge with upstream

This commit is contained in:
Marius Pana 2019-03-12 21:55:51 +02:00
commit caa6da7821
10 changed files with 428 additions and 66 deletions

View File

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

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

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

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,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) {

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

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

View File

@ -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 createOpts = {
vlan_id: +args[0]
};
if (opts.name) {
createOpts.name = opts.name;
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;
}
@ -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;

View File

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

View File

@ -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": {