Hackathon - VNC
This commit is contained in:
parent
da5f3bade8
commit
6fb1825f94
103
lib/cloudapi2.js
103
lib/cloudapi2.js
@ -42,13 +42,15 @@ var querystring = require('querystring');
|
||||
var vasync = require('vasync');
|
||||
var auth = require('smartdc-auth');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var mod_watershed = require('watershed');
|
||||
var mod_restify = require('restify-clients');
|
||||
|
||||
var bunyannoop = require('./bunyannoop');
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
var SaferJsonClient = require('./SaferJsonClient');
|
||||
|
||||
|
||||
var shed = new mod_watershed.Watershed();
|
||||
|
||||
// ---- globals
|
||||
|
||||
@ -152,6 +154,9 @@ function CloudApi(options) {
|
||||
//this.token = options.token;
|
||||
|
||||
this.client = new SaferJsonClient(options);
|
||||
|
||||
// Websockets cannot use the JSON client
|
||||
this.webSocketClient = new mod_restify.HttpClient(options);
|
||||
}
|
||||
|
||||
|
||||
@ -302,6 +307,91 @@ CloudApi.prototype._request = function _request(opts, cb) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud API websocket request wrapper
|
||||
*
|
||||
* @param {Object|String} opts - object or string for endpoint
|
||||
* - {String} path - URL endpoint to hit
|
||||
* - {String} method - HTTP(s) request method
|
||||
* - {Object} data - data to be passed
|
||||
* - {Object} headers - optional additional request headers
|
||||
* @param {Function} cb passed via the restify client
|
||||
*/
|
||||
CloudApi.prototype._createWebSocket = function _createWebSocket(opts, cb) {
|
||||
var self = this;
|
||||
if (typeof (opts) === 'string')
|
||||
opts = {path: opts};
|
||||
assert.object(opts, 'opts');
|
||||
assert.optionalObject(opts.data, 'opts.data');
|
||||
assert.optionalString(opts.method, 'opts.method');
|
||||
assert.optionalObject(opts.headers, 'opts.headers');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var method = (opts.method || 'GET').toLowerCase();
|
||||
|
||||
// GET and POST are the only two methods that seem to make sense for
|
||||
// creating a websocket
|
||||
assert.ok(['get', 'post'].indexOf(method) >= 0,
|
||||
'invalid HTTP method given');
|
||||
|
||||
if (self.roles && self.roles.length > 0) {
|
||||
if (opts.path.indexOf('?') !== -1) {
|
||||
opts.path += '&as-role=' + self.roles.join(',');
|
||||
} else {
|
||||
opts.path += '?as-role=' + self.roles.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
self._getAuthHeaders(method, opts.path, function (err, headers) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var wskey = shed.generateKey();
|
||||
|
||||
if (opts.headers) {
|
||||
common.objMerge(headers, opts.headers);
|
||||
}
|
||||
common.objMerge(headers, {
|
||||
'connection': 'upgrade',
|
||||
'upgrade': 'websocket',
|
||||
'sec-websocket-key': wskey,
|
||||
'sec-websocket-version': 13
|
||||
});
|
||||
|
||||
var reqOpts = {
|
||||
path: opts.path,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var upgradeCb = function (upgradeErr, req, res, body) {
|
||||
if (upgradeErr) {
|
||||
cb(upgradeErr);
|
||||
return;
|
||||
}
|
||||
|
||||
req.once('upgradeResult', function (uErr, uRes, socket, head) {
|
||||
if (uErr) {
|
||||
cb(uErr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything currently using websockets are latency
|
||||
// sensitive, so send data as soon as it's given to us
|
||||
socket.setNoDelay(true);
|
||||
var client = shed.connect(uRes, socket, head, wskey);
|
||||
cb(null, client, uRes);
|
||||
});
|
||||
};
|
||||
|
||||
if (opts.data)
|
||||
self.webSocketClient[method](reqOpts, opts.data, upgradeCb);
|
||||
else
|
||||
self.webSocketClient[method](reqOpts, upgradeCb);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A simple wrapper around making a GET request to an endpoint and
|
||||
* passing back the body returned
|
||||
@ -831,6 +921,17 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a VNC connection to a HVM's console
|
||||
*
|
||||
* @param {String} uuid (required) The machine id.
|
||||
* @param {Function} callback of the form `function (err, shed, res)`
|
||||
*/
|
||||
CloudApi.prototype.getMachineVnc = function getMachineVnc(uuid, cb) {
|
||||
var endpoint = format('/%s/machines/%s/vnc', this.account, uuid);
|
||||
this._createWebSocket({path: endpoint}, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* resize a machine by id.
|
||||
*
|
||||
|
150
lib/do_instance/do_vnc.js
Normal file
150
lib/do_instance/do_vnc.js
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton instance vnc ...`
|
||||
*/
|
||||
|
||||
var net = require('net');
|
||||
var vasync = require('vasync');
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var format = require('util').format;
|
||||
|
||||
function getInstance(ctx, next) {
|
||||
ctx.cli.tritonapi.getInstance(ctx.id, function onInstance(err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.inst = inst;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function createServer(port, cb) {
|
||||
var server = net.createServer(function (conn) {
|
||||
cb(null, conn);
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
|
||||
server.on('listening', function serverListen() {
|
||||
var actualPort = server.address().port;
|
||||
var connstr = format('vnc://127.0.0.1:%d', actualPort);
|
||||
console.log('Listening on ' + connstr);
|
||||
});
|
||||
|
||||
server.on('error', function serverError(err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function startProxy(ctx, next) {
|
||||
createServer(ctx.port, function onConnect(cErr, conn) {
|
||||
if (cErr) {
|
||||
next(cErr);
|
||||
return;
|
||||
}
|
||||
|
||||
// VNC is latency sensitive, so send data as soon as it's available
|
||||
conn.setNoDelay(true);
|
||||
|
||||
// The VNC protocol starts with the _server_ sending a handshake
|
||||
// to the client, so we explicitly want to defer creation of the
|
||||
// websocket until we have a connection on the proxy
|
||||
ctx.cli.tritonapi.getInstanceVnc(ctx.inst.id, function vnc(vErr, shed) {
|
||||
conn.on('data', function serverData(data) {
|
||||
shed.send(data);
|
||||
});
|
||||
|
||||
shed.on('binary', function shedData(data) {
|
||||
conn.write(data);
|
||||
});
|
||||
|
||||
conn.on('end', function serverEnd(data) {
|
||||
console.log('# Connection closed');
|
||||
shed.end();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
shed.on('end', function shedEnd(code, reason) {
|
||||
conn.end();
|
||||
// XXX: Should we translate codes into exit values?
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
shed.on('error', function shedError(shedErr) {
|
||||
conn.end();
|
||||
// send 'end' event should be called after this
|
||||
});
|
||||
|
||||
shed.on('connectionReset', function shedReset() {
|
||||
console.log('# Connection reset by peer');
|
||||
conn.end();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function do_vnc(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length === 0) {
|
||||
callback(new errors.UsageError('missing INST arg'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args.shift();
|
||||
var port = opts.port || 0;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top, id: id, port: port}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
// We could skip the instance lookup here and directly call
|
||||
// tritonapi.getInstanceVnc with 'id', however, the instance id given
|
||||
// would not be validated until a connection is made to the server
|
||||
// proxy we create with start_server. Instead the id is validated
|
||||
// before we start the proxy so that we can immediately exit if
|
||||
// there is an error.
|
||||
getInstance,
|
||||
startProxy
|
||||
]}, callback);
|
||||
}
|
||||
|
||||
do_vnc.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['port', 'p'],
|
||||
helpArg: 'PORT',
|
||||
type: 'positiveInteger',
|
||||
help: 'The port number the server listens on. If not specified, '
|
||||
+ 'a random port number is used.'
|
||||
}
|
||||
];
|
||||
|
||||
do_vnc.synopses = ['{{name}} vnc [OPTIONS] INST'];
|
||||
do_vnc.help = [
|
||||
'Start VNC server for instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_vnc.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_vnc;
|
@ -45,6 +45,7 @@ function InstanceCLI(top) {
|
||||
'enable-firewall',
|
||||
'disable-firewall',
|
||||
{ group: '' },
|
||||
'vnc',
|
||||
'ssh',
|
||||
'ip',
|
||||
'wait',
|
||||
@ -77,6 +78,7 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
|
||||
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
|
||||
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall');
|
||||
|
||||
InstanceCLI.prototype.do_vnc = require('./do_vnc');
|
||||
InstanceCLI.prototype.do_ssh = require('./do_ssh');
|
||||
InstanceCLI.prototype.do_ip = require('./do_ip');
|
||||
InstanceCLI.prototype.do_wait = require('./do_wait');
|
||||
|
@ -1139,6 +1139,39 @@ TritonApi.prototype.getInstance = function getInstance(opts, cb) {
|
||||
});
|
||||
};
|
||||
|
||||
// ---- instance console vnc
|
||||
|
||||
/**
|
||||
* Get VNC connection to the console of an HVM instance.
|
||||
* @param {String} id: Required. The instance UUID, name, or short ID.
|
||||
* @param {Function} callback of the form `function (err, shed)`
|
||||
*/
|
||||
TritonApi.prototype.getInstanceVnc = function getInstanceVnc(id, cb) {
|
||||
assert.string(id, 'id');
|
||||
|
||||
var self = this;
|
||||
var shed = null;
|
||||
|
||||
vasync.pipeline({arg: {client: self, id: id}, funcs: [
|
||||
_stepInstId,
|
||||
function getVncConnection(arg, next) {
|
||||
self.cloudapi.getMachineVnc(arg.instId, function (err, shed_, _) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
shed = shed_;
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
} else {
|
||||
cb(null, shed);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ---- instance enable/disable firewall
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
"tabula": "1.9.0",
|
||||
"vasync": "1.6.3",
|
||||
"verror": "1.10.0",
|
||||
"watershed": "0.4.0",
|
||||
"which": "1.2.4",
|
||||
"wordwrap": "1.0.0"
|
||||
},
|
||||
|
Reference in New Issue
Block a user