From c86804cfe416360d6918a476f05de2c5cdd743e5 Mon Sep 17 00:00:00 2001 From: Todd Whiteman Date: Fri, 29 Jun 2018 16:44:17 -0700 Subject: [PATCH] TRITON-52 x-DC image copy Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 7 +++ lib/cloudapi2.js | 34 ++++++++++++ lib/do_image/do_copy.js | 119 ++++++++++++++++++++++++++++++++++++++++ lib/do_image/do_list.js | 2 +- lib/do_image/index.js | 2 + lib/tritonapi.js | 100 +++++++++++++++++++++++++++++++-- 6 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 lib/do_image/do_copy.js diff --git a/CHANGES.md b/CHANGES.md index e44c01b..8b5dca7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,13 @@ Known issues: --allow-shared-images cli option when calling `triton create` command to provision from a shared image (or clone the image then provision from the clone). +- [TRITON-52] x-DC image copy. A user can copy an image that they own into + another datacenter within the same cloud using the `triton image copy` cli + command. Example: + + ``` + triton -p us-east-1 image cp my-custom-image us-sw-1 + ``` ## 6.0.0 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index f98a773..945b8d5 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -1082,6 +1082,40 @@ CloudApi.prototype.cloneImage = function cloneImage(opts, cb) { }); }; +/** + * Import image from another datacenter in the same cloud. + * + * + * @param {Object} opts + * - {String} datacenter Required. The datacenter to import from. + * - {UUID} id Required. The id of the image to update. + * @param {Function} cb of the form `function (err, body, res)` + */ +CloudApi.prototype.importImageFromDatacenter = +function importImageFromDatacenter(opts, cb) { + assert.string(opts.datacenter, 'datacenter'); + assert.uuid(opts.id, 'id'); + assert.func(cb, 'cb'); + + var p = this._path(format('/%s/images', this.account), { + action: 'import-from-datacenter', + datacenter: opts.datacenter, + id: opts.id + }); + + this._request({ + method: 'POST', + path: p, + data: {} + }, 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_copy.js b/lib/do_image/do_copy.js new file mode 100644 index 0000000..88f673b --- /dev/null +++ b/lib/do_image/do_copy.js @@ -0,0 +1,119 @@ +/* + * 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 copy ...` + */ + +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +// ---- the command + +function do_copy(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: expected 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 copyImage(ctx, next) { + log.trace({dryRun: opts.dry_run, account: ctx.account, args: args}, + 'image copy'); + + if (opts.dry_run) { + next(); + return; + } + + tritonapi.copyImageToDatacenter( + {image: args[0], datacenter: args[1]}, + function (err, img) { + if (err) { + next(new errors.TritonError(err, 'error copying image')); + return; + } + + log.trace({img: img}, 'image copy result'); + + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Copied image %s to datacenter %s', + common.imageRepr(img), args[1]); + } + + next(); + }); + } + ]}, function (err) { + cb(err); + }); +} + +do_copy.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 copying.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + } +]; + +do_copy.synopses = [ + '{{name}} {{cmd}} [OPTIONS] IMAGE DATACENTER' +]; + +do_copy.help = [ + /* BEGIN JSSTYLED */ + 'Copy image to another datacenter.', + '', + '{{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).', + 'You must be the owner of the image to copy it. (You can use `triton image', + 'clone` to get your own image clone of an image shared to you.)', + '', + '"DATACENTER" is the name of the datacenter to which to copy your image.', + 'Use `triton datacenters` to show the available datacenter names.' + /* END JSSTYLED */ +].join('\n'); + +do_copy.aliases = ['cp']; + +// TODO: tritonimage should really be 'tritonownedimage' or something to +// limit to images owned by this account +// TODO: tritondatacenter bash completion +do_copy.completionArgtypes = ['tritonimage', 'tritondatacenter', 'none']; + +module.exports = do_copy; diff --git a/lib/do_image/do_list.js b/lib/do_image/do_list.js index 22eca82..89aaad9 100644 --- a/lib/do_image/do_list.js +++ b/lib/do_image/do_list.js @@ -132,7 +132,7 @@ function do_list(subcmd, opts, args, callback) { // Add image sharing flags. if (Array.isArray(img.acl) && img.acl.length > 0) { - assert.string(ctx.account, 'ctx.account'); + assert.string(ctx.account.id, 'ctx.account.id'); if (img.owner === ctx.account.id) { // This image has been shared with other accounts. flags.push('+'); diff --git a/lib/do_image/index.js b/lib/do_image/index.js index e719037..6ee6e4b 100644 --- a/lib/do_image/index.js +++ b/lib/do_image/index.js @@ -34,6 +34,7 @@ function ImageCLI(top) { 'list', 'get', 'clone', + 'copy', 'create', 'delete', 'export', @@ -53,6 +54,7 @@ ImageCLI.prototype.init = function init(opts, args, cb) { ImageCLI.prototype.do_list = require('./do_list'); ImageCLI.prototype.do_get = require('./do_get'); ImageCLI.prototype.do_clone = require('./do_clone'); +ImageCLI.prototype.do_copy = require('./do_copy'); ImageCLI.prototype.do_create = require('./do_create'); ImageCLI.prototype.do_delete = require('./do_delete'); ImageCLI.prototype.do_export = require('./do_export'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index f32e330..6e26c1d 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -493,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { ? true : !profile.insecure); var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; - var opts = { + self._cloudapiOpts = { url: profile.url, account: profile.actAsAccount || profile.account, principal: { @@ -509,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { if (profile.privKey) { var key = sshpk.parsePrivateKey(profile.privKey); this.keyPair = - opts.principal.keyPair = + self._cloudapiOpts.principal.keyPair = auth.KeyPair.fromPrivateKey(key); - this.cloudapi = cloudapi.createClient(opts); + this.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); } else { var kr = new auth.KeyRing(); @@ -521,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) { cb(err); return; } - self.keyPair = opts.principal.keyPair = kp; - self.cloudapi = cloudapi.createClient(opts); + self.keyPair = self._cloudapiOpts.principal.keyPair = kp; + self.cloudapi = cloudapi.createClient(self._cloudapiOpts); cb(null); }); } @@ -994,6 +994,96 @@ TritonApi.prototype.cloneImage = function cloneImage(opts, cb) }); }; +/** + * Copy an image to another Datacenter. + * + * Note: This somewhat flips the sense of the CloudAPI ImportImageFromDatacenter + * endpoint, in that it instead calls *the target DC* to pull from this + * profile's DC. The target DC's CloudAPI URL is determined from this DC's + * `ListDatacenters` endpoint. It is assumed that all other Triton profile + * attributes (account, keyId) suffice to auth with the target DC. + * + * @param {Object} opts + * - {String} datacenter The datacenter name to copy to. Required. + * - {String} image The image UUID, name, or short ID. Required. + * @param {Function} cb `function (err, img)` + * On failure `err` is an error instance, else it is null. + * On success: `img` is the copied image object. + */ +TritonApi.prototype.copyImageToDatacenter = +function copyImageToDatacenter(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.datacenter, 'opts.datacenter'); + assert.string(opts.image, 'opts.image'); + assert.func(cb, 'cb'); + + var arg = { + client: self, + datacenter: opts.datacenter, + image: opts.image + }; + var img; + + vasync.pipeline({arg: arg, funcs: [ + _stepImg, + function getDatacenters(ctx, next) { + self.cloudapi.listDatacenters({}, function (err, dcs, res) { + if (err) { + next(err); + return; + } + if (!dcs.hasOwnProperty(ctx.datacenter)) { + next(new errors.TritonError(format( + '"%s" is not a valid datacenter name, possible ' + + 'names are: %s', + ctx.datacenter, + Object.keys(dcs).join(', ')))); + return; + } + ctx.datacenterUrl = dcs[ctx.datacenter]; + assert.string(ctx.datacenterUrl, 'ctx.datacenterUrl'); + + // CloudAPI added image copying in 9.2.0, which is also + // the version that included this header. + var currentDcName = res.headers['triton-datacenter-name']; + if (!currentDcName) { + next(new errors.TritonError(err, format( + 'this datacenter does not support image copying (%s)', + res.headers['server']))); + return; + } + // Note: currentDcName is where the image currently resides. + ctx.currentDcName = currentDcName; + + next(); + }); + }, + function cloudApiImportImageFromDatacenter(ctx, next) { + var targetCloudapiOpts = jsprim.mergeObjects( + { + url: ctx.datacenterUrl, + log: self.log.child({datacenter: opts.datacenter}, true) + }, + null, + self._cloudapiOpts + ); + var targetCloudapi = cloudapi.createClient(targetCloudapiOpts); + + targetCloudapi.importImageFromDatacenter({ + datacenter: ctx.currentDcName, + id: ctx.img.id + }, function _importImageCb(err, img_) { + targetCloudapi.close(); + img = img_; + next(err); + }); + } + ]}, function (err) { + cb(err, img); + }); +}; + /** * Get an active package by ID, exact name, or short ID, in that order. *