diff --git a/CHANGES.md b/CHANGES.md index 501276a..fd420d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/README.md b/README.md index 943cf9a..a291e42 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 945b8d5..512e65e 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -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); diff --git a/lib/do_image/do_get.js b/lib/do_image/do_get.js index eaa9f1a..06832ed 100644 --- a/lib/do_image/do_get.js +++ b/lib/do_image/do_get.js @@ -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.' } ]; diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index 888f967..2663108 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -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) { diff --git a/lib/do_instance/do_ssh.js b/lib/do_instance/do_ssh.js index a417ccb..4162136 100644 --- a/lib/do_instance/do_ssh.js +++ b/lib/do_instance/do_ssh.js @@ -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 ', diff --git a/lib/do_network/do_create.js b/lib/do_network/do_create.js index 71448b5..023aef8 100644 --- a/lib/do_network/do_create.js +++ b/lib/do_network/do_create.js @@ -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; diff --git a/lib/do_vlan/do_create.js b/lib/do_vlan/do_create.js index 7e6192c..9a58dc1 100644 --- a/lib/do_vlan/do_create.js +++ b/lib/do_vlan/do_create.js @@ -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; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 6e26c1d..77575c1 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -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; diff --git a/package.json b/package.json index 23d8ea9..b177ad7 100644 --- a/package.json +++ b/package.json @@ -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": {