TRITON-52 x-DC image copy
Reviewed by: Trent Mick <trentm@gmail.com> Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
parent
dc5dc12052
commit
c86804cfe4
@ -26,6 +26,13 @@ Known issues:
|
|||||||
--allow-shared-images cli option when calling `triton create` command to
|
--allow-shared-images cli option when calling `triton create` command to
|
||||||
provision from a shared image (or clone the image then provision from the
|
provision from a shared image (or clone the image then provision from the
|
||||||
clone).
|
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
|
## 6.0.0
|
||||||
|
|
||||||
|
@ -1082,6 +1082,40 @@ CloudApi.prototype.cloneImage = function cloneImage(opts, cb) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import image from another datacenter in the same cloud.
|
||||||
|
* <http://apidocs.joyent.com/cloudapi/#ImportImageFromDatacenter>
|
||||||
|
*
|
||||||
|
* @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.
|
* Wait for an image to go one of a set of specfic states.
|
||||||
*
|
*
|
||||||
|
119
lib/do_image/do_copy.js
Normal file
119
lib/do_image/do_copy.js
Normal file
@ -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;
|
@ -132,7 +132,7 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
// Add image sharing flags.
|
// Add image sharing flags.
|
||||||
if (Array.isArray(img.acl) && img.acl.length > 0) {
|
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) {
|
if (img.owner === ctx.account.id) {
|
||||||
// This image has been shared with other accounts.
|
// This image has been shared with other accounts.
|
||||||
flags.push('+');
|
flags.push('+');
|
||||||
|
@ -34,6 +34,7 @@ function ImageCLI(top) {
|
|||||||
'list',
|
'list',
|
||||||
'get',
|
'get',
|
||||||
'clone',
|
'clone',
|
||||||
|
'copy',
|
||||||
'create',
|
'create',
|
||||||
'delete',
|
'delete',
|
||||||
'export',
|
'export',
|
||||||
@ -53,6 +54,7 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
|
|||||||
ImageCLI.prototype.do_list = require('./do_list');
|
ImageCLI.prototype.do_list = require('./do_list');
|
||||||
ImageCLI.prototype.do_get = require('./do_get');
|
ImageCLI.prototype.do_get = require('./do_get');
|
||||||
ImageCLI.prototype.do_clone = require('./do_clone');
|
ImageCLI.prototype.do_clone = require('./do_clone');
|
||||||
|
ImageCLI.prototype.do_copy = require('./do_copy');
|
||||||
ImageCLI.prototype.do_create = require('./do_create');
|
ImageCLI.prototype.do_create = require('./do_create');
|
||||||
ImageCLI.prototype.do_delete = require('./do_delete');
|
ImageCLI.prototype.do_delete = require('./do_delete');
|
||||||
ImageCLI.prototype.do_export = require('./do_export');
|
ImageCLI.prototype.do_export = require('./do_export');
|
||||||
|
100
lib/tritonapi.js
100
lib/tritonapi.js
@ -493,7 +493,7 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
|
|||||||
? true : !profile.insecure);
|
? true : !profile.insecure);
|
||||||
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
|
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
|
||||||
|
|
||||||
var opts = {
|
self._cloudapiOpts = {
|
||||||
url: profile.url,
|
url: profile.url,
|
||||||
account: profile.actAsAccount || profile.account,
|
account: profile.actAsAccount || profile.account,
|
||||||
principal: {
|
principal: {
|
||||||
@ -509,9 +509,9 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
|
|||||||
if (profile.privKey) {
|
if (profile.privKey) {
|
||||||
var key = sshpk.parsePrivateKey(profile.privKey);
|
var key = sshpk.parsePrivateKey(profile.privKey);
|
||||||
this.keyPair =
|
this.keyPair =
|
||||||
opts.principal.keyPair =
|
self._cloudapiOpts.principal.keyPair =
|
||||||
auth.KeyPair.fromPrivateKey(key);
|
auth.KeyPair.fromPrivateKey(key);
|
||||||
this.cloudapi = cloudapi.createClient(opts);
|
this.cloudapi = cloudapi.createClient(self._cloudapiOpts);
|
||||||
cb(null);
|
cb(null);
|
||||||
} else {
|
} else {
|
||||||
var kr = new auth.KeyRing();
|
var kr = new auth.KeyRing();
|
||||||
@ -521,8 +521,8 @@ TritonApi.prototype._setupProfile = function _setupProfile(cb) {
|
|||||||
cb(err);
|
cb(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.keyPair = opts.principal.keyPair = kp;
|
self.keyPair = self._cloudapiOpts.principal.keyPair = kp;
|
||||||
self.cloudapi = cloudapi.createClient(opts);
|
self.cloudapi = cloudapi.createClient(self._cloudapiOpts);
|
||||||
cb(null);
|
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.
|
* Get an active package by ID, exact name, or short ID, in that order.
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user