From 33ff58c3d3d5573904c08010a24dc232ba3c346a Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 3 Apr 2017 12:37:58 -0700 Subject: [PATCH] joyent/node-triton#195 test *kvm* image creation --- test/config.json.sample | 10 ++ test/integration/cli-image-create-kvm.test.js | 169 ++++++++++++++++++ test/integration/helpers.js | 161 +++++++++++++++-- 3 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 test/integration/cli-image-create-kvm.test.js diff --git a/test/config.json.sample b/test/config.json.sample index 8768ff9..8cfeb8e 100644 --- a/test/config.json.sample +++ b/test/config.json.sample @@ -22,9 +22,19 @@ // to true. "skipAffinityTests": 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, + // The params used for test provisions. By default the tests use: // the smallest RAM package, the latest base* image. "package": "", "resizePackage": "", "image": "" + + // 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. + "kvmPackage": "", + "kvmImage": "" } diff --git a/test/integration/cli-image-create-kvm.test.js b/test/integration/cli-image-create-kvm.test.js new file mode 100644 index 0000000..8269a99 --- /dev/null +++ b/test/integration/cli-image-create-kvm.test.js @@ -0,0 +1,169 @@ +/* + * 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 2016, Joyent, Inc. + */ + +/* + * Test image commands. + */ + +var format = require('util').format; +var os = require('os'); +var test = require('tape'); +var vasync = require('vasync'); + +var common = require('../../lib/common'); +var h = require('./helpers'); + + +// --- globals + +var _RESOURCE_NAME_PREFIX = 'nodetritontest-image-create-kvm-' + os.hostname(); +var ORIGIN_INST_ALIAS = _RESOURCE_NAME_PREFIX + '-origin'; +var IMAGE_DATA = { + name: _RESOURCE_NAME_PREFIX + '-image', + version: '1.0.0' +}; +var DERIVED_INST_ALIAS = _RESOURCE_NAME_PREFIX + '-derived'; +delete _RESOURCE_NAME_PREFIX; + +var testOpts = { + skip: !h.CONFIG.allowWriteActions || h.CONFIG.skipKvmTests +}; + + +// --- Tests + +test('triton image ...', testOpts, function (tt) { + var imgNameVer = IMAGE_DATA.name + '@' + IMAGE_DATA.version; + var originInst; + var img; + + 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 origin inst ' + ORIGIN_INST_ALIAS, + function (t) { + h.deleteTestInst(t, ORIGIN_INST_ALIAS, function onDel() { + t.end(); + }); + }); + + // TODO: `triton rm -f` would be helpful for this + tt.test(' setup: rm existing derived inst ' + DERIVED_INST_ALIAS, + function (t) { + h.deleteTestInst(t, DERIVED_INST_ALIAS, function onDel() { + t.end(); + }); + }); + + tt.test(' setup: rm existing img ' + imgNameVer, function (t) { + h.deleteTestImg(t, imgNameVer, function onDel() { + t.end(); + }); + }); + + var originImgNameOrId; + tt.test(' setup: find origin image', function (t) { + h.getTestKvmImg(t, function (err, imgId) { + t.ifError(err, 'getTestImg' + (err ? ': ' + err : '')); + originImgNameOrId = imgId; + t.end(); + }); + }); + + var pkgId; + tt.test(' setup: find test package', function (t) { + h.getTestKvmPkg(t, function (err, pkgId_) { + t.ifError(err, 'getTestPkg' + (err ? ': ' + err : '')); + pkgId = pkgId_; + t.end(); + }); + }); + + var markerFile = '/nodetritontest-was-here.txt'; + tt.test(' setup: triton create ... -n ' + ORIGIN_INST_ALIAS, function (t) { + var argv = ['create', '-wj', '-n', ORIGIN_INST_ALIAS, + '-m', 'user-script=touch ' + markerFile, + originImgNameOrId, pkgId]; + h.safeTriton(t, argv, function (err, stdout) { + var lines = h.jsonStreamParse(stdout); + originInst = lines[1]; + t.ok(originInst.id, 'originInst.id: ' + originInst.id); + t.equal(lines[1].state, 'running', 'originInst is running'); + t.end(); + }); + }); + + // TODO: I'd like to use this 'triton ssh INST touch $markerFile' to + // tweak the image. However, that current hangs when run via + // tape (don't know why yet). Instead we'll use a user-script to + // change the origin as our image change. + // + //tt.test(' setup: add marker to origin', function (t) { + // var argv = ['ssh', originInst.id, + // '-o', 'StrictHostKeyChecking=no', + // '-o', 'UserKnownHostsFile=/dev/null', + // 'touch', markerFile]; + // h.safeTriton(t, argv, function (err, stdout) { + // t.ifError(err, 'adding origin marker file, err=' + err); + // t.end(); + // }); + //}); + + tt.test(' triton image create ...', function (t) { + var argv = ['image', 'create', '-j', '-w', '-t', 'foo=bar', + originInst.id, IMAGE_DATA.name, IMAGE_DATA.version]; + h.safeTriton(t, argv, function (err, stdout) { + var lines = h.jsonStreamParse(stdout); + img = lines[1]; + t.ok(img, 'created image, id=' + img.id); + t.equal(img.name, IMAGE_DATA.name, 'img.name'); + t.equal(img.version, IMAGE_DATA.version, 'img.version'); + t.equal(img.public, false, 'img.public is false'); + t.equal(img.state, 'active', 'img.state is active'); + t.equal(img.origin, originInst.image, 'img.origin'); + t.end(); + }); + }); + + var derivedInst; + tt.test(' triton create ... -n ' + DERIVED_INST_ALIAS, function (t) { + var argv = ['create', '-wj', '-n', DERIVED_INST_ALIAS, img.id, pkgId]; + h.safeTriton(t, argv, function (err, stdout) { + var lines = h.jsonStreamParse(stdout); + derivedInst = lines[1]; + t.ok(derivedInst.id, 'derivedInst.id: ' + derivedInst.id); + t.equal(lines[1].state, 'running', 'derivedInst is running'); + t.end(); + }); + }); + + // TODO: Once have `triton ssh ...` working in test suite without hangs, + // then want to check that the created VM has the markerFile. + + // Remove instances. 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', originInst.id, derivedInst.id], + function () { + t.end(); + }); + }); + + tt.test(' cleanup: triton image rm', function (t) { + h.safeTriton(t, ['image', 'rm', '-f', img.id], function () { + t.end(); + }); + }); +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 8a573a6..75b6de5 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright 2017 Joyent, Inc. */ /* @@ -132,6 +132,9 @@ function triton(args, opts, cb) { * @param {Tape} t - tape test object * @param {Object|Array} opts - options object, or just the `triton` 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 + * to ignore `err`. */ function safeTriton(t, opts, cb) { assert.object(t, 't'); @@ -160,6 +163,7 @@ function safeTriton(t, opts, cb) { } + /* * Find and return an image that can be used for test provisions. We look * for an available base or minimal image. @@ -206,6 +210,47 @@ function getTestImg(t, cb) { } +/* + * Find and return an image that can be used for test *KVM* 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 getTestKvmImg(t, cb) { + if (CONFIG.kvmImage) { + assert.string(CONFIG.kvmPackage, 'CONFIG.kvmPackage'); + t.ok(CONFIG.kvmImage, 'kvmImage from config: ' + CONFIG.kvmImage); + cb(null, CONFIG.kvmImage); + 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 KVM image (using subset of supported names): %s (%s)', + imgId, imgRepr)); + cb(err, imgId); + }); +} + + /* * Find and return an package that can be used for test provisions. * @@ -222,6 +267,10 @@ function getTestPkg(t, cb) { safeTriton(t, ['pkg', 'ls', '-j'], function (err, stdout) { var pkgs = jsonStreamParse(stdout); + // Filter out 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; @@ -231,6 +280,36 @@ function getTestPkg(t, cb) { }); } +/* + * Find and return an package that can be used for *KVM* 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 getTestKvmPkg(t, cb) { + if (CONFIG.kvmPackage) { + assert.string(CONFIG.kvmPackage, 'CONFIG.kvmPackage'); + t.ok(CONFIG.kvmPackage, 'kvmPackage from config: ' + CONFIG.kvmPackage); + cb(null, CONFIG.kvmPackage); + return; + } + + 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 second smallest package name that can be used for * test provisions. @@ -323,26 +402,76 @@ function createTestInst(t, name, cb) { /* - * Remove test instance, if exists. + * Delete the given test instance (by name or id). It is not an error for the + * instance to not exist. I.e. this is somewhat like `rm -f FILE`. + * + * Once we've validated that the inst exists, it *is* an error if the delete + * fails. This function checks that with `t.ifErr`. + * + * @param {Tape} t - Tape test object on which to assert details. + * @param {String} instNameOrId - The instance name or id to delete. + * @param {Function} cb - `function ()`. A deletion error is NOT returned + * currently, because it is checked via `t.ifErr`. */ -function deleteTestInst(t, name, cb) { - triton(['inst', 'get', '-j', name], function (err, stdout, stderr) { +function deleteTestInst(t, instNameOrId, cb) { + assert.object(t, 't'); + assert.string(instNameOrId, 'instNameOrId'); + assert.func(cb, 'cb'); + + triton(['inst', 'get', '-j', instNameOrId], + function onInstGet(err, stdout, _) { if (err) { if (err.code === 3) { // `triton` code for ResourceNotFound - t.ok(true, 'no pre-existing alias in the way'); + t.ok(true, 'no existing inst ' + instNameOrId); + cb(); } else { - t.ifErr(err); + t.ifErr(err, err); + cb(); } - - return cb(); + } else { + var instToRm = JSON.parse(stdout); + safeTriton(t, ['inst', 'rm', '-w', instToRm.id], function onRm() { + t.ok(true, 'deleted inst ' + instToRm.id); + cb(); + }); } + }); +} - var oldInst = JSON.parse(stdout); +/* + * Delete the given test image (by name or id). It is not an error for the + * image to not exist. I.e. this is somewhat like `rm -f FILE`. + * + * Once we've validated that the image exists, it *is* an error if the delete + * fails. This function checks that with `t.ifErr`. + * + * @param {Tape} t - Tape test object on which to assert details. + * @param {String} imgNameOrId - The image name or id to delete. + * @param {Function} cb - `function ()`. A deletion error is NOT returned + * currently, because it is checked via `t.ifErr`. + */ +function deleteTestImg(t, imgNameOrId, cb) { + assert.object(t, 't'); + assert.string(imgNameOrId, 'imgNameOrId'); + assert.func(cb, 'cb'); - safeTriton(t, ['delete', '-w', oldInst.id], function (dErr) { - t.ifError(dErr, 'deleted old inst ' + oldInst.id); - cb(); - }); + triton(['img', 'get', '-j', imgNameOrId], + function onImgGet(err, stdout, _) { + if (err) { + if (err.code === 3) { // `triton` code for ResourceNotFound + t.ok(true, 'no existing img ' + imgNameOrId); + cb(); + } else { + t.ifErr(err, err); + cb(); + } + } else { + var imgToRm = JSON.parse(stdout); + safeTriton(t, ['img', 'rm', '-w', imgToRm.id], function onRm() { + t.ok(true, 'deleted img ' + imgToRm.id); + cb(); + }); + } }); } @@ -366,12 +495,18 @@ module.exports = { CONFIG: CONFIG, triton: triton, safeTriton: safeTriton, + createClient: createClient, createTestInst: createTestInst, deleteTestInst: deleteTestInst, + deleteTestImg: deleteTestImg, + getTestImg: getTestImg, + getTestKvmImg: getTestKvmImg, getTestPkg: getTestPkg, + getTestKvmPkg: getTestKvmPkg, getResizeTestPkg: getResizeTestPkg, + jsonStreamParse: jsonStreamParse, printConfig: printConfig,