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 vasync = require('vasync');
|
||||||
var auth = require('smartdc-auth');
|
var auth = require('smartdc-auth');
|
||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
var mod_watershed = require('watershed');
|
||||||
|
var mod_restify = require('restify-clients');
|
||||||
|
|
||||||
var bunyannoop = require('./bunyannoop');
|
var bunyannoop = require('./bunyannoop');
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
var SaferJsonClient = require('./SaferJsonClient');
|
var SaferJsonClient = require('./SaferJsonClient');
|
||||||
|
|
||||||
|
var shed = new mod_watershed.Watershed();
|
||||||
|
|
||||||
// ---- globals
|
// ---- globals
|
||||||
|
|
||||||
@ -152,6 +154,9 @@ function CloudApi(options) {
|
|||||||
//this.token = options.token;
|
//this.token = options.token;
|
||||||
|
|
||||||
this.client = new SaferJsonClient(options);
|
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
|
* A simple wrapper around making a GET request to an endpoint and
|
||||||
* passing back the body returned
|
* 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.
|
* 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',
|
'enable-firewall',
|
||||||
'disable-firewall',
|
'disable-firewall',
|
||||||
{ group: '' },
|
{ group: '' },
|
||||||
|
'vnc',
|
||||||
'ssh',
|
'ssh',
|
||||||
'ip',
|
'ip',
|
||||||
'wait',
|
'wait',
|
||||||
@ -77,6 +78,7 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
|
|||||||
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
|
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
|
||||||
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_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_ssh = require('./do_ssh');
|
||||||
InstanceCLI.prototype.do_ip = require('./do_ip');
|
InstanceCLI.prototype.do_ip = require('./do_ip');
|
||||||
InstanceCLI.prototype.do_wait = require('./do_wait');
|
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
|
// ---- instance enable/disable firewall
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"tabula": "1.9.0",
|
"tabula": "1.9.0",
|
||||||
"vasync": "1.6.3",
|
"vasync": "1.6.3",
|
||||||
"verror": "1.10.0",
|
"verror": "1.10.0",
|
||||||
|
"watershed": "0.4.0",
|
||||||
"which": "1.2.4",
|
"which": "1.2.4",
|
||||||
"wordwrap": "1.0.0"
|
"wordwrap": "1.0.0"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user