2014-02-07 23:21:24 +02:00
|
|
|
/*
|
2015-09-04 21:12:20 +03:00
|
|
|
* 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/.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2016-03-02 10:05:06 +02:00
|
|
|
* Copyright 2016 Joyent, Inc.
|
2016-12-13 20:04:41 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
/* BEGIN JSSTYLED */
|
|
|
|
/*
|
|
|
|
* Core `TritonApi` client class. A TritonApi client object is a wrapper around
|
|
|
|
* a lower-level `CloudApi` client that makes raw calls to
|
|
|
|
* [Cloud API](https://apidocs.joyent.com/cloudapi/). The wrapper provides
|
|
|
|
* some conveniences, for example:
|
|
|
|
* - referring to resources by "shortId" (8-char UUID prefixes) or "name"
|
|
|
|
* (e.g. an VM instance has a unique name for an account, but the raw
|
|
|
|
* Cloud API only supports lookup by full UUID);
|
|
|
|
* - filling in of image details for instances which only have an "image_uuid"
|
|
|
|
* in Cloud API responses;
|
|
|
|
* - support for waiting for async operations to complete via "wait" parameters;
|
|
|
|
* - profile handling.
|
|
|
|
*
|
2016-12-15 00:33:29 +02:00
|
|
|
*
|
2016-12-13 20:04:41 +02:00
|
|
|
* Preparing a TritonApi is a three-step process. (Note: Some users might
|
|
|
|
* prefer to use the `createClient` convenience function in "index.js" that
|
|
|
|
* wraps up all three steps into a single call.)
|
|
|
|
*
|
|
|
|
* 1. Create the client object.
|
|
|
|
* 2. Initialize it (mainly involves finding the SSH key identified by the
|
|
|
|
* `keyId`).
|
|
|
|
* 3. Optionally, unlock the SSH key (if it is passphrase-protected and not in
|
|
|
|
* an ssh-agent). If you know that your key is not passphrase-protected
|
|
|
|
* or is an ssh-agent, then you can skip this step. The failure mode for
|
|
|
|
* a locked key looks like this:
|
|
|
|
* SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use.
|
|
|
|
* at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12)
|
|
|
|
* at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23)
|
|
|
|
* at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22)
|
|
|
|
*
|
2016-12-15 00:33:29 +02:00
|
|
|
* # Usage
|
|
|
|
*
|
2016-12-13 20:04:41 +02:00
|
|
|
* var mod_triton = require('triton');
|
|
|
|
*
|
|
|
|
* // 1. Create the TritonApi instance.
|
|
|
|
* var client = mod_triton.createTritonApiClient({
|
|
|
|
* log: log,
|
|
|
|
* profile: profile, // See mod_triton.loadProfile
|
|
|
|
* config: config // See mod_triton.loadConfig
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* // 2. Call `init` to setup the profile. This involves finding the SSH
|
|
|
|
* // key identified by the profile's keyId.
|
|
|
|
* client.init(function (initErr) {
|
|
|
|
* if (initErr) boom(initErr);
|
|
|
|
*
|
|
|
|
* // 3. Unlock the SSH key, if necessary. Possibilities are:
|
|
|
|
* // (a) Skip this step. If the key is locked, you will get a
|
|
|
|
* // "SigningError" at first attempt to sign. See example above.
|
|
|
|
* // (b) The key is not locked.
|
|
|
|
* // `client.keyPair.isLocked() === false`
|
|
|
|
* // (c) You have a passphrase for the key:
|
|
|
|
* if (client.keyPair.isLocked()) {
|
|
|
|
* // This throws if the passphrase is incorrect.
|
|
|
|
* client.keyPair.unlock(passphrase);
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // (d) Or you use a function that will prompt for a passphrase
|
|
|
|
* // and unlock with that. E.g., `promptPassphraseUnlockKey`
|
|
|
|
* // is one provided by this package that with prompt on stdin.
|
|
|
|
* mod_triton.promptPassphraseUnlockKey({
|
|
|
|
* tritonapi: client
|
|
|
|
* }, function (unlockErr) {
|
|
|
|
* if (unlockErr) boom(unlockErr);
|
2014-02-07 23:21:24 +02:00
|
|
|
*
|
2016-12-13 20:04:41 +02:00
|
|
|
* // 4. Now you can finally make an API call. For example:
|
|
|
|
* client.listImages(function (err, imgs) {
|
|
|
|
* // ...
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
* });
|
2016-12-15 00:33:29 +02:00
|
|
|
*
|
|
|
|
*
|
|
|
|
* # TritonApi method callback patterns
|
|
|
|
*
|
|
|
|
* Guidelines for the `cb` callback form for TritonApi methods are as follows:
|
|
|
|
*
|
|
|
|
* - Methods that delete a resource (i.e. call DELETE endpoints on cloudapi)
|
|
|
|
* should have a callback of one of the following forms:
|
|
|
|
* function (err)
|
|
|
|
* function (err, res) # if 'res' is useful to caller
|
|
|
|
* where `res` is the response object. The latter form is used if there
|
|
|
|
* is a reasonable use case for a caller needing it.
|
|
|
|
*
|
|
|
|
* - Other methods should have a callback of one of the following forms:
|
|
|
|
* function (err, theThing)
|
|
|
|
* function (err, theThing, res)
|
|
|
|
* function (err, _, res) # no meaningful body; useful 'res'
|
|
|
|
* function (err)
|
|
|
|
* `res` is the response object (from the original cloudapi request, in
|
|
|
|
* the case of methods that make an async request, and then poll waiting
|
|
|
|
* for completion). `theThing` is an endpoint-specific object. Typically it
|
|
|
|
* is the parsed JSON body from the cloudapi response. In some cases there
|
|
|
|
* is no meaningful response body (e.g. for RenameMachine), but the res can
|
|
|
|
* be useful. Here we use `_` to put a placeholder for the body, and keep
|
|
|
|
* `res` in the third position.
|
2014-02-07 23:21:24 +02:00
|
|
|
*/
|
2016-12-13 20:04:41 +02:00
|
|
|
/* END JSSTYLED */
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
var assert = require('assert-plus');
|
2014-02-20 05:52:58 +02:00
|
|
|
var auth = require('smartdc-auth');
|
2014-02-08 10:15:26 +02:00
|
|
|
var EventEmitter = require('events').EventEmitter;
|
2014-02-07 23:21:24 +02:00
|
|
|
var fs = require('fs');
|
2015-08-26 20:02:01 +03:00
|
|
|
var format = require('util').format;
|
2015-09-04 01:12:08 +03:00
|
|
|
var mkdirp = require('mkdirp');
|
2015-07-26 08:45:20 +03:00
|
|
|
var once = require('once');
|
2014-02-07 23:21:24 +02:00
|
|
|
var path = require('path');
|
2015-08-25 23:11:40 +03:00
|
|
|
var restifyClients = require('restify-clients');
|
2015-09-02 20:47:06 +03:00
|
|
|
// We are cheating here. restify-clients should export its 'bunyan'.
|
|
|
|
var restifyBunyanSerializers =
|
|
|
|
require('restify-clients/lib/helpers/bunyan').serializers;
|
2015-08-26 06:53:48 +03:00
|
|
|
var tabula = require('tabula');
|
2015-08-27 03:21:27 +03:00
|
|
|
var vasync = require('vasync');
|
2016-12-13 20:04:41 +02:00
|
|
|
var sshpk = require('sshpk');
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2014-02-20 05:52:58 +02:00
|
|
|
var cloudapi = require('./cloudapi2');
|
2014-02-07 23:21:24 +02:00
|
|
|
var common = require('./common');
|
2015-07-26 08:45:20 +03:00
|
|
|
var errors = require('./errors');
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
|
2015-11-25 21:04:44 +02:00
|
|
|
// ---- globals
|
|
|
|
|
|
|
|
var CLOUDAPI_ACCEPT_VERSION = '~8||~7';
|
|
|
|
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2015-11-13 02:04:12 +02:00
|
|
|
// ---- internal support stuff
|
|
|
|
|
|
|
|
function _assertRoleTagResourceType(resourceType, errName) {
|
|
|
|
assert.string(resourceType, errName);
|
|
|
|
var knownResourceTypes = ['resource', 'instance', 'image',
|
|
|
|
'package', 'network'];
|
|
|
|
assert.ok(knownResourceTypes.indexOf(resourceType) !== -1,
|
|
|
|
'unknown resource type: ' + resourceType);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _roleTagResourceUrl(account, type, id) {
|
|
|
|
var ns = {
|
|
|
|
instance: 'machines',
|
|
|
|
image: 'images',
|
|
|
|
'package': 'packages',
|
|
|
|
network: 'networks'
|
|
|
|
}[type];
|
|
|
|
assert.ok(ns, 'unknown resource type: ' + type);
|
|
|
|
|
|
|
|
return format('/%s/%s/%s', account, ns, id);
|
|
|
|
}
|
|
|
|
|
2016-02-09 22:23:55 +02:00
|
|
|
/**
|
|
|
|
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id`
|
|
|
|
* instance name, shortid or uuid, and determines the instance id (setting it
|
|
|
|
* as `arg.instId`).
|
|
|
|
*/
|
|
|
|
function _stepInstId(arg, next) {
|
|
|
|
assert.object(arg.client, 'arg.client');
|
|
|
|
assert.string(arg.id, 'arg.id');
|
|
|
|
|
|
|
|
if (common.isUUID(arg.id)) {
|
|
|
|
arg.instId = arg.id;
|
|
|
|
next();
|
|
|
|
} else {
|
|
|
|
arg.client.getInstance({
|
|
|
|
id: arg.id,
|
|
|
|
fields: ['id']
|
|
|
|
}, function (err, inst) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
} else {
|
|
|
|
arg.instId = inst.id;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2015-11-13 02:04:12 +02:00
|
|
|
|
2017-02-17 03:00:32 +02:00
|
|
|
/**
|
|
|
|
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.package`
|
|
|
|
* package name, short id or uuid, and determines the package id (setting it
|
|
|
|
* as `arg.pkgId`). Also sets `arg.pkgName` so that we can use this to test when
|
|
|
|
* the instance has been updated.
|
|
|
|
*/
|
|
|
|
function _stepPkgId(arg, next) {
|
|
|
|
assert.object(arg.client, 'arg.client');
|
|
|
|
assert.string(arg.package, 'arg.package');
|
|
|
|
|
|
|
|
arg.client.getPackage(arg.package, function (err, pkg) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
} else {
|
|
|
|
arg.pkgId = pkg.id;
|
|
|
|
arg.pkgName = pkg.name;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-07 23:44:35 +03:00
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
*/
|
|
|
|
function _stepImgId(arg, next) {
|
|
|
|
assert.object(arg.client, 'arg.client');
|
|
|
|
assert.string(arg.image, 'arg.image');
|
|
|
|
|
|
|
|
arg.client.getImage(arg.image, function (err, img) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
} else {
|
|
|
|
arg.imgId = img.id;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-02-04 15:39:50 +02:00
|
|
|
/**
|
|
|
|
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id`
|
|
|
|
* fwrule shortid or uuid, and determines the fwrule id (setting it
|
|
|
|
* as `arg.fwruleId`).
|
|
|
|
*
|
|
|
|
* If the fwrule *was* retrieved, that is set as `arg.fwrule`.
|
|
|
|
*/
|
|
|
|
function _stepFwRuleId(arg, next) {
|
|
|
|
assert.object(arg.client, 'arg.client');
|
|
|
|
assert.string(arg.id, 'arg.id');
|
|
|
|
|
|
|
|
if (common.isUUID(arg.id)) {
|
|
|
|
arg.fwruleId = arg.id;
|
|
|
|
next();
|
|
|
|
} else {
|
|
|
|
arg.client.getFirewallRule(arg.id, function (err, fwrule) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
} else {
|
|
|
|
arg.fwruleId = fwrule.id;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2015-11-13 02:04:12 +02:00
|
|
|
|
2015-09-04 21:04:45 +03:00
|
|
|
//---- TritonApi class
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
/**
|
2015-09-04 21:04:45 +03:00
|
|
|
* Create a TritonApi client.
|
2014-02-07 23:21:24 +02:00
|
|
|
*
|
2016-12-13 20:04:41 +02:00
|
|
|
* Public properties (TODO: doc all of these):
|
|
|
|
* - profile
|
|
|
|
* - config
|
|
|
|
* - log
|
|
|
|
* - cacheDir (only available if configured with a configDir)
|
|
|
|
* - keyPair (available after init)
|
|
|
|
* - cloudapi (available after init)
|
|
|
|
*
|
2015-09-08 19:55:48 +03:00
|
|
|
* @param opts {Object}
|
2014-02-07 23:21:24 +02:00
|
|
|
* - log {Bunyan Logger}
|
2015-08-27 03:21:27 +03:00
|
|
|
* ...
|
2014-02-07 23:21:24 +02:00
|
|
|
*/
|
2015-09-08 19:55:48 +03:00
|
|
|
function TritonApi(opts) {
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.object(opts.log, 'opts.log');
|
|
|
|
assert.object(opts.profile, 'opts.profile');
|
|
|
|
assert.object(opts.config, 'opts.config');
|
|
|
|
|
|
|
|
this.profile = opts.profile;
|
|
|
|
this.config = opts.config;
|
2016-12-13 20:04:41 +02:00
|
|
|
this.keyPair = null;
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2014-02-20 05:52:58 +02:00
|
|
|
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
|
|
|
|
// Note: This was fixed in restify, then broken again in
|
|
|
|
// https://github.com/mcavage/node-restify/pull/501
|
2015-09-08 19:55:48 +03:00
|
|
|
if (opts.log.serializers &&
|
|
|
|
(!opts.log.serializers.client_req ||
|
|
|
|
!opts.log.serializers.client_req)) {
|
|
|
|
this.log = opts.log.child({
|
2015-09-02 20:47:06 +03:00
|
|
|
serializers: restifyBunyanSerializers
|
2014-02-20 05:52:58 +02:00
|
|
|
});
|
|
|
|
} else {
|
2015-09-08 19:55:48 +03:00
|
|
|
this.log = opts.log;
|
2014-02-20 05:52:58 +02:00
|
|
|
}
|
2015-09-08 19:55:48 +03:00
|
|
|
|
2015-12-08 21:59:45 +02:00
|
|
|
if (this.config._configDir) {
|
2015-09-08 19:55:48 +03:00
|
|
|
this.cacheDir = path.resolve(this.config._configDir,
|
|
|
|
this.config.cacheDir,
|
2015-11-13 02:04:12 +02:00
|
|
|
common.profileSlug(this.profile));
|
2015-09-03 06:48:14 +03:00
|
|
|
this.log.trace({cacheDir: this.cacheDir}, 'cache dir');
|
|
|
|
}
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-12-08 21:59:45 +02:00
|
|
|
TritonApi.prototype.close = function close() {
|
2016-12-13 20:04:41 +02:00
|
|
|
if (this.cloudapi) {
|
|
|
|
this.cloudapi.close();
|
|
|
|
delete this.cloudapi;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
TritonApi.prototype.init = function init(cb) {
|
|
|
|
var self = this;
|
|
|
|
if (this.cacheDir) {
|
|
|
|
fs.exists(this.cacheDir, function (exists) {
|
|
|
|
if (!exists) {
|
|
|
|
mkdirp(self.cacheDir, function (err) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self._setupProfile(cb);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
self._setupProfile(cb);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
self._setupProfile(cb);
|
|
|
|
}
|
2015-12-08 21:59:45 +02:00
|
|
|
};
|
|
|
|
|
2016-12-13 20:04:41 +02:00
|
|
|
TritonApi.prototype._setupProfile = function _setupProfile(cb) {
|
|
|
|
var self = this;
|
|
|
|
var profile = this.profile;
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
assert.object(profile, 'profile');
|
|
|
|
assert.string(profile.account, 'profile.account');
|
2015-12-02 20:52:47 +02:00
|
|
|
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
|
2015-08-25 23:11:40 +03:00
|
|
|
assert.string(profile.keyId, 'profile.keyId');
|
|
|
|
assert.string(profile.url, 'profile.url');
|
2015-11-04 01:40:59 +02:00
|
|
|
assert.optionalString(profile.user, 'profile.user');
|
2016-06-08 00:19:06 +03:00
|
|
|
assert.optionalArrayOfString(profile.roles, 'profile.roles');
|
2015-08-25 23:11:40 +03:00
|
|
|
assert.optionalString(profile.privKey, 'profile.privKey');
|
|
|
|
assert.optionalBool(profile.insecure, 'profile.insecure');
|
2015-11-25 21:04:44 +02:00
|
|
|
assert.optionalString(profile.acceptVersion, 'profile.acceptVersion');
|
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
var rejectUnauthorized = (profile.insecure === undefined
|
|
|
|
? true : !profile.insecure);
|
2015-11-25 21:04:44 +02:00
|
|
|
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
|
2015-08-25 23:11:40 +03:00
|
|
|
|
2016-12-13 20:04:41 +02:00
|
|
|
var opts = {
|
2015-08-25 23:11:40 +03:00
|
|
|
url: profile.url,
|
2015-12-02 20:52:47 +02:00
|
|
|
account: profile.actAsAccount || profile.account,
|
2016-12-13 20:04:41 +02:00
|
|
|
principal: {
|
|
|
|
account: profile.account,
|
|
|
|
user: profile.user
|
|
|
|
},
|
2016-06-08 00:19:06 +03:00
|
|
|
roles: profile.roles,
|
2015-11-25 21:04:44 +02:00
|
|
|
version: acceptVersion,
|
2015-08-25 23:11:40 +03:00
|
|
|
rejectUnauthorized: rejectUnauthorized,
|
|
|
|
log: this.log
|
2016-12-13 20:04:41 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if (profile.privKey) {
|
|
|
|
var key = sshpk.parsePrivateKey(profile.privKey);
|
|
|
|
this.keyPair =
|
|
|
|
opts.principal.keyPair =
|
|
|
|
auth.KeyPair.fromPrivateKey(key);
|
|
|
|
this.cloudapi = cloudapi.createClient(opts);
|
|
|
|
cb(null);
|
|
|
|
} else {
|
|
|
|
var kr = new auth.KeyRing();
|
|
|
|
var fp = sshpk.parseFingerprint(profile.keyId);
|
|
|
|
kr.findSigningKeyPair(fp, function (err, kp) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self.keyPair = opts.principal.keyPair = kp;
|
|
|
|
self.cloudapi = cloudapi.createClient(opts);
|
|
|
|
cb(null);
|
|
|
|
});
|
|
|
|
}
|
2015-07-26 08:45:20 +03:00
|
|
|
};
|
|
|
|
|
2015-09-08 19:55:48 +03:00
|
|
|
|
|
|
|
TritonApi.prototype._cachePutJson = function _cachePutJson(key, obj, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.string(this.cacheDir, 'this.cacheDir');
|
|
|
|
assert.string(key, 'key');
|
|
|
|
assert.object(obj, 'obj');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var keyPath = path.resolve(this.cacheDir, key);
|
|
|
|
var data = JSON.stringify(obj);
|
|
|
|
fs.writeFile(keyPath, data, {encoding: 'utf8'}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
self.log.info({err: err, keyPath: keyPath}, 'error caching');
|
|
|
|
}
|
|
|
|
cb();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-02-15 20:12:10 +02:00
|
|
|
/**
|
|
|
|
* Lookup the given key in the cache and return a hit or `undefined`.
|
|
|
|
*
|
|
|
|
* @param {String} key: The cache key, e.g. 'images.json'.
|
2016-12-22 23:27:13 +02:00
|
|
|
* @param {Number} ttl: The number of seconds the cached data is valid.
|
2016-02-15 20:12:10 +02:00
|
|
|
* @param {Function} cb: `function (err, hit)`.
|
|
|
|
* `err` is an Error if there was an unexpected error loading from the
|
|
|
|
* cache. `hit` is undefined if there was no cache hit. On a hit, the
|
|
|
|
* type of `hit` depends on the key.
|
|
|
|
*/
|
2016-12-22 23:27:13 +02:00
|
|
|
TritonApi.prototype._cacheGetJson = function _cacheGetJson(key, ttl, cb) {
|
2015-09-08 19:55:48 +03:00
|
|
|
var self = this;
|
|
|
|
assert.string(this.cacheDir, 'this.cacheDir');
|
|
|
|
assert.string(key, 'key');
|
2016-12-22 23:27:13 +02:00
|
|
|
assert.number(ttl, 'ttl');
|
2015-09-08 19:55:48 +03:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var keyPath = path.resolve(this.cacheDir, key);
|
2016-12-20 05:00:00 +02:00
|
|
|
fs.stat(keyPath, function (statErr, stats) {
|
|
|
|
if (!statErr &&
|
2016-12-22 23:27:13 +02:00
|
|
|
// TTL is in seconds so we need to multiply by 1000.
|
|
|
|
stats.mtime.getTime() + (ttl * 1000) >= (new Date()).getTime()) {
|
2016-12-20 05:00:00 +02:00
|
|
|
fs.readFile(keyPath, 'utf8', function (err, data) {
|
|
|
|
if (err && err.code === 'ENOENT') {
|
|
|
|
self.log.trace({keyPath: keyPath},
|
|
|
|
'cache file does not exist');
|
|
|
|
cb();
|
|
|
|
} else if (err) {
|
|
|
|
self.log.warn({err: err, keyPath: keyPath},
|
|
|
|
'error reading cache file');
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
var obj;
|
|
|
|
try {
|
|
|
|
obj = JSON.parse(data);
|
|
|
|
} catch (dataErr) {
|
|
|
|
self.log.trace({err: dataErr, keyPath: keyPath},
|
|
|
|
'error parsing JSON cache file, removing');
|
|
|
|
fs.unlink(keyPath, function (err2) {
|
|
|
|
if (err2) {
|
|
|
|
self.log.warn({err: err2},
|
|
|
|
'failed to remove JSON cache file');
|
|
|
|
}
|
|
|
|
cb();
|
|
|
|
});
|
|
|
|
return;
|
2015-09-22 00:00:58 +03:00
|
|
|
}
|
2016-12-20 05:00:00 +02:00
|
|
|
cb(null, obj);
|
2015-09-22 00:00:58 +03:00
|
|
|
});
|
2016-12-20 05:00:00 +02:00
|
|
|
} else if (statErr && statErr.code !== 'ENOENT') {
|
|
|
|
cb(statErr);
|
|
|
|
} else {
|
|
|
|
cb();
|
2015-09-22 00:00:58 +03:00
|
|
|
}
|
2015-09-08 19:55:48 +03:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-08-26 19:59:12 +03:00
|
|
|
/**
|
2015-09-21 20:41:13 +03:00
|
|
|
* CloudAPI listImages wrapper with optional caching.
|
|
|
|
*
|
|
|
|
* @param opts {Object} Optional.
|
2015-12-30 00:53:49 +02:00
|
|
|
* - useCache {Boolean} Default false. Whether to use Triton's local cache.
|
2016-09-17 02:01:38 +03:00
|
|
|
* Currently the cache is only used and updated if the filters are
|
|
|
|
* exactly `{state: "all"}`. IOW, the ListImages call that returns
|
|
|
|
* all visible images.
|
2015-12-30 00:53:49 +02:00
|
|
|
* - ... all other cloudapi ListImages options per
|
|
|
|
* <https://apidocs.joyent.com/cloudapi/#ListImages>
|
2015-09-21 20:41:13 +03:00
|
|
|
* @param {Function} callback `function (err, imgs)`
|
2015-08-26 19:59:12 +03:00
|
|
|
*/
|
2015-09-04 21:04:45 +03:00
|
|
|
TritonApi.prototype.listImages = function listImages(opts, cb) {
|
2015-08-26 19:59:12 +03:00
|
|
|
var self = this;
|
|
|
|
if (cb === undefined) {
|
|
|
|
cb = opts;
|
|
|
|
opts = {};
|
|
|
|
}
|
|
|
|
assert.object(opts, 'opts');
|
2015-08-27 03:21:27 +03:00
|
|
|
assert.optionalBool(opts.useCache, 'opts.useCache');
|
2015-08-26 19:59:12 +03:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2015-09-21 20:41:13 +03:00
|
|
|
var listOpts = common.objCopy(opts);
|
|
|
|
delete listOpts.useCache;
|
|
|
|
|
2015-12-30 00:53:49 +02:00
|
|
|
// For now at least, we only cache full results (no filtering).
|
|
|
|
var useCache = Boolean(opts.useCache);
|
|
|
|
var cacheKey;
|
2016-09-17 02:01:38 +03:00
|
|
|
if (Object.keys(listOpts).length === 1 && listOpts.state === 'all') {
|
2015-12-30 00:53:49 +02:00
|
|
|
cacheKey = 'images.json';
|
2016-09-17 02:01:38 +03:00
|
|
|
} else {
|
|
|
|
useCache = false;
|
2015-12-30 00:53:49 +02:00
|
|
|
}
|
2015-09-08 19:55:48 +03:00
|
|
|
var cached;
|
|
|
|
var fetched;
|
|
|
|
var res;
|
2015-08-26 19:59:12 +03:00
|
|
|
|
2015-09-08 19:55:48 +03:00
|
|
|
vasync.pipeline({funcs: [
|
|
|
|
function tryCache(_, next) {
|
2015-12-30 00:53:49 +02:00
|
|
|
if (!useCache) {
|
2015-09-08 19:55:48 +03:00
|
|
|
return next();
|
2015-08-26 19:59:12 +03:00
|
|
|
}
|
2016-12-22 23:27:13 +02:00
|
|
|
self._cacheGetJson(cacheKey, 5*60, function (err, cached_) {
|
2015-09-08 19:55:48 +03:00
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
cached = cached_;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function listImgs(_, next) {
|
|
|
|
if (cached) {
|
|
|
|
return next();
|
2015-08-26 19:59:12 +03:00
|
|
|
}
|
|
|
|
|
2015-09-21 20:41:13 +03:00
|
|
|
self.cloudapi.listImages(listOpts, function (err, imgs, res_) {
|
2015-09-08 19:55:48 +03:00
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
fetched = imgs;
|
|
|
|
res = res_;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function cacheFetched(_, next) {
|
2015-12-30 00:53:49 +02:00
|
|
|
if (cacheKey && fetched) {
|
|
|
|
self._cachePutJson(cacheKey, fetched, next);
|
|
|
|
} else {
|
|
|
|
next();
|
2015-08-26 19:59:12 +03:00
|
|
|
}
|
2015-09-08 19:55:48 +03:00
|
|
|
}
|
2015-08-26 19:59:12 +03:00
|
|
|
|
2015-09-08 19:55:48 +03:00
|
|
|
]}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
cb(err, null, res);
|
|
|
|
} else {
|
|
|
|
cb(null, fetched || cached, res);
|
|
|
|
}
|
|
|
|
});
|
2015-08-26 19:59:12 +03:00
|
|
|
};
|
2015-07-26 08:45:20 +03:00
|
|
|
|
2015-09-08 19:55:48 +03:00
|
|
|
|
2015-07-26 08:45:20 +03:00
|
|
|
/**
|
2015-08-26 19:15:17 +03:00
|
|
|
* Get an image by ID, exact name, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If there is more than one image with that name, then the latest
|
|
|
|
* (by published_at) is returned.
|
2015-07-26 08:45:20 +03:00
|
|
|
*/
|
2015-10-05 23:34:24 +03:00
|
|
|
TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
if (typeof (opts) === 'string')
|
|
|
|
opts = {name: opts};
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.name, 'opts.name');
|
|
|
|
assert.optionalBool(opts.useCache, 'opts.useCache');
|
2015-08-26 06:53:48 +03:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2015-10-05 23:34:24 +03:00
|
|
|
var img;
|
|
|
|
if (common.isUUID(opts.name)) {
|
|
|
|
vasync.pipeline({funcs: [
|
|
|
|
function tryCache(_, next) {
|
|
|
|
if (!opts.useCache) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2015-12-30 00:53:49 +02:00
|
|
|
var cacheKey = 'images.json';
|
2016-12-22 23:27:13 +02:00
|
|
|
self._cacheGetJson(cacheKey, 60*60, function (err, images) {
|
2015-10-05 23:34:24 +03:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
2016-02-14 18:58:29 +02:00
|
|
|
if (images) {
|
|
|
|
for (var i = 0; i < images.length; i++) {
|
|
|
|
if (images[i].id === opts.name) {
|
|
|
|
img = images[i];
|
|
|
|
break;
|
|
|
|
}
|
2015-10-07 09:33:18 +03:00
|
|
|
}
|
|
|
|
}
|
2015-10-05 23:34:24 +03:00
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function cloudApiGetImage(_, next) {
|
|
|
|
if (img !== undefined) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2015-10-07 09:28:25 +03:00
|
|
|
self.cloudapi.getImage({id: opts.name}, function (err, img_) {
|
|
|
|
img = img_;
|
2015-10-07 09:09:52 +03:00
|
|
|
if (err && err.restCode === 'ResourceNotFound') {
|
2015-10-07 09:33:18 +03:00
|
|
|
err = new errors.ResourceNotFoundError(err, format(
|
|
|
|
'image with id %s was not found', opts.name));
|
2015-10-07 09:09:52 +03:00
|
|
|
}
|
2015-10-05 23:34:24 +03:00
|
|
|
next(err);
|
|
|
|
});
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
2015-10-07 09:28:25 +03:00
|
|
|
]}, function done(err) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb(null, img);
|
2015-10-05 23:34:24 +03:00
|
|
|
}
|
2015-10-07 09:28:25 +03:00
|
|
|
});
|
2015-08-26 06:53:48 +03:00
|
|
|
} else {
|
2015-10-05 23:34:24 +03:00
|
|
|
var s = opts.name.split('@');
|
|
|
|
var name = s[0];
|
2015-09-21 23:37:26 +03:00
|
|
|
var version = s[1];
|
2016-01-26 09:23:36 +02:00
|
|
|
var nameSelector;
|
2015-09-22 00:16:47 +03:00
|
|
|
|
2016-01-19 22:30:46 +02:00
|
|
|
var listOpts = {
|
|
|
|
// Explicitly include inactive images.
|
|
|
|
state: 'all'
|
|
|
|
};
|
2015-09-22 01:57:53 +03:00
|
|
|
if (version) {
|
2016-01-26 09:23:36 +02:00
|
|
|
nameSelector = name + '@' + version;
|
2015-10-07 09:28:25 +03:00
|
|
|
listOpts.name = name;
|
|
|
|
listOpts.version = version;
|
2016-01-12 20:27:46 +02:00
|
|
|
// XXX This is bogus now?
|
2015-10-07 09:28:25 +03:00
|
|
|
listOpts.useCache = opts.useCache;
|
2016-01-26 09:23:36 +02:00
|
|
|
} else {
|
|
|
|
nameSelector = name;
|
2015-09-22 01:57:53 +03:00
|
|
|
}
|
2015-10-07 09:28:25 +03:00
|
|
|
this.cloudapi.listImages(listOpts, function (err, imgs) {
|
2015-08-26 06:53:48 +03:00
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 06:53:48 +03:00
|
|
|
var nameMatches = [];
|
2015-08-26 19:15:17 +03:00
|
|
|
var shortIdMatches = [];
|
2015-08-26 06:53:48 +03:00
|
|
|
for (var i = 0; i < imgs.length; i++) {
|
2015-10-05 23:34:24 +03:00
|
|
|
img = imgs[i];
|
2015-08-26 19:15:17 +03:00
|
|
|
if (img.name === name) {
|
|
|
|
nameMatches.push(img);
|
|
|
|
}
|
2015-09-21 23:37:26 +03:00
|
|
|
if (common.uuidToShortId(img.id) === name) {
|
2015-08-26 19:15:17 +03:00
|
|
|
shortIdMatches.push(img);
|
2015-07-26 08:45:20 +03:00
|
|
|
}
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 19:15:17 +03:00
|
|
|
if (nameMatches.length === 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(null, nameMatches[0]);
|
2015-08-26 19:15:17 +03:00
|
|
|
} else if (nameMatches.length > 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
tabula.sortArrayOfObjects(nameMatches, 'published_at');
|
|
|
|
cb(null, nameMatches[nameMatches.length - 1]);
|
2015-08-26 19:15:17 +03:00
|
|
|
} else if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
2016-01-26 09:23:36 +02:00
|
|
|
'no image with %s or short id "%s" was found',
|
|
|
|
nameSelector, name)));
|
2015-08-26 19:15:17 +03:00
|
|
|
} else {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(
|
2016-01-26 09:23:36 +02:00
|
|
|
format('no image with %s "%s" was found '
|
|
|
|
+ 'and "%s" is an ambiguous short id',
|
|
|
|
nameSelector, name, name)));
|
2015-07-26 08:45:20 +03:00
|
|
|
}
|
2015-08-26 06:53:48 +03:00
|
|
|
});
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-04-07 23:44:35 +03:00
|
|
|
/**
|
|
|
|
* Export and image to Manta.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} image The image UUID, name, or short ID. Required.
|
|
|
|
* - {String} manta_path The path in Manta where the image will be
|
|
|
|
* exported. Required.
|
|
|
|
* @param {Function} cb `function (err, exportInfo, res)`
|
|
|
|
* On failure `err` is an error instance, else it is null.
|
|
|
|
* On success: `exportInfo` is an object with three properties:
|
|
|
|
* - {String} manta_url The url of the Manta API endpoint where the
|
|
|
|
* image was exported.
|
|
|
|
* - {String} manifest_path The pathname in Manta of the exported image
|
|
|
|
* manifest.
|
|
|
|
* - {String} image_path The pathname in Manta of the exported image.
|
|
|
|
* and `res` is the CloudAPI `ExportImage` response.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.exportImage = function exportImage(opts, cb)
|
|
|
|
{
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.image, 'opts.image');
|
|
|
|
assert.string(opts.manta_path, 'opts.manta_path');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var res = null;
|
|
|
|
var exportInfo = null;
|
|
|
|
var arg = {
|
|
|
|
image: opts.image,
|
|
|
|
client: self
|
|
|
|
};
|
|
|
|
|
|
|
|
vasync.pipeline({arg: arg, funcs: [
|
|
|
|
_stepImgId,
|
|
|
|
function cloudApiExportImage(ctx, next) {
|
|
|
|
self.cloudapi.exportImage({
|
|
|
|
id: ctx.imgId, manta_path: opts.manta_path },
|
|
|
|
function (err, exportInfo_, res_) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
exportInfo = exportInfo_;
|
|
|
|
res = res_;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
cb(err, exportInfo, res);
|
|
|
|
} else {
|
|
|
|
cb(null, exportInfo, res);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2015-07-26 08:45:20 +03:00
|
|
|
/**
|
2015-08-26 20:02:01 +03:00
|
|
|
* Get an active package by ID, exact name, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If there is more than one package with that name, then this errors out.
|
2015-07-26 08:45:20 +03:00
|
|
|
*/
|
2015-09-04 21:04:45 +03:00
|
|
|
TritonApi.prototype.getPackage = function getPackage(name, cb) {
|
2015-08-26 06:53:48 +03:00
|
|
|
assert.string(name, 'name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2015-09-22 00:00:58 +03:00
|
|
|
if (common.isUUID(name)) {
|
2015-08-26 06:53:48 +03:00
|
|
|
this.cloudapi.getPackage({id: name}, function (err, pkg) {
|
|
|
|
if (err) {
|
2015-10-07 09:09:52 +03:00
|
|
|
if (err.restCode === 'ResourceNotFound') {
|
|
|
|
err = new errors.ResourceNotFoundError(err,
|
|
|
|
format('package with id %s was not found', name));
|
|
|
|
}
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb(null, pkg);
|
|
|
|
}
|
2015-07-26 08:45:20 +03:00
|
|
|
});
|
2015-08-26 06:53:48 +03:00
|
|
|
} else {
|
|
|
|
this.cloudapi.listPackages(function (err, pkgs) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 06:53:48 +03:00
|
|
|
var nameMatches = [];
|
2015-08-26 20:02:01 +03:00
|
|
|
var shortIdMatches = [];
|
2015-08-26 06:53:48 +03:00
|
|
|
for (var i = 0; i < pkgs.length; i++) {
|
2015-08-26 20:02:01 +03:00
|
|
|
var pkg = pkgs[i];
|
|
|
|
if (pkg.name === name) {
|
|
|
|
nameMatches.push(pkg);
|
|
|
|
}
|
|
|
|
if (pkg.id.slice(0, 8) === name) {
|
|
|
|
shortIdMatches.push(pkg);
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
|
|
|
if (nameMatches.length === 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(null, nameMatches[0]);
|
2015-08-26 20:02:01 +03:00
|
|
|
} else if (nameMatches.length > 1) {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.TritonError(format(
|
2015-08-26 06:53:48 +03:00
|
|
|
'package name "%s" is ambiguous: matches %d packages',
|
|
|
|
name, nameMatches.length)));
|
2015-08-26 20:02:01 +03:00
|
|
|
} else if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
2015-09-02 10:03:17 +03:00
|
|
|
'no package with name or short id "%s" was found', name)));
|
2015-08-26 20:02:01 +03:00
|
|
|
} else {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(
|
|
|
|
format('no package with name "%s" was found '
|
2015-09-02 10:03:17 +03:00
|
|
|
+ 'and "%s" is an ambiguous short id', name)));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an network by ID, exact name, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If the name is ambiguous, then this errors out.
|
|
|
|
*/
|
2015-09-04 21:04:45 +03:00
|
|
|
TritonApi.prototype.getNetwork = function getNetwork(name, cb) {
|
2015-09-02 10:03:17 +03:00
|
|
|
assert.string(name, 'name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.isUUID(name)) {
|
|
|
|
this.cloudapi.getNetwork(name, function (err, net) {
|
|
|
|
if (err) {
|
2015-10-07 09:09:52 +03:00
|
|
|
if (err.restCode === 'ResourceNotFound') {
|
|
|
|
// Wrap with *our* ResourceNotFound for exitStatus=3.
|
|
|
|
err = new errors.ResourceNotFoundError(err,
|
|
|
|
format('network with id %s was not found', name));
|
|
|
|
}
|
2015-09-02 10:03:17 +03:00
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb(null, net);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.cloudapi.listNetworks(function (err, nets) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
var nameMatches = [];
|
|
|
|
var shortIdMatches = [];
|
|
|
|
for (var i = 0; i < nets.length; i++) {
|
|
|
|
var net = nets[i];
|
|
|
|
if (net.name === name) {
|
|
|
|
nameMatches.push(net);
|
|
|
|
}
|
|
|
|
if (net.id.slice(0, 8) === name) {
|
|
|
|
shortIdMatches.push(net);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nameMatches.length === 1) {
|
|
|
|
cb(null, nameMatches[0]);
|
|
|
|
} else if (nameMatches.length > 1) {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.TritonError(format(
|
2015-09-02 10:03:17 +03:00
|
|
|
'network name "%s" is ambiguous: matches %d networks',
|
|
|
|
name, nameMatches.length)));
|
|
|
|
} else if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
2015-09-02 10:03:17 +03:00
|
|
|
'no network with name or short id "%s" was found', name)));
|
|
|
|
} else {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
|
|
|
'no network with name "%s" was found '
|
2015-09-02 10:03:17 +03:00
|
|
|
+ 'and "%s" is an ambiguous short id', name)));
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2015-07-26 08:45:20 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-08-26 03:27:46 +03:00
|
|
|
/**
|
2016-02-09 22:23:55 +02:00
|
|
|
* Get an instance.
|
2015-08-26 03:27:46 +03:00
|
|
|
*
|
2016-03-02 10:05:06 +02:00
|
|
|
* Alternative call signature: `getInstance(id, cb)`.
|
2016-02-09 22:23:55 +02:00
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {UUID} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {Array} fields: Optional. An array of instance field names that are
|
|
|
|
* wanted by the caller. This *can* allow the implementation to avoid
|
|
|
|
* extra API calls. E.g. `['id', 'name']`.
|
2016-03-02 10:05:06 +02:00
|
|
|
* @param {Function} cb `function (err, inst, res)`
|
|
|
|
* Note that deleted instances will result in `err` being a
|
|
|
|
* `InstanceDeletedError` and `inst` being defined. On success, `res` is
|
|
|
|
* the response object from a `GetMachine`, if one was made (possibly not
|
|
|
|
* if the instance was retrieved from `ListMachines` calls).
|
2015-08-26 03:27:46 +03:00
|
|
|
*/
|
2016-02-09 22:23:55 +02:00
|
|
|
TritonApi.prototype.getInstance = function getInstance(opts, cb) {
|
2015-08-27 03:21:27 +03:00
|
|
|
var self = this;
|
2016-02-09 22:23:55 +02:00
|
|
|
if (typeof (opts) === 'string') {
|
|
|
|
opts = {id: opts};
|
|
|
|
}
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalArrayOfString(opts.fields, 'opts.fields');
|
2015-08-27 03:21:27 +03:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2016-03-02 10:05:06 +02:00
|
|
|
/*
|
|
|
|
* Some wrapping/massaging of some CloudAPI GetMachine errors.
|
|
|
|
*/
|
|
|
|
var errFromGetMachineErr = function (err) {
|
|
|
|
if (!err) {
|
|
|
|
// jsl:pass
|
|
|
|
} else if (err.restCode === 'ResourceNotFound') {
|
|
|
|
// The CloudApi 404 error message sucks: "VM not found".
|
|
|
|
err = new errors.ResourceNotFoundError(err,
|
|
|
|
format('instance with id %s was not found', opts.id));
|
|
|
|
} else if (err.statusCode === 410) {
|
|
|
|
// GetMachine returns '410 Gone' for deleted machines.
|
|
|
|
err = new errors.InstanceDeletedError(err,
|
|
|
|
format('instance %s was deleted', opts.id));
|
|
|
|
}
|
|
|
|
return err;
|
|
|
|
};
|
|
|
|
|
2015-11-10 01:09:37 +02:00
|
|
|
var res;
|
2015-08-27 03:21:27 +03:00
|
|
|
var shortId;
|
|
|
|
var inst;
|
2016-01-26 02:12:14 +02:00
|
|
|
var instFromList;
|
2015-08-27 03:21:27 +03:00
|
|
|
|
|
|
|
vasync.pipeline({funcs: [
|
|
|
|
function tryUuid(_, next) {
|
|
|
|
var uuid;
|
2016-02-09 22:23:55 +02:00
|
|
|
if (common.isUUID(opts.id)) {
|
|
|
|
uuid = opts.id;
|
2015-08-27 03:21:27 +03:00
|
|
|
} else {
|
2016-02-09 22:23:55 +02:00
|
|
|
shortId = common.normShortId(opts.id);
|
2015-08-27 03:21:27 +03:00
|
|
|
if (shortId && common.isUUID(shortId)) {
|
|
|
|
// E.g. a >32-char docker container ID normalized to a UUID.
|
|
|
|
uuid = shortId;
|
|
|
|
} else {
|
|
|
|
return next();
|
|
|
|
}
|
2015-08-26 03:27:46 +03:00
|
|
|
}
|
2015-11-10 01:09:37 +02:00
|
|
|
self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
|
|
|
|
res = res_;
|
2015-08-31 21:14:14 +03:00
|
|
|
inst = inst_;
|
2016-03-02 10:05:06 +02:00
|
|
|
err = errFromGetMachineErr(err);
|
2015-08-27 03:21:27 +03:00
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function tryName(_, next) {
|
2016-01-26 02:12:14 +02:00
|
|
|
if (inst || instFromList) {
|
2015-08-27 03:21:27 +03:00
|
|
|
return next();
|
|
|
|
}
|
2016-02-09 22:23:55 +02:00
|
|
|
self.cloudapi.listMachines({name: opts.id}, function (err, insts) {
|
2015-08-27 03:21:27 +03:00
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
for (var i = 0; i < insts.length; i++) {
|
2016-02-09 22:23:55 +02:00
|
|
|
if (insts[i].name === opts.id) {
|
2016-01-26 02:12:14 +02:00
|
|
|
instFromList = insts[i];
|
2015-08-27 03:21:27 +03:00
|
|
|
// Relying on rule that instance name is unique
|
|
|
|
// for a user and DC.
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function tryShortId(_, next) {
|
2016-01-26 02:12:14 +02:00
|
|
|
if (inst || instFromList || !shortId) {
|
2015-08-27 03:21:27 +03:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
var nextOnce = once(next);
|
|
|
|
|
|
|
|
var match;
|
|
|
|
var s = self.cloudapi.createListMachinesStream();
|
|
|
|
s.on('error', function (err) {
|
|
|
|
nextOnce(err);
|
|
|
|
});
|
|
|
|
s.on('readable', function () {
|
2015-09-01 02:56:26 +03:00
|
|
|
var candidate;
|
|
|
|
while ((candidate = s.read()) !== null) {
|
|
|
|
if (candidate.id.slice(0, shortId.length) === shortId) {
|
2015-08-27 03:21:27 +03:00
|
|
|
if (match) {
|
2015-10-07 09:09:52 +03:00
|
|
|
return nextOnce(new errors.TritonError(
|
2015-08-27 03:21:27 +03:00
|
|
|
'instance short id "%s" is ambiguous',
|
|
|
|
shortId));
|
|
|
|
} else {
|
2015-09-01 02:56:26 +03:00
|
|
|
match = candidate;
|
2015-08-27 03:21:27 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
s.on('end', function () {
|
|
|
|
if (match) {
|
2016-01-26 02:12:14 +02:00
|
|
|
instFromList = match;
|
2015-08-27 03:21:27 +03:00
|
|
|
}
|
|
|
|
nextOnce();
|
|
|
|
});
|
2016-01-26 02:12:14 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
|
|
* There can be fields that only exist on the machine object from
|
|
|
|
* GetMachine, and not from ListMachine. `dns_names` is one of these.
|
|
|
|
* Therefore, if we got the machine from filtering ListMachine, then
|
|
|
|
* we need to re-GetMachine.
|
|
|
|
*/
|
|
|
|
function reGetIfFromList(_, next) {
|
|
|
|
if (inst || !instFromList) {
|
|
|
|
next();
|
|
|
|
return;
|
2016-02-09 22:23:55 +02:00
|
|
|
} else if (opts.fields) {
|
|
|
|
// If already have all the requested fields, no need to re-get.
|
|
|
|
var missingAField = false;
|
|
|
|
for (var i = 0; i < opts.fields.length; i++) {
|
|
|
|
if (! instFromList.hasOwnProperty(opts.fields[i])) {
|
|
|
|
missingAField = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!missingAField) {
|
|
|
|
inst = instFromList;
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2016-01-26 02:12:14 +02:00
|
|
|
}
|
2016-02-09 22:23:55 +02:00
|
|
|
|
2016-01-26 02:12:14 +02:00
|
|
|
var uuid = instFromList.id;
|
|
|
|
self.cloudapi.getMachine(uuid, function (err, inst_, res_) {
|
|
|
|
res = res_;
|
|
|
|
inst = inst_;
|
2016-03-02 10:05:06 +02:00
|
|
|
err = errFromGetMachineErr(err);
|
2016-01-26 02:12:14 +02:00
|
|
|
next(err);
|
|
|
|
});
|
2015-08-27 03:21:27 +03:00
|
|
|
}
|
|
|
|
]}, function (err) {
|
2016-03-02 10:05:06 +02:00
|
|
|
if (err || inst) {
|
|
|
|
cb(err, inst, res);
|
2015-08-27 03:21:27 +03:00
|
|
|
} else {
|
2015-10-07 09:09:52 +03:00
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
2016-02-09 22:23:55 +02:00
|
|
|
'no instance with name or short id "%s" was found', opts.id)));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-03-11 21:24:44 +02:00
|
|
|
// ---- instance enable/disable firewall
|
2016-03-11 16:07:11 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable the firewall on an instance.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2016-03-11 21:24:44 +02:00
|
|
|
* - {String} id: Required. The instance ID, name, or short ID.
|
|
|
|
* @param {Function} callback `function (err, fauxInst, res)`
|
|
|
|
* On failure `err` is an error instance, else it is null.
|
|
|
|
* On success: `fauxInst` is an object with just the instance id,
|
|
|
|
* `{id: <instance UUID>}` and `res` is the CloudAPI
|
|
|
|
* `EnableMachineFirewall` response.
|
|
|
|
* The API call does not return the instance/machine object, hence we
|
|
|
|
* are limited to just the id for `fauxInst`.
|
2016-03-11 16:07:11 +02:00
|
|
|
*/
|
|
|
|
TritonApi.prototype.enableInstanceFirewall =
|
|
|
|
function enableInstanceFirewall(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
2016-03-11 21:24:44 +02:00
|
|
|
var fauxInst;
|
2016-03-11 16:07:11 +02:00
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function enableFirewall(arg, next) {
|
2016-03-11 21:24:44 +02:00
|
|
|
fauxInst = {id: arg.instId};
|
|
|
|
|
2016-03-11 16:07:11 +02:00
|
|
|
self.cloudapi.enableMachineFirewall(arg.instId,
|
2016-03-11 21:24:44 +02:00
|
|
|
function (err, _, _res) {
|
2016-03-11 16:07:11 +02:00
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
2016-03-11 21:24:44 +02:00
|
|
|
cb(err, fauxInst, res);
|
2016-03-11 16:07:11 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disable the firewall on an instance.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2016-03-11 21:24:44 +02:00
|
|
|
* - {String} id: Required. The instance ID, name, or short ID.
|
|
|
|
* @param {Function} callback `function (err, fauxInst, res)`
|
|
|
|
* On failure `err` is an error instance, else it is null.
|
|
|
|
* On success: `fauxInst` is an object with just the instance id,
|
|
|
|
* `{id: <instance UUID>}` and `res` is the CloudAPI
|
|
|
|
* `EnableMachineFirewall` response.
|
|
|
|
* The API call does not return the instance/machine object, hence we
|
|
|
|
* are limited to just the id for `fauxInst`.
|
2016-03-11 16:07:11 +02:00
|
|
|
*/
|
|
|
|
TritonApi.prototype.disableInstanceFirewall =
|
|
|
|
function disableInstanceFirewall(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
2016-03-11 21:24:44 +02:00
|
|
|
var fauxInst;
|
2016-03-11 16:07:11 +02:00
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function disableFirewall(arg, next) {
|
2016-03-11 21:24:44 +02:00
|
|
|
fauxInst = {id: arg.instId};
|
|
|
|
|
2016-03-11 16:07:11 +02:00
|
|
|
self.cloudapi.disableMachineFirewall(arg.instId,
|
2016-03-11 21:24:44 +02:00
|
|
|
function (err, _, _res) {
|
2016-03-11 16:07:11 +02:00
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
2016-03-11 21:24:44 +02:00
|
|
|
cb(err, fauxInst, res);
|
2016-03-11 16:07:11 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-02-04 15:39:50 +02:00
|
|
|
// ---- instance snapshots
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a snapshot of an instance.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {String} name: The name for new snapshot. Optional.
|
|
|
|
* @param {Function} callback `function (err, snapshots, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.createInstanceSnapshot =
|
|
|
|
function createInstanceSnapshot(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalString(opts.name, 'opts.name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var snapshot;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function createSnapshot(arg, next) {
|
|
|
|
self.cloudapi.createMachineSnapshot({
|
|
|
|
id: arg.instId,
|
|
|
|
name: opts.name
|
|
|
|
}, function (err, snap, _res) {
|
|
|
|
res = _res;
|
|
|
|
res.instId = arg.instId; // gross hack, in case caller needs it
|
|
|
|
snapshot = snap;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, snapshot, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List an instance's snapshots.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, snapshots, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.listInstanceSnapshots =
|
|
|
|
function listInstanceSnapshots(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var snapshots;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function listSnapshots(arg, next) {
|
|
|
|
self.cloudapi.listMachineSnapshots({
|
|
|
|
id: arg.instId,
|
|
|
|
name: opts.name
|
|
|
|
}, function (err, snaps, _res) {
|
|
|
|
res = _res;
|
|
|
|
res.instId = arg.instId; // gross hack, in case caller needs it
|
|
|
|
snapshots = snaps;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, snapshots, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an instance's snapshot.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {String} name: The name of the snapshot. Required.
|
|
|
|
* @param {Function} callback `function (err, snapshot, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.getInstanceSnapshot =
|
|
|
|
function getInstanceSnapshot(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.name, 'opts.name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var snapshot;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function getSnapshot(arg, next) {
|
|
|
|
self.cloudapi.getMachineSnapshot({
|
|
|
|
id: arg.instId,
|
|
|
|
name: opts.name
|
|
|
|
}, function (err, _snap, _res) {
|
|
|
|
res = _res;
|
|
|
|
res.instId = arg.instId; // gross hack, in case caller needs it
|
|
|
|
snapshot = _snap;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, snapshot, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete an instance's snapshot.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {String} name: The name of the snapshot. Required.
|
|
|
|
* @param {Function} callback `function (err, res)`
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.deleteInstanceSnapshot =
|
|
|
|
function deleteInstanceSnapshot(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.name, 'opts.name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function deleteSnapshot(arg, next) {
|
|
|
|
self.cloudapi.deleteMachineSnapshot({
|
|
|
|
id: arg.instId,
|
|
|
|
name: opts.name
|
|
|
|
}, function (err, _res) {
|
|
|
|
res = _res;
|
|
|
|
res.instId = arg.instId; // gross hack, in case caller needs it
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-02-09 22:23:55 +02:00
|
|
|
// ---- instance tags
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List an instance's tags.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#ListMachineTags>
|
|
|
|
*
|
|
|
|
* Alternative call signature: `listInstanceTags(id, callback)`.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {UUID} id: The instance ID, name, or short ID. Required.
|
|
|
|
* @param {Function} cb: `function (err, tags, res)`
|
|
|
|
* On success, `res` is *possibly* the response object from either a
|
|
|
|
* `ListMachineTags` or a `GetMachine` call.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.listInstanceTags = function listInstanceTags(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
if (typeof (opts) === 'string') {
|
|
|
|
opts = {id: opts};
|
|
|
|
}
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.isUUID(opts.id)) {
|
|
|
|
self.cloudapi.listMachineTags(opts, cb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.getInstance({
|
|
|
|
id: opts.id,
|
|
|
|
fields: ['id', 'tags']
|
|
|
|
}, function (err, inst, res) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// No need to call `ListMachineTags` now.
|
|
|
|
cb(null, inst.tags, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an instance tag value.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#GetMachineTag>
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {UUID} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {String} tag: The tag name. Required.
|
|
|
|
* @param {Function} cb: `function (err, value, res)`
|
|
|
|
* On success, `value` is the tag value *as a string*. See note above.
|
|
|
|
* On success, `res` is *possibly* the response object from either a
|
|
|
|
* `GetMachineTag` or a `GetMachine` call.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.getInstanceTag = function getInstanceTag(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.tag, 'opts.tag');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.isUUID(opts.id)) {
|
|
|
|
self.cloudapi.getMachineTag(opts, cb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.getInstance({
|
|
|
|
id: opts.id,
|
|
|
|
fields: ['id', 'tags']
|
|
|
|
}, function (err, inst, res) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// No need to call `GetMachineTag` now.
|
|
|
|
if (inst.tags.hasOwnProperty(opts.tag)) {
|
|
|
|
var value = inst.tags[opts.tag];
|
|
|
|
cb(null, value, res);
|
|
|
|
} else {
|
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
|
|
|
'tag with name "%s" was not found', opts.tag)));
|
2015-08-26 03:27:46 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2016-02-09 22:23:55 +02:00
|
|
|
/**
|
|
|
|
* Shared implementation for any methods to change instance tags.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {Object} change: Required. Describes the tag change to make. It
|
|
|
|
* has an "action" field and, depending on the particular action, a
|
|
|
|
* "tags" field.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
|
|
|
|
* Warning: A concurrent tag update to the same tags can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err, tags, res)`
|
|
|
|
* On success, `tags` is the updated set of instance tags and `res` is
|
|
|
|
* the response object from the underlying CloudAPI call. Note that `tags`
|
|
|
|
* is not set (undefined) for the "delete" and "deleteAll" actions.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype._changeInstanceTags =
|
|
|
|
function _changeInstanceTags(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.object(opts.change, 'opts.change');
|
|
|
|
var KNOWN_CHANGE_ACTIONS = ['set', 'replace', 'delete', 'deleteAll'];
|
|
|
|
assert.ok(KNOWN_CHANGE_ACTIONS.indexOf(opts.change.action) != -1,
|
|
|
|
'invalid change action: ' + opts.change.action);
|
|
|
|
switch (opts.change.action) {
|
|
|
|
case 'set':
|
|
|
|
case 'replace':
|
|
|
|
assert.object(opts.change.tags,
|
|
|
|
'opts.change.tags for action=' + opts.change.action);
|
|
|
|
break;
|
|
|
|
case 'delete':
|
|
|
|
assert.string(opts.change.tagName,
|
|
|
|
'opts.change.tagName for action=delete');
|
|
|
|
break;
|
|
|
|
case 'deleteAll':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('unexpected action: ' + opts.change.action);
|
|
|
|
}
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var theRes;
|
|
|
|
var updatedTags;
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function changeTheTags(arg, next) {
|
|
|
|
switch (opts.change.action) {
|
|
|
|
case 'set':
|
|
|
|
self.cloudapi.addMachineTags({
|
|
|
|
id: arg.instId,
|
|
|
|
tags: opts.change.tags
|
|
|
|
}, function (err, tags, res) {
|
|
|
|
updatedTags = tags;
|
|
|
|
theRes = res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'replace':
|
|
|
|
self.cloudapi.replaceMachineTags({
|
|
|
|
id: arg.instId,
|
|
|
|
tags: opts.change.tags
|
|
|
|
}, function (err, tags, res) {
|
|
|
|
updatedTags = tags;
|
|
|
|
theRes = res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'delete':
|
|
|
|
self.cloudapi.deleteMachineTag({
|
|
|
|
id: arg.instId,
|
|
|
|
tag: opts.change.tagName
|
|
|
|
}, function (err, res) {
|
|
|
|
theRes = res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'deleteAll':
|
|
|
|
self.cloudapi.deleteMachineTags({
|
|
|
|
id: arg.instId
|
|
|
|
}, function (err, res) {
|
|
|
|
theRes = res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('unexpected action: ' + opts.change.action);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
function waitForChanges(arg, next) {
|
|
|
|
if (!opts.wait) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self.waitForInstanceTagChanges({
|
|
|
|
id: arg.instId,
|
|
|
|
timeout: opts.waitTimeout,
|
|
|
|
change: opts.change
|
|
|
|
}, next);
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb(null, updatedTags, theRes);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wait (via polling) for the given tag changes to have taken on the instance.
|
|
|
|
*
|
|
|
|
* Dev Note: This polls `ListMachineTags` until it looks like the given changes
|
|
|
|
* have been applied. This is unreliable with concurrent tag updates. A
|
|
|
|
* workaround for that is `opts.timeout`. A better long term solution would
|
|
|
|
* be for cloudapi to expose some handle on the underlying Triton workflow
|
|
|
|
* jobs performing these, and poll/wait on those.
|
|
|
|
*
|
|
|
|
* @param {Object} opts: Required.
|
|
|
|
* - {UUID} id: Required. The instance id.
|
|
|
|
* Limitation: Currently requiring this to be the full instance UUID.
|
|
|
|
* - {Number} timeout: Optional. A number of milliseconds after which to
|
|
|
|
* timeout (callback with `TimeoutError`) the wait. By default this is
|
|
|
|
* Infinity.
|
|
|
|
* - {Object} changes: Required. It always has an 'action' field (one of
|
|
|
|
* 'set', 'replace', 'delete', 'deleteAll') and, depending on the
|
|
|
|
* action, a 'tags' (set, replace), 'tagName' (delete) or 'tagNames'
|
|
|
|
* (delete).
|
|
|
|
* @param {Function} cb: `function (err, updatedTags)`
|
|
|
|
* On failure, `err` can be an error from `ListMachineTags` or
|
|
|
|
* `TimeoutError`.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.waitForInstanceTagChanges =
|
|
|
|
function waitForInstanceTagChanges(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.uuid(opts.id, 'opts.id');
|
|
|
|
assert.optionalNumber(opts.timeout, 'opts.timeout');
|
|
|
|
var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity;
|
|
|
|
assert.ok(timeout > 0, 'opts.timeout must be greater than zero');
|
|
|
|
assert.object(opts.change, 'opts.change');
|
|
|
|
var KNOWN_CHANGE_ACTIONS = ['set', 'replace', 'delete', 'deleteAll'];
|
|
|
|
assert.ok(KNOWN_CHANGE_ACTIONS.indexOf(opts.change.action) != -1,
|
|
|
|
'invalid change action: ' + opts.change.action);
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var tagNames;
|
|
|
|
switch (opts.change.action) {
|
|
|
|
case 'set':
|
|
|
|
case 'replace':
|
|
|
|
assert.object(opts.change.tags, 'opts.change.tags');
|
|
|
|
break;
|
|
|
|
case 'delete':
|
|
|
|
if (opts.change.tagNames) {
|
|
|
|
assert.arrayOfString(opts.change.tagNames, 'opts.change.tagNames');
|
|
|
|
tagNames = opts.change.tagNames;
|
|
|
|
} else {
|
|
|
|
assert.string(opts.change.tagName, 'opts.change.tagName');
|
|
|
|
tagNames = [opts.change.tagName];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'deleteAll':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('unexpected action: ' + opts.change.action);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Hardcoded 2s poll interval for now. Not yet configurable, being mindful
|
|
|
|
* of avoiding lots of clients naively swamping a CloudAPI and hitting
|
|
|
|
* throttling.
|
|
|
|
* TODO: General client support for dealing with polling and throttling.
|
|
|
|
*/
|
|
|
|
var POLL_INTERVAL = 2 * 1000;
|
|
|
|
|
|
|
|
var startTime = Date.now();
|
|
|
|
|
|
|
|
var poll = function () {
|
|
|
|
self.log.trace({id: opts.id}, 'waitForInstanceTagChanges: poll inst');
|
|
|
|
self.cloudapi.listMachineTags({id: opts.id}, function (err, tags) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine in changes are not yet applied (incomplete).
|
|
|
|
var incomplete = false;
|
|
|
|
var i, k, keys;
|
|
|
|
switch (opts.change.action) {
|
|
|
|
case 'set':
|
|
|
|
keys = Object.keys(opts.change.tags);
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
|
|
k = keys[i];
|
|
|
|
if (tags[k] !== opts.change.tags[k]) {
|
|
|
|
self.log.trace({tag: k},
|
|
|
|
'waitForInstanceTagChanges incomplete set: '
|
|
|
|
+ 'unexpected value for tag');
|
|
|
|
incomplete = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'replace':
|
|
|
|
keys = Object.keys(opts.change.tags);
|
|
|
|
var tagsCopy = common.objCopy(tags);
|
|
|
|
for (i = 0; i < keys.length; i++) {
|
|
|
|
k = keys[i];
|
|
|
|
if (tagsCopy[k] !== opts.change.tags[k]) {
|
|
|
|
self.log.trace({tag: k},
|
|
|
|
'waitForInstanceTagChanges incomplete replace: '
|
|
|
|
+ 'unexpected value for tag');
|
|
|
|
incomplete = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
delete tagsCopy[k];
|
|
|
|
}
|
|
|
|
var extraneousTags = Object.keys(tagsCopy);
|
|
|
|
if (extraneousTags.length > 0) {
|
|
|
|
self.log.trace({extraneousTags: extraneousTags},
|
|
|
|
'waitForInstanceTagChanges incomplete replace: '
|
|
|
|
+ 'extraneous tags');
|
|
|
|
incomplete = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'delete':
|
|
|
|
for (i = 0; i < tagNames.length; i++) {
|
|
|
|
k = tagNames[i];
|
|
|
|
if (tags.hasOwnProperty(k)) {
|
|
|
|
self.log.trace({tag: k},
|
|
|
|
'waitForInstanceTagChanges incomplete delete: '
|
|
|
|
+ 'extraneous tag');
|
|
|
|
incomplete = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'deleteAll':
|
|
|
|
if (Object.keys(tags).length > 0) {
|
|
|
|
self.log.trace({tag: k},
|
|
|
|
'waitForInstanceTagChanges incomplete deleteAll: '
|
|
|
|
+ 'still have tags');
|
|
|
|
incomplete = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error('unexpected action: ' + opts.change.action);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!incomplete) {
|
|
|
|
self.log.trace('waitForInstanceTagChanges: complete');
|
|
|
|
cb(null, tags);
|
|
|
|
} else {
|
|
|
|
var elapsedTime = Date.now() - startTime;
|
|
|
|
if (elapsedTime > timeout) {
|
|
|
|
cb(new errors.TimeoutError(format('timeout waiting for '
|
|
|
|
+ 'tag changes on instance %s (elapsed %ds)',
|
2016-03-09 19:19:44 +02:00
|
|
|
opts.id, Math.round(elapsedTime / 1000))));
|
2016-02-09 22:23:55 +02:00
|
|
|
} else {
|
|
|
|
setTimeout(poll, POLL_INTERVAL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
setImmediate(poll);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set instance tags.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#AddMachineTags>
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {Object} tags: The tag name/value pairs. Required.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
|
|
|
|
* Warning: A concurrent tag update to the same tags can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err, updatedTags, res)`
|
|
|
|
* On success, `updatedTags` is the updated set of instance tags and `res`
|
|
|
|
* is the response object from the `AddMachineTags` CloudAPI call.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.setInstanceTags = function setInstanceTags(opts, cb) {
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.object(opts.tags, 'opts.tags');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
this._changeInstanceTags({
|
|
|
|
id: opts.id,
|
|
|
|
change: {
|
|
|
|
action: 'set',
|
|
|
|
tags: opts.tags
|
|
|
|
},
|
|
|
|
wait: opts.wait,
|
|
|
|
waitTimeout: opts.waitTimeout
|
|
|
|
}, cb);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replace all instance tags.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#ReplaceMachineTags>
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {Object} tags: The tag name/value pairs. Required.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
|
|
|
|
* Warning: A concurrent tag update to the same tags can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err, tags, res)`
|
|
|
|
* On success, `tags` is the updated set of instance tags and `res` is
|
|
|
|
* the response object from the `ReplaceMachineTags` CloudAPI call.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.replaceAllInstanceTags =
|
|
|
|
function replaceAllInstanceTags(opts, cb) {
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.object(opts.tags, 'opts.tags');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
this._changeInstanceTags({
|
|
|
|
id: opts.id,
|
|
|
|
change: {
|
|
|
|
action: 'replace',
|
|
|
|
tags: opts.tags
|
|
|
|
},
|
|
|
|
wait: opts.wait,
|
|
|
|
waitTimeout: opts.waitTimeout
|
|
|
|
}, cb);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete the named instance tag.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTag>
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {String} tag: The tag name. Required.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
|
|
|
|
* Warning: A concurrent tag update to the same tags can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.deleteInstanceTag = function deleteInstanceTag(opts, cb) {
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.tag, 'opts.tag');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
this._changeInstanceTags({
|
|
|
|
id: opts.id,
|
|
|
|
change: {
|
|
|
|
action: 'delete',
|
|
|
|
tagName: opts.tag
|
|
|
|
},
|
|
|
|
wait: opts.wait,
|
|
|
|
waitTimeout: opts.waitTimeout
|
|
|
|
}, function (err, updatedTags, res) {
|
|
|
|
cb(err, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete all tags for the given instance.
|
|
|
|
* <http://apidocs.joyent.com/cloudapi/#DeleteMachineTags>
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the tag update is complete.
|
|
|
|
* Warning: A concurrent tag update to the same tags can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.deleteAllInstanceTags =
|
|
|
|
function deleteAllInstanceTags(opts, cb) {
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
this._changeInstanceTags({
|
|
|
|
id: opts.id,
|
|
|
|
change: {
|
|
|
|
action: 'deleteAll'
|
|
|
|
},
|
|
|
|
wait: opts.wait,
|
|
|
|
waitTimeout: opts.waitTimeout
|
|
|
|
}, function (err, updatedTags, res) {
|
|
|
|
cb(err, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-02-04 15:39:50 +02:00
|
|
|
// ---- Firewall Rules
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a firewall rule by ID, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If there is more than one firewall rule with that short ID, then this errors
|
|
|
|
* out.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.getFirewallRule = function getFirewallRule(id, cb) {
|
|
|
|
assert.string(id, 'id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.isUUID(id)) {
|
|
|
|
this.cloudapi.getFirewallRule(id, function (err, fwrule) {
|
|
|
|
if (err) {
|
|
|
|
if (err.restCode === 'ResourceNotFound') {
|
|
|
|
err = new errors.ResourceNotFoundError(err,
|
|
|
|
format('firewall rule with id %s was not found', id));
|
|
|
|
}
|
|
|
|
cb(err);
|
|
|
|
} else {
|
|
|
|
cb(null, fwrule);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.cloudapi.listFirewallRules({}, function (err, fwrules) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
var shortIdMatches = fwrules.filter(function (fwrule) {
|
|
|
|
return fwrule.id.slice(0, 8) === id;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
|
|
|
cb(new errors.ResourceNotFoundError(format(
|
|
|
|
'no firewall rule with short id "%s" was found', id)));
|
|
|
|
} else {
|
|
|
|
cb(new errors.ResourceNotFoundError(
|
|
|
|
format('"%s" is an ambiguous short id, with multiple ' +
|
|
|
|
'matching firewall rules', id)));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List all firewall rules affecting an instance.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The instance ID, name, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, instances, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.listInstanceFirewallRules =
|
|
|
|
function listInstanceFirewallRules(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var fwrules;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function listRules(arg, next) {
|
|
|
|
self.cloudapi.listMachineFirewallRules({
|
|
|
|
id: arg.instId
|
|
|
|
}, function (err, rules, _res) {
|
|
|
|
res = _res;
|
|
|
|
fwrules = rules;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, fwrules, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List all instances affected by a firewall rule.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The fwrule ID, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, instances, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.listFirewallRuleInstances =
|
|
|
|
function listFirewallRuleInstances(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var instances;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepFwRuleId,
|
|
|
|
|
|
|
|
function listInsts(arg, next) {
|
|
|
|
self.cloudapi.listFirewallRuleMachines({
|
|
|
|
id: arg.fwruleId
|
|
|
|
}, function (err, machines, _res) {
|
|
|
|
res = _res;
|
|
|
|
instances = machines;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, instances, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a firewall rule.
|
|
|
|
*
|
|
|
|
* Dev Note: Currently cloudapi UpdateFirewallRule *requires* the 'rule' field,
|
|
|
|
* which is overkill. `TritonApi.updateFirewallRule` adds sugar by making
|
|
|
|
* 'rule' optional.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The fwrule ID, or short ID. Required.
|
|
|
|
* - {String} rule: The fwrule text. Optional.
|
|
|
|
* - {Boolean} enabled: Default to false. Optional.
|
|
|
|
* - {String} description: Description of the rule. Optional.
|
|
|
|
* At least one of the fields must be provided.
|
|
|
|
* @param {Function} callback `function (err, fwrule, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.updateFirewallRule = function updateFirewallRule(opts, cb) {
|
|
|
|
// TODO: strict opts field validation
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalString(opts.rule, 'opts.rule');
|
|
|
|
assert.optionalBool(opts.enabled, 'opts.enabled');
|
|
|
|
assert.optionalString(opts.description, 'opts.description');
|
|
|
|
assert.ok(opts.rule !== undefined || opts.enabled !== undefined ||
|
|
|
|
opts.description !== undefined, 'at least one of opts.rule, '
|
|
|
|
+ 'opts.enabled, or opts.description is required');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var updatedFwrule;
|
|
|
|
var updateOpts = common.objCopy(opts);
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepFwRuleId,
|
|
|
|
|
|
|
|
/*
|
|
|
|
* CloudAPI currently requires the 'rule' field. We provide sugar here
|
|
|
|
* and fill it in for you.
|
|
|
|
*/
|
|
|
|
function sugarFillRuleField(arg, next) {
|
|
|
|
if (updateOpts.rule) {
|
|
|
|
next();
|
|
|
|
} else if (arg.fwrule) {
|
|
|
|
updateOpts.rule = arg.fwrule.rule;
|
|
|
|
next();
|
|
|
|
} else {
|
|
|
|
self.getFirewallRule(arg.fwruleId, function (err, fwrule) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
} else {
|
|
|
|
updateOpts.rule = fwrule.rule;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
function updateRule(arg, next) {
|
|
|
|
updateOpts.id = arg.fwruleId;
|
|
|
|
self.cloudapi.updateFirewallRule(updateOpts,
|
|
|
|
function (err, fwrule, res_) {
|
|
|
|
res = res_;
|
|
|
|
updatedFwrule = fwrule;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, updatedFwrule, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable a firewall rule.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The fwrule ID, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, fwrule, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.enableFirewallRule = function enableFirewallRule(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var fwrule;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepFwRuleId,
|
|
|
|
|
|
|
|
function enableRule(arg, next) {
|
|
|
|
self.cloudapi.enableFirewallRule({
|
|
|
|
id: arg.fwruleId
|
|
|
|
}, function (err, rule, _res) {
|
|
|
|
res = _res;
|
|
|
|
fwrule = rule;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, fwrule, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disable a firewall rule.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The fwrule ID, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, fwrule, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.disableFirewallRule =
|
|
|
|
function disableFirewallRule(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
var fwrule;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepFwRuleId,
|
|
|
|
|
|
|
|
function disableRule(arg, next) {
|
|
|
|
self.cloudapi.disableFirewallRule({
|
|
|
|
id: arg.fwruleId
|
|
|
|
}, function (err, rule, _res) {
|
|
|
|
res = _res;
|
|
|
|
fwrule = rule;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, fwrule, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a firewall rule.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: The fwrule ID, or short ID. Required.
|
|
|
|
* @param {Function} callback `function (err, res)`
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.deleteFirewallRule = function deleteFirewallRule(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepFwRuleId,
|
|
|
|
|
|
|
|
function deleteRule(arg, next) {
|
|
|
|
self.cloudapi.deleteFirewallRule({
|
|
|
|
id: arg.fwruleId
|
|
|
|
}, function (err, _res) {
|
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2016-02-09 22:23:55 +02:00
|
|
|
// ---- RBAC
|
|
|
|
|
2015-11-10 01:09:37 +02:00
|
|
|
/**
|
2015-11-13 02:04:12 +02:00
|
|
|
* Get role tags for a resource.
|
2015-11-10 01:09:37 +02:00
|
|
|
*
|
2015-11-13 02:04:12 +02:00
|
|
|
* @param {Object} opts
|
|
|
|
* - resourceType {String} One of:
|
|
|
|
* resource (a raw RBAC resource URL)
|
|
|
|
* instance
|
|
|
|
* image
|
|
|
|
* package
|
|
|
|
* network
|
|
|
|
* - id {String} The resource identifier. E.g. for an instance this can be
|
|
|
|
* the ID (a UUID), login or short id. Whatever `triton` typically allows
|
|
|
|
* for identification.
|
|
|
|
* @param {Function} callback `function (err, roleTags, resource)`
|
2015-11-10 01:09:37 +02:00
|
|
|
*/
|
2015-11-13 02:04:12 +02:00
|
|
|
TritonApi.prototype.getRoleTags = function getRoleTags(opts, cb) {
|
2015-11-10 01:09:37 +02:00
|
|
|
var self = this;
|
2015-11-13 02:04:12 +02:00
|
|
|
assert.object(opts, 'opts');
|
|
|
|
_assertRoleTagResourceType(opts.resourceType, 'opts.resourceType');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
2015-11-10 01:09:37 +02:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2015-11-13 02:04:12 +02:00
|
|
|
function roleTagsFromRes(res) {
|
|
|
|
return (
|
|
|
|
(res.headers['role-tag'] || '')
|
|
|
|
/* JSSTYLED */
|
|
|
|
.split(/\s*,\s*/)
|
|
|
|
.filter(function (r) { return r.trim(); })
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-11-10 01:09:37 +02:00
|
|
|
var roleTags;
|
2015-11-13 02:04:12 +02:00
|
|
|
var resource;
|
2015-11-10 01:09:37 +02:00
|
|
|
|
|
|
|
vasync.pipeline({arg: {}, funcs: [
|
2015-11-13 02:04:12 +02:00
|
|
|
function resolveResourceId(ctx, next) {
|
|
|
|
if (opts.resourceType === 'resource') {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var getFuncName = {
|
|
|
|
instance: 'getInstance',
|
|
|
|
image: 'getImage',
|
|
|
|
'package': 'getPackage',
|
|
|
|
network: 'getNetwork'
|
|
|
|
}[opts.resourceType];
|
|
|
|
self[getFuncName](opts.id, function (err, resource_, res) {
|
2015-11-10 01:09:37 +02:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
2015-11-13 02:04:12 +02:00
|
|
|
resource = resource_;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Sometimes `getInstance` et al return a CloudAPI `GetMachine`
|
|
|
|
* res on which there is a 'role-tag' header that we want.
|
|
|
|
*/
|
|
|
|
if (res) {
|
|
|
|
roleTags = roleTagsFromRes(res);
|
|
|
|
}
|
2015-11-10 01:09:37 +02:00
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
2015-11-13 02:04:12 +02:00
|
|
|
function getResourceIfNecessary(ctx, next) {
|
|
|
|
if (roleTags) {
|
2015-11-10 01:09:37 +02:00
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-11-13 02:04:12 +02:00
|
|
|
var resourceUrl = (opts.resourceType === 'resource'
|
|
|
|
? opts.id
|
|
|
|
: _roleTagResourceUrl(self.profile.account,
|
|
|
|
opts.resourceType, resource.id));
|
|
|
|
self.cloudapi.getRoleTags({resource: resourceUrl},
|
|
|
|
function (err, roleTags_, resource_) {
|
2015-11-10 01:09:37 +02:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
2015-11-13 02:04:12 +02:00
|
|
|
roleTags = roleTags_;
|
|
|
|
resource = resource_;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, roleTags, resource);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set role tags for a resource.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - resourceType {String} One of:
|
|
|
|
* resource (a raw RBAC resource URL)
|
|
|
|
* instance
|
|
|
|
* image
|
|
|
|
* package
|
|
|
|
* network
|
|
|
|
* - id {String} The resource identifier. E.g. for an instance this can be
|
|
|
|
* the ID (a UUID), login or short id. Whatever `triton` typically allows
|
|
|
|
* for identification.
|
|
|
|
* - roleTags {Array}
|
|
|
|
* @param {Function} callback `function (err)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.setRoleTags = function setRoleTags(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
_assertRoleTagResourceType(opts.resourceType, 'opts.resourceType');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.arrayOfString(opts.roleTags, 'opts.roleTags');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {}, funcs: [
|
|
|
|
function resolveResourceId(ctx, next) {
|
|
|
|
if (opts.resourceType === 'resource') {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var getFuncName = {
|
|
|
|
instance: 'getInstance',
|
|
|
|
image: 'getImage',
|
|
|
|
'package': 'getPackage',
|
|
|
|
network: 'getNetwork'
|
|
|
|
}[opts.resourceType];
|
|
|
|
self[getFuncName](opts.id, function (err, resource, res) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ctx.resource = resource;
|
2015-11-10 01:09:37 +02:00
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
2015-11-13 02:04:12 +02:00
|
|
|
|
|
|
|
function setTheRoleTags(ctx, next) {
|
|
|
|
var resourceUrl = (opts.resourceType === 'resource'
|
|
|
|
? opts.id
|
|
|
|
: _roleTagResourceUrl(self.profile.account,
|
|
|
|
opts.resourceType, ctx.resource.id));
|
|
|
|
self.cloudapi.setRoleTags({
|
|
|
|
resource: resourceUrl,
|
|
|
|
roleTags: opts.roleTags
|
|
|
|
}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
2015-11-10 01:09:37 +02:00
|
|
|
}
|
|
|
|
]}, function (err) {
|
2015-11-13 02:04:12 +02:00
|
|
|
cb(err);
|
2015-11-10 01:09:37 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-11-04 01:40:59 +02:00
|
|
|
|
|
|
|
/**
|
2015-11-21 22:41:16 +02:00
|
|
|
* Get an RBAC user by ID or login.
|
2015-11-04 01:40:59 +02:00
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2015-11-21 22:41:16 +02:00
|
|
|
* - id {UUID|String} The user ID (a UUID) or login.
|
2015-11-04 01:40:59 +02:00
|
|
|
* - roles {Boolean} Optional. Whether to includes roles of which this
|
|
|
|
* user is a member. Default false.
|
2015-11-06 01:13:14 +02:00
|
|
|
* - keys {Boolean} Optional. Set to `true` to also (with a separate
|
|
|
|
* request) retrieve the `keys` for this user. Default is false.
|
2015-11-04 01:40:59 +02:00
|
|
|
* @param {Function} callback of the form `function (err, user)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.getUser = function getUser(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalBool(opts.roles, 'opts.roles');
|
2015-11-06 01:13:14 +02:00
|
|
|
assert.optionalBool(opts.keys, 'opts.keys');
|
2015-11-04 01:40:59 +02:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var context = {};
|
|
|
|
vasync.pipeline({arg: context, funcs: [
|
|
|
|
function tryGetUser(ctx, next) {
|
|
|
|
var getOpts = {
|
|
|
|
id: opts.id,
|
|
|
|
membership: opts.roles
|
|
|
|
};
|
|
|
|
self.cloudapi.getUser(getOpts, function (err, user) {
|
|
|
|
if (err) {
|
|
|
|
if (err.restCode === 'ResourceNotFound') {
|
2015-11-21 22:41:16 +02:00
|
|
|
// TODO: feels like overkill to wrap this, ensure
|
|
|
|
// decent cloudapi error for this, then don't wrap.
|
|
|
|
next(new errors.ResourceNotFoundError(err,
|
|
|
|
format('user with login or id "%s" was not found',
|
|
|
|
opts.id)));
|
2015-11-04 01:40:59 +02:00
|
|
|
} else {
|
|
|
|
next(err);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ctx.user = user;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
2015-11-06 01:13:14 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
function getKeys(ctx, next) {
|
|
|
|
if (!opts.keys) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2015-11-10 01:09:37 +02:00
|
|
|
self.cloudapi.listUserKeys({userId: ctx.user.id},
|
|
|
|
function (err, keys) {
|
2015-11-06 01:13:14 +02:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ctx.user.keys = keys;
|
|
|
|
next();
|
|
|
|
});
|
2015-11-04 01:40:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, context.user);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-11-05 01:38:38 +02:00
|
|
|
/**
|
2015-11-21 22:41:16 +02:00
|
|
|
* Delete an RBAC role by ID or name.
|
2015-11-05 01:38:38 +02:00
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2015-11-21 22:41:16 +02:00
|
|
|
* - id {UUID|String} The role id (a UUID) or name.
|
2015-11-05 22:30:06 +02:00
|
|
|
* @param {Function} callback of the form `function (err)`
|
2015-11-05 01:38:38 +02:00
|
|
|
*/
|
|
|
|
TritonApi.prototype.deleteRole = function deleteRole(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
/*
|
|
|
|
* CloudAPI DeleteRole only accepts a role id (UUID).
|
|
|
|
*/
|
|
|
|
var context = {};
|
|
|
|
vasync.pipeline({arg: context, funcs: [
|
|
|
|
function getId(ctx, next) {
|
|
|
|
if (common.isUUID(opts.id)) {
|
|
|
|
ctx.id = opts.id;
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-11-21 22:41:16 +02:00
|
|
|
self.cloudapi.getRole({id: opts.id}, function (err, role) {
|
2015-11-05 22:30:06 +02:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
2015-11-05 01:38:38 +02:00
|
|
|
ctx.id = role.id;
|
2015-11-05 22:30:06 +02:00
|
|
|
next();
|
2015-11-05 01:38:38 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function deleteIt(ctx, next) {
|
|
|
|
self.cloudapi.deleteRole({id: ctx.id}, next);
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-11-04 01:40:59 +02:00
|
|
|
|
2015-11-05 22:30:06 +02:00
|
|
|
/**
|
2015-11-21 22:41:16 +02:00
|
|
|
* Delete an RBAC policy by ID or name.
|
2015-11-05 22:30:06 +02:00
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2015-11-21 22:41:16 +02:00
|
|
|
* - id {UUID|String} The policy id (a UUID) or name.
|
2015-11-05 22:30:06 +02:00
|
|
|
* @param {Function} callback of the form `function (err)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.deletePolicy = function deletePolicy(opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
/*
|
|
|
|
* CloudAPI DeletePolicy only accepts a policy id (UUID).
|
|
|
|
*/
|
|
|
|
var context = {};
|
|
|
|
vasync.pipeline({arg: context, funcs: [
|
|
|
|
function getId(ctx, next) {
|
|
|
|
if (common.isUUID(opts.id)) {
|
|
|
|
ctx.id = opts.id;
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-11-21 22:41:16 +02:00
|
|
|
self.cloudapi.getPolicy({id: opts.id}, function (err, policy) {
|
2015-11-05 22:30:06 +02:00
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ctx.id = policy.id;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function deleteIt(ctx, next) {
|
|
|
|
self.cloudapi.deletePolicy({id: ctx.id}, next);
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-02-09 02:49:00 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Reboot an instance by id.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: Required. The instance name, short id, or id (a UUID).
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the reboot is complete.
|
|
|
|
* Warning: Time skew (between the cloudapi server and the CN on
|
|
|
|
* which the instance resides) or a concurrent reboot can result in this
|
|
|
|
* polling being unable to notice the change properly. Use `waitTimeout`
|
|
|
|
* to put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} callback of the form `function (err, _, res)`
|
|
|
|
*
|
|
|
|
* Dev Note: This polls on MachineAudit... which might be heavy on TritonDC's
|
|
|
|
* currently implementation of that. PUBAPI-1347 is a better solution.
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.rebootInstance = function rebootInstance(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
function randrange(min, max) {
|
|
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
|
}
|
|
|
|
function timeDiffMs(relativeTo) {
|
|
|
|
var diff = process.hrtime(relativeTo);
|
|
|
|
var ms = (diff[0] * 1e3) + (diff[1] / 1e6); // in milliseconds
|
|
|
|
return ms;
|
|
|
|
}
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function rebootIt(arg, next) {
|
|
|
|
self.cloudapi.rebootMachine(arg.instId, function (err, _, _res) {
|
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function waitForIt(arg, next) {
|
|
|
|
if (!opts.wait) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Polling on the instance `state` doesn't work for a reboot,
|
|
|
|
* because a first poll value of "running" is ambiguous: was it
|
|
|
|
* a fast reboot, or has the instance not yet left the running
|
|
|
|
* state?
|
|
|
|
*
|
|
|
|
* Lacking PUBAPI-1347, we'll use the MachineAudit endpoint to
|
|
|
|
* watch for a 'reboot' action that finished after the server time
|
|
|
|
* for the RebootMachine response (i.e. the "Date" header), e.g.:
|
|
|
|
* date: Wed, 08 Feb 2017 20:55:35 GMT
|
|
|
|
* Example reboot audit entry:
|
|
|
|
* {"success":"yes",
|
|
|
|
* "time":"2017-02-08T20:55:44.045Z",
|
|
|
|
* "action":"reboot",
|
|
|
|
* ...}
|
|
|
|
*
|
|
|
|
* Hardcoded 2s poll interval for now (randomized for the first
|
|
|
|
* poll). Not yet configurable, being mindful of avoiding lots of
|
|
|
|
* clients naively swamping a CloudAPI and hitting throttling.
|
|
|
|
*/
|
|
|
|
var POLL_INTERVAL = 2 * 1000;
|
|
|
|
var startTime = process.hrtime();
|
|
|
|
var dateHeader = res.headers['date'];
|
|
|
|
var resTime = Date.parse(dateHeader);
|
|
|
|
if (!dateHeader) {
|
|
|
|
next(new errors.InternalError(format(
|
|
|
|
'cannot wait for reboot: CloudAPI RebootMachine response '
|
|
|
|
+ 'did not include a "Date" header (req %s)',
|
|
|
|
res.headers['request-id'])));
|
|
|
|
return;
|
|
|
|
} else if (isNaN(resTime)) {
|
|
|
|
next(new errors.InternalError(format(
|
|
|
|
'cannot wait for reboot: could not parse CloudAPI '
|
|
|
|
+ 'RebootMachine response "Date" header: "%s" (req %s)',
|
|
|
|
dateHeader, res.headers['request-id'])));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self.log.trace({id: arg.instId, resTime: resTime},
|
|
|
|
'wait for reboot audit record');
|
|
|
|
|
|
|
|
var pollMachineAudit = function () {
|
|
|
|
self.cloudapi.machineAudit(arg.instId, function (aErr, audit) {
|
|
|
|
if (aErr) {
|
|
|
|
next(aErr);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Search the top few audit records, in case some other
|
|
|
|
* action slipped in.
|
|
|
|
*/
|
|
|
|
var theRecord = null;
|
|
|
|
for (var i = 0; i < audit.length; i++) {
|
|
|
|
if (audit[i].action === 'reboot' &&
|
2017-04-07 23:44:35 +03:00
|
|
|
Date.parse(audit[i].time) > resTime) {
|
2017-02-09 02:49:00 +02:00
|
|
|
theRecord = audit[i];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!theRecord) {
|
|
|
|
if (opts.waitTimeout) {
|
|
|
|
var elapsedMs = timeDiffMs(startTime);
|
|
|
|
if (elapsedMs > opts.waitTimeout) {
|
|
|
|
next(new errors.TimeoutError(format('timeout '
|
|
|
|
+ 'waiting for instance %s reboot '
|
|
|
|
+ '(elapsed %ds)',
|
|
|
|
arg.instId,
|
|
|
|
Math.round(elapsedMs / 1000))));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTimeout(pollMachineAudit, POLL_INTERVAL);
|
|
|
|
} else if (theRecord.success !== 'yes') {
|
|
|
|
next(new errors.TritonError(format(
|
|
|
|
'reboot failed (audit id %s)', theRecord.id)));
|
|
|
|
} else {
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Add a random start delay to avoid a number of concurrent reboots
|
|
|
|
* all polling at the same time.
|
|
|
|
*/
|
|
|
|
setTimeout(pollMachineAudit,
|
|
|
|
(POLL_INTERVAL / 2) + randrange(0, POLL_INTERVAL));
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, null, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2017-02-17 03:00:32 +02:00
|
|
|
/**
|
|
|
|
* Resize a machine by id.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
|
|
|
* - {String} id: Required. The instance name, short id, or id (a UUID).
|
|
|
|
* - {String} package: Required. The new package name, shortId,
|
|
|
|
* or id (a UUID).
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the rename is complete.
|
|
|
|
* Warning: A concurrent resize of the same instance can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} callback of the form `function (err, _, res)`
|
|
|
|
*/
|
|
|
|
TritonApi.prototype.resizeInstance = function resizeInstance(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.package, 'opts.package');
|
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
vasync.pipeline(
|
|
|
|
{arg: {client: self, id: opts.id, package: opts.package}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
_stepPkgId,
|
|
|
|
|
|
|
|
function resizeMachine(arg, next) {
|
|
|
|
self.cloudapi.resizeMachine({id: arg.instId, package: arg.pkgId},
|
|
|
|
function (err, _res) {
|
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
function waitForSizeChanges(arg, next) {
|
|
|
|
if (!opts.wait) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self._waitForInstanceUpdate({
|
|
|
|
id: arg.instId,
|
|
|
|
timeout: opts.waitTimeout,
|
|
|
|
isUpdated: function (machine) {
|
|
|
|
return arg.pkgName === machine.package;
|
|
|
|
}
|
|
|
|
}, next);
|
|
|
|
}
|
|
|
|
]}, function (err) {
|
|
|
|
cb(err, null, res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-11-01 06:54:07 +02:00
|
|
|
/**
|
|
|
|
* rename a machine by id.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2016-12-21 04:30:24 +02:00
|
|
|
* - {String} id: Required. The instance name, short id, or id (a UUID).
|
|
|
|
* - {String} name: Required. The new instance name.
|
|
|
|
* - {Boolean} wait: Wait (via polling) until the rename is complete.
|
|
|
|
* Warning: A concurrent rename of the same instance can result in this
|
|
|
|
* polling being unable to notice the change. Use `waitTimeout` to
|
|
|
|
* put an upper bound.
|
|
|
|
* - {Number} waitTimeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting. Only relevant if
|
|
|
|
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} callback of the form `function (err, _, res)`
|
2016-11-01 06:54:07 +02:00
|
|
|
*/
|
|
|
|
TritonApi.prototype.renameInstance = function renameInstance(opts, cb) {
|
|
|
|
assert.string(opts.id, 'opts.id');
|
|
|
|
assert.string(opts.name, 'opts.name');
|
2016-12-21 04:30:24 +02:00
|
|
|
assert.optionalBool(opts.wait, 'opts.wait');
|
|
|
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
2016-11-01 06:54:07 +02:00
|
|
|
assert.func(cb, 'cb');
|
2016-12-21 04:30:24 +02:00
|
|
|
|
2016-11-01 06:54:07 +02:00
|
|
|
var self = this;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
|
|
|
_stepInstId,
|
|
|
|
|
|
|
|
function renameMachine(arg, next) {
|
|
|
|
self.cloudapi.renameMachine({id: arg.instId, name: opts.name},
|
2016-12-21 04:30:24 +02:00
|
|
|
function (err, _, _res) {
|
2016-11-01 06:54:07 +02:00
|
|
|
res = _res;
|
|
|
|
next(err);
|
|
|
|
});
|
2016-12-21 04:30:24 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
function waitForNameChanges(arg, next) {
|
|
|
|
if (!opts.wait) {
|
|
|
|
next();
|
|
|
|
return;
|
|
|
|
}
|
2017-02-17 03:00:32 +02:00
|
|
|
self._waitForInstanceUpdate({
|
2016-12-21 04:30:24 +02:00
|
|
|
id: arg.instId,
|
|
|
|
timeout: opts.waitTimeout,
|
2017-02-17 03:00:32 +02:00
|
|
|
isUpdated: function (machine) {
|
|
|
|
return opts.name === machine.name;
|
|
|
|
}
|
2016-12-21 04:30:24 +02:00
|
|
|
}, next);
|
2016-11-01 06:54:07 +02:00
|
|
|
}
|
|
|
|
]}, function (err) {
|
2016-12-21 04:30:24 +02:00
|
|
|
cb(err, null, res);
|
2016-11-01 06:54:07 +02:00
|
|
|
});
|
|
|
|
};
|
2015-11-05 22:30:06 +02:00
|
|
|
|
2016-12-21 04:30:24 +02:00
|
|
|
/**
|
|
|
|
* Shared implementation for any methods to change instance name.
|
|
|
|
*
|
|
|
|
* @param {Object} opts
|
2017-02-17 03:00:32 +02:00
|
|
|
* - {String} id: Required. The instance ID Required.
|
|
|
|
* - {Function} isUpdated: Required. A function which is passed the
|
|
|
|
* machine data, should check if the change has been applied and
|
|
|
|
* return a Boolean.
|
2016-12-21 04:30:24 +02:00
|
|
|
* - {Number} timeout: The number of milliseconds after which to
|
|
|
|
* timeout (call `cb` with a timeout error) waiting.
|
|
|
|
* Default is Infinity (i.e. it doesn't timeout).
|
|
|
|
* @param {Function} cb: `function (err)`
|
|
|
|
*/
|
2017-02-17 03:00:32 +02:00
|
|
|
TritonApi.prototype._waitForInstanceUpdate =
|
|
|
|
function _waitForInstanceUpdate(opts, cb) {
|
2016-12-21 04:30:24 +02:00
|
|
|
var self = this;
|
|
|
|
assert.object(opts, 'opts');
|
|
|
|
assert.uuid(opts.id, 'opts.id');
|
2017-02-17 03:00:32 +02:00
|
|
|
assert.func(opts.isUpdated, 'opts.isUpdated');
|
2016-12-21 04:30:24 +02:00
|
|
|
assert.optionalNumber(opts.timeout, 'opts.timeout');
|
|
|
|
var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity;
|
|
|
|
assert.ok(timeout > 0, 'opts.timeout must be greater than zero');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Hardcoded 2s poll interval for now. Not yet configurable, being mindful
|
|
|
|
* of avoiding lots of clients naively swamping a CloudAPI and hitting
|
|
|
|
* throttling.
|
|
|
|
*/
|
|
|
|
var POLL_INTERVAL = 2 * 1000;
|
|
|
|
|
|
|
|
var startTime = Date.now();
|
|
|
|
|
|
|
|
var poll = function () {
|
|
|
|
self.cloudapi.getMachine({id: opts.id}, function (err, machine) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
return;
|
|
|
|
}
|
2017-02-17 03:00:32 +02:00
|
|
|
if (opts.isUpdated(machine)) {
|
2016-12-21 04:30:24 +02:00
|
|
|
cb();
|
|
|
|
return;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
var elapsedTime = Date.now() - startTime;
|
|
|
|
if (elapsedTime > timeout) {
|
|
|
|
cb(new errors.TimeoutError(format('timeout waiting for '
|
|
|
|
+ 'instance %s rename (elapsed %ds)',
|
|
|
|
opts.id, Math.round(elapsedTime / 1000))));
|
|
|
|
} else {
|
|
|
|
setTimeout(poll, POLL_INTERVAL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
setImmediate(poll);
|
|
|
|
};
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
//---- exports
|
|
|
|
|
2015-11-25 21:04:44 +02:00
|
|
|
module.exports = {
|
|
|
|
CLOUDAPI_ACCEPT_VERSION: CLOUDAPI_ACCEPT_VERSION,
|
|
|
|
createClient: function createClient(opts) {
|
|
|
|
return new TritonApi(opts);
|
|
|
|
}
|
2015-09-30 01:13:34 +03:00
|
|
|
};
|