run-on-ssh
Version:
Run a node.js script on a given ssh server
233 lines (206 loc) • 7.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _promise = require('promise');
var _promise2 = _interopRequireDefault(_promise);
var _thenRequest = require('then-request');
var _thenRequest2 = _interopRequireDefault(_thenRequest);
var _ssh = require('./ssh');
var _ssh2 = _interopRequireDefault(_ssh);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var BASE_URL = 'https://api.digitalocean.com/v2';
var DigiatlOcean = function () {
function DigiatlOcean(token) {
(0, _classCallCheck3.default)(this, DigiatlOcean);
this._token = token;
this._runningPolls = 0;
this._rateLimit = {
limit: 5000,
remaining: 5000,
reset: Math.floor(Date.now() / 1000)
};
}
(0, _createClass3.default)(DigiatlOcean, [{
key: '_timeToRateLimitReset',
value: function _timeToRateLimitReset() {
return Math.max(1000, this._rateLimit.reset * 1000 - Date.now());
}
}, {
key: 'request',
value: function request(method, path, options) {
var _this = this;
path = path.replace(BASE_URL, '');
return new _promise2.default(function (resolve, reject) {
var attempt = function attempt() {
(0, _thenRequest2.default)(method, BASE_URL + path, (0, _extends3.default)({}, options || {}, {
headers: (0, _extends3.default)({}, options && options.headers ? options.headers : {}, {
authorization: 'Bearer ' + _this._token
})
})).done(function (res) {
if (res.headers['ratelimit-limit']) {
_this._rateLimit = {
limit: res.headers['ratelimit-limit'],
remaining: res.headers['ratelimit-remaining'],
reset: res.headers['ratelimit-reset']
};
if (res.statusCode === 429) {
console.log('Rate limit exceeded, backing off for ' + _this._timeToRateLimitReset() / 1000 + ' seconds');
return setTimeout(attempt, _this._timeToRateLimitReset());
}
}
resolve(res);
}, reject);
};
attempt();
});
}
}, {
key: 'requestJSON',
value: function requestJSON(method, path, options) {
return this.request(method, path, options).then(function (res) {
return JSON.parse(res.getBody('utf8'));
});
}
}, {
key: 'getPaged',
value: function getPaged(path, key, filter) {
var _this2 = this;
return new _promise2.default(function (resolve, reject) {
var result = [];
var nextPage = function nextPage(url) {
_this2.requestJSON('get', url).done(function (response) {
result.push.apply(result, (0, _toConsumableArray3.default)(filter ? response[key].filter(filter) : response[key]));
if (response.links && response.links.pages && response.links.pages.next) {
nextPage(response.links.pages.next);
} else {
resolve(result);
}
}, reject);
};
nextPage(path);
});
}
}, {
key: 'poll',
value: function poll(path, isReady, timeout) {
var _this3 = this;
this._runningPolls++;
timeout = timeout || 60 * 1000;
var timeoutEnd = Date.now() + timeout;
return new _promise2.default(function (resolve, reject) {
var poll = function poll(finalAttempt) {
_this3.requestJSON('get', path).then(function (result) {
if (isReady(result)) {
resolve(result);
} else if (finalAttempt) {
reject(new Error('Operation timed out after ' + timeout / 1000 + ' seconds'));
} else if (Date.now() > timeoutEnd) {
poll(true);
} else {
// poll less frequently for operations that are typically slower
// poll less frequently if we are running out of rate limit
var timeToRateLimitReset = _this3._timeToRateLimitReset();
// console.log(
// this._rateLimit.remaining + ' remaining of ' + this._rateLimit.limit + ' with reset in ' +
// (timeToRateLimitReset / 1000) + ' seconds'
// );
var remaining = (_this3._rateLimit.remaining || 1) / _this3._runningPolls;
// console.log('backoff: ' + (remaining / 1000));
setTimeout(function () {
return poll();
}, timeout / 60 + timeToRateLimitReset / remaining);
}
}).done(null, reject);
};
poll();
}).finally(function () {
return _this3._runningPolls--;
});
}
}, {
key: 'connect',
value: function connect(droplet, options) {
var addresses = droplet.networks.v4.filter(function (n) {
return n.type === 'public';
});
if (addresses.length === 0) {
throw new Error('Could not find the IP address of droplet');
}
var connection = new _ssh2.default({
host: addresses[0].ip_address,
port: 22,
username: 'root',
privateKey: this._privateKey
}, options);
return connection.ready.then(function () {
return connection;
});
}
}, {
key: 'runAction',
value: function runAction(path, action, options) {
var _this4 = this;
return this.requestJSON('post', path + '/actions', { json: action }).then(function (result) {
var actions = result.actions ? result.actions : [result.action];
return _promise2.default.all(actions.map(function (_ref) {
var id = _ref.id;
return _this4.poll('/actions/' + id, function (result) {
return result.action.status !== 'in-progress';
}, options && options.timeout).then(function (result) {
if (result.action.status !== 'completed') {
throw new Error(action.type + ' did not complete successfully');
}
});
}));
}).then(function () {});
}
}, {
key: 'renameDroplet',
value: function renameDroplet(droplet, name) {
return this._runAction('/droplets/' + droplet.id, { type: 'rename', name: name }).then(function () {
return droplet;
});
}
}, {
key: 'rebuildDroplet',
value: function rebuildDroplet(droplet, image) {
console.log('rebuild droplet');
return this._runAction('/droplets/' + droplet.id, { type: 'rebuild', image: image }).then(function () {
return droplet;
});
}
}, {
key: 'shutdownDroplet',
value: function shutdownDroplet(droplet) {
var _this5 = this;
console.log('shutdown droplet');
return new _promise2.default(function (resolve, reject) {
var attempt = function attempt(attemptNumber) {
_this5.runAction('/droplets/' + droplet.id, { type: 'shutdown' }).then(function () {
return _this5.poll('/droplets/' + droplet.id, function (result) {
return result.droplet.status === 'off';
});
}).done(resolve, function (err) {
if (attemptNumber < 3) {
attempt(attemptNumber + 1);
} else {
reject(err);
}
});
};
attempt(0);
});
}
}]);
return DigiatlOcean;
}();
exports.default = DigiatlOcean;