From 3f243f8c8f4bb8622f99566af23e4da919aadba0 Mon Sep 17 00:00:00 2001 From: Todd Whiteman Date: Wed, 14 Feb 2018 11:52:29 -0800 Subject: [PATCH 1/2] TRITON-116 node-triton image sharing Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 5 + lib/cloudapi2.js | 29 +++++- lib/do_image/do_share.js | 115 ++++++++++++++++++++++ lib/do_image/do_unshare.js | 115 ++++++++++++++++++++++ lib/do_image/index.js | 6 +- lib/tritonapi.js | 109 ++++++++++++++++++-- package.json | 5 +- test/integration/cli-image-create.test.js | 59 ++++++++++- 8 files changed, 431 insertions(+), 12 deletions(-) create mode 100644 lib/do_image/do_share.js create mode 100644 lib/do_image/do_unshare.js diff --git a/CHANGES.md b/CHANGES.md index f6332c5..609b4df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,11 @@ Known issues: ## not yet released +## 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_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 f427e6d..832cd59 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/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 0d00f3f..03cc0eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "5.6.1", + "version": "5.7.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "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/integration/cli-image-create.test.js b/test/integration/cli-image-create.test.js index 392813f..3f63c1d 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'); @@ -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. From 26b97b5bedd64dbce51cf470af3c8fdff3cda57c Mon Sep 17 00:00:00 2001 From: Josh Wilsdon Date: Mon, 19 Feb 2018 17:28:42 -0800 Subject: [PATCH 2/2] TRITON-124 add node-triton support for bhyve Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 7 ++ lib/do_fwrule/do_instances.js | 4 +- lib/do_instance/do_create.js | 12 ++- lib/do_instance/do_list.js | 4 +- lib/do_instance/do_snapshot/do_create.js | 4 +- package.json | 2 +- test/config.json.sample | 11 +++ .../cli-instance-create-bhyve.test.js | 92 +++++++++++++++++++ test/integration/helpers.js | 76 ++++++++++++++- 9 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 test/integration/cli-instance-create-bhyve.test.js diff --git a/CHANGES.md b/CHANGES.md index 609b4df..92e075a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,13 @@ 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 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_instance/do_create.js b/lib/do_instance/do_create.js index 47b7e18..886bebb 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/package.json b/package.json index 03cc0eb..3516090 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "5.7.0", + "version": "5.8.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": { 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-instance-create-bhyve.test.js b/test/integration/cli-instance-create-bhyve.test.js new file mode 100644 index 0000000..a437b04 --- /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: triton 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..4a2f205 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2017 Joyent, Inc. + * Copyright 2018 Joyent, Inc. */ /* @@ -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. * @@ -511,8 +583,10 @@ module.exports = { deleteTestImg: deleteTestImg, getTestImg: getTestImg, + getTestBhyveImg: getTestBhyveImg, getTestKvmImg: getTestKvmImg, getTestPkg: getTestPkg, + getTestBhyvePkg: getTestBhyvePkg, getTestKvmPkg: getTestKvmPkg, getResizeTestPkg: getResizeTestPkg,