diff --git a/CHANGES.md b/CHANGES.md index f6332c5..92e075a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,18 @@ Known issues: ## not yet released +## 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 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index ba43791..45924b9 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -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/ @@ -767,6 +767,33 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) { }); }; +/** + * Update an image. + * + * + * @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 shareImage(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); + }); +}; + /** * Wait for an image to go one of a set of specfic states. * diff --git a/lib/do_fwrule/do_instances.js b/lib/do_fwrule/do_instances.js index 53bc6bf..b549f16 100644 --- a/lib/do_fwrule/do_instances.js +++ b/lib/do_fwrule/do_instances.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2016 Joyent, Inc. + * Copyright 2018 Joyent, Inc. * * `triton fwrule instances ...` */ @@ -111,6 +111,7 @@ 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'); @@ -159,6 +160,7 @@ 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"', diff --git a/lib/do_image/do_share.js b/lib/do_image/do_share.js new file mode 100644 index 0000000..9bc994f --- /dev/null +++ b/lib/do_image/do_share.js @@ -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; diff --git a/lib/do_image/do_unshare.js b/lib/do_image/do_unshare.js new file mode 100644 index 0000000..5675114 --- /dev/null +++ b/lib/do_image/do_unshare.js @@ -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; diff --git a/lib/do_image/index.js b/lib/do_image/index.js index 27349fa..e7eceb0 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright (c) 2018, Joyent, Inc. * * `triton image ...` */ @@ -36,6 +36,8 @@ function ImageCLI(top) { 'create', 'delete', 'export', + 'share', + 'unshare', 'wait' ] }); @@ -52,6 +54,8 @@ ImageCLI.prototype.do_get = require('./do_get'); 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'); diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index 54ae5f6..96ac34a 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright 2018 Joyent, Inc. * * `triton instance create ...` */ @@ -376,6 +376,9 @@ function do_create(subcmd, opts, args, cb) { function (net) { return net.id; }) }; + if (opts.brand) { + createOpts.brand = opts.brand; + } if (ctx.volMounts) { createOpts.volumes = ctx.volMounts; } @@ -492,6 +495,13 @@ do_create.options = [ { group: 'Create options' }, + { + names: ['brand'], + helpArg: 'BRAND', + type: 'string', + help: 'Override the default brand for this instance. Most users will ' + + 'not need this option.' + }, { names: ['name', 'n'], helpArg: 'NAME', diff --git a/lib/do_instance/do_list.js b/lib/do_instance/do_list.js index 2619a3a..3892efc 100644 --- a/lib/do_instance/do_list.js +++ b/lib/do_instance/do_list.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2017, Joyent, Inc. + * Copyright (c) 2018, Joyent, Inc. * * `triton instance list ...` */ @@ -150,6 +150,7 @@ 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'); @@ -208,6 +209,7 @@ 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"', diff --git a/lib/do_instance/do_snapshot/do_create.js b/lib/do_instance/do_snapshot/do_create.js index de7b5cf..62df2f6 100644 --- a/lib/do_instance/do_snapshot/do_create.js +++ b/lib/do_instance/do_snapshot/do_create.js @@ -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']; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 28e7be9..b1683cc 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright (c) 2018, Joyent, Inc. */ /* BEGIN JSSTYLED */ @@ -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(); } }); @@ -745,10 +745,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 +769,101 @@ 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); + }); +}; + /** * Get an active package by ID, exact name, or short ID, in that order. * diff --git a/package.json b/package.json index e44b069..96f8d4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spearhead", "description": "Spearhead Cloud CLI and client (https://spearhead.cloud)", - "version": "5.6.4", + "version": "5.8.0", "author": "Spearhead Systems (spearhead.systems)", "homepage": "https://code.spearhead.cloud/Spearhead/node-spearhead", "dependencies": { @@ -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": { diff --git a/test/config.json.sample b/test/config.json.sample index ec64816..a9326b8 100644 --- a/test/config.json.sample +++ b/test/config.json.sample @@ -26,6 +26,11 @@ // to true. "skipAffinityTests": false, + // Optional. Set to 'true' to skip testing of bhyve things. Some DCs might + // not support bhyve (no packages or images available, and/or no CNs with + // bhyve compatible hardware). + "skipBhyveTests": false, + // Optional. Set to 'true' to skip testing of KVM things. Some DCs might // not support KVM (no KVM packages or images available). "skipKvmTests": false, @@ -36,6 +41,12 @@ "resizePackage": "", "image": "" + // The params used for test *bhyve* provisions. By default the tests use: + // the smallest RAM package with "kvm" in the name, the latest + // ubuntu-certified image. + "bhyvePackage": "", + "bhyveImage": "", + // The params used for test *KVM* provisions. By default the tests use: // the smallest RAM package with "kvm" in the name, the latest // ubuntu-certified image. diff --git a/test/integration/cli-image-create.test.js b/test/integration/cli-image-create.test.js index 392813f..3b39425 100644 --- a/test/integration/cli-image-create.test.js +++ b/test/integration/cli-image-create.test.js @@ -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. diff --git a/test/integration/cli-instance-create-bhyve.test.js b/test/integration/cli-instance-create-bhyve.test.js new file mode 100644 index 0000000..242097c --- /dev/null +++ b/test/integration/cli-instance-create-bhyve.test.js @@ -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(); + }); + }); +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index e091dda..d68d436 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright 2018 Joyent, Inc. */ /* @@ -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 { @@ -211,6 +211,46 @@ function getTestImg(t, cb) { } +/* + * Find and return an image that can be used for test *bhyve* provisions. + * + * @param {Tape} t - tape test object + * @param {Function} cb - `function (err, imgId)` + * where `imgId` is an image identifier (an image name, shortid, or id). + */ +function getTestBhyveImg(t, cb) { + if (CONFIG.bhyveImage) { + assert.string(CONFIG.bhyvePackage, 'CONFIG.bhyvePackage'); + t.ok(CONFIG.bhyveImage, 'bhyveImage from config: ' + CONFIG.bhyveImage); + cb(null, CONFIG.bhyveImage); + return; + } + + var candidateImageNames = { + 'ubuntu-certified-16.04': true + }; + safeTriton(t, ['img', 'ls', '-j'], function (err, stdout) { + var imgId; + var imgs = jsonStreamParse(stdout); + // Newest images first. + tabula.sortArrayOfObjects(imgs, ['-published_at']); + var imgRepr; + for (var i = 0; i < imgs.length; i++) { + var img = imgs[i]; + if (candidateImageNames[img.name]) { + imgId = img.id; + imgRepr = f('%s@%s', img.name, img.version); + break; + } + } + + t.ok(imgId, + f('latest bhyve image (using subset of supported names): %s (%s)', + imgId, imgRepr)); + cb(err, imgId); + }); +} + /* * Find and return an image that can be used for test *KVM* provisions. * @@ -281,6 +321,38 @@ function getTestPkg(t, cb) { }); } +/* + * Find and return an package that can be used for *bhyve* test provisions. + * + * @param {Tape} t - tape test object + * @param {Function} cb - `function (err, pkgId)` + * where `pkgId` is an package identifier (a name, shortid, or id). + */ +function getTestBhyvePkg(t, cb) { + if (CONFIG.bhyvePackage) { + assert.string(CONFIG.bhyvePackage, 'CONFIG.bhyvePackage'); + t.ok(CONFIG.bhyvePackage, 'bhyvePackage from config: ' + + CONFIG.bhyvePackage); + cb(null, CONFIG.bhyvePackage); + return; + } + + // bhyve uses the same packages as kvm + safeTriton(t, ['pkg', 'ls', '-j'], function (err, stdout) { + var pkgs = jsonStreamParse(stdout); + // Filter on those with 'kvm' in the name. + pkgs = pkgs.filter(function (pkg) { + return pkg.name.indexOf('kvm') !== -1; + }); + // Smallest RAM first. + tabula.sortArrayOfObjects(pkgs, ['memory']); + var pkgId = pkgs[0].id; + t.ok(pkgId, f('smallest (RAM) available kvm package: %s (%s)', + pkgId, pkgs[0].name)); + cb(null, pkgId); + }); +} + /* * Find and return an package that can be used for *KVM* test provisions. * @@ -361,7 +433,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); } @@ -511,8 +583,10 @@ module.exports = { deleteTestImg: deleteTestImg, getTestImg: getTestImg, + getTestBhyveImg: getTestBhyveImg, getTestKvmImg: getTestKvmImg, getTestPkg: getTestPkg, + getTestBhyvePkg: getTestBhyvePkg, getTestKvmPkg: getTestKvmPkg, getResizeTestPkg: getResizeTestPkg,