-
Notifications
You must be signed in to change notification settings - Fork 249
RFD 189: Console Access - vmadmd console proxy #1159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
nwilkens
wants to merge
9
commits into
master
Choose a base branch
from
rfd-0189-console-access
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
fd44461
Add console proxy support to vmadmd
nwilkens b4a61eb
Add console to runtime_info for all VM brands
nwilkens f6b4547
Spawn console proxy for all running VMs at startup
nwilkens c09edc8
Fix console proxy to use fs streams for device files
nwilkens dda998b
Use socat to proxy zone console devices
nwilkens 44a60a8
Use zoneadmd console socket instead of /dev/zcons device
nwilkens abfe095
Add zoneadmd console handshake for non-KVM brands
nwilkens 44e65b8
Fix resource leaks and add handshake timeout in console proxy
nwilkens 5e564d5
Add named constants and improve protocol validation
nwilkens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,6 +63,11 @@ var REPORTED_STATES = ['running', 'stopped']; | |
| var VMADMD_PORT = 8080; | ||
| var VMADMD_AUTOBOOT_FILE = '/tmp/.autoboot_vmadmd'; | ||
|
|
||
| // Console proxy configuration | ||
| var CONSOLE_HANDSHAKE_TIMEOUT = 5000; // Timeout for zoneadmd handshake (ms) | ||
| var CONSOLE_LOG_TRUNCATE_LEN = 100; // Max chars to log from handshake errors | ||
|
|
||
| var CONSOLE = {}; | ||
| var PROV_WAIT = {}; | ||
| var SDC = {}; | ||
| var SPICE = {}; | ||
|
|
@@ -411,6 +416,186 @@ function reloadRemoteDisplay(vmobj) | |
| spawnRemoteDisplay(vmobj); | ||
| } | ||
|
|
||
| // | ||
| // spawnConsoleProxy() | ||
| // | ||
| // Creates a TCP proxy for console access (serial console for KVM, zone console | ||
| // for other brands). | ||
| // | ||
| // For KVM: Proxies to unix socket at <zonepath>/root/tmp/vm.console | ||
| // For other brands: Proxies to zone console device at /dev/zcons/<zonename>/zoneconsole | ||
| // | ||
| // vmobj must have: | ||
| // | ||
| // brand | ||
| // state | ||
| // uuid | ||
| // zonename | ||
| // zonepath | ||
| // zone_state | ||
| // | ||
| function spawnConsoleProxy(vmobj) | ||
| { | ||
| var addr; | ||
| var consolePath; | ||
| var port = 0; // Let the OS assign an ephemeral port | ||
| var server; | ||
| var zonepath = vmobj.zonepath; | ||
|
|
||
| if (!vmobj.zonepath) { | ||
| zonepath = '/zones/' + vmobj.uuid; | ||
| } | ||
|
|
||
| if (vmobj.state !== 'running' && vmobj.zone_state !== 'running') { | ||
| log.debug('skipping console setup for non-running VM ' + vmobj.uuid); | ||
| return; | ||
| } | ||
|
|
||
| // Determine console path based on brand | ||
| if (vmobj.brand === 'kvm') { | ||
| // KVM uses unix socket for serial console | ||
| consolePath = path.join(zonepath, '/root/tmp/vm.console'); | ||
| } else { | ||
| // Bhyve, Joyent, LX, etc. use zoneadmd console socket | ||
| consolePath = '/var/run/zones/' + vmobj.zonename + '.console_sock'; | ||
| } | ||
|
|
||
| // Create TCP server that proxies to console socket | ||
| server = net.createServer(function (c) { | ||
| var consoleSocket = new net.Socket(); | ||
| var remote_address = ''; | ||
| var isKvm = (vmobj.brand === 'kvm'); | ||
| var handshakeDone = false; | ||
| var handshakeTimer = null; | ||
| var cleanedUp = false; | ||
|
|
||
| remote_address = '[' + c.remoteAddress + ']:' + c.remotePort; | ||
|
|
||
| // Cleanup function ensures all resources are properly released | ||
| function cleanup() { | ||
| if (cleanedUp) { | ||
| return; | ||
| } | ||
| cleanedUp = true; | ||
|
|
||
| if (handshakeTimer) { | ||
| clearTimeout(handshakeTimer); | ||
| handshakeTimer = null; | ||
| } | ||
|
|
||
| if (consoleSocket && !consoleSocket.destroyed) { | ||
| consoleSocket.destroy(); | ||
| } | ||
| } | ||
|
|
||
| c.on('close', function (had_error) { | ||
| log.info('console connection ended from ' + remote_address + | ||
| ' for VM ' + vmobj.uuid); | ||
| cleanup(); | ||
| }); | ||
|
|
||
| consoleSocket.on('error', function (err) { | ||
| log.warn('console socket error for VM ' + vmobj.uuid + | ||
| ': ' + err.message); | ||
| cleanup(); | ||
| c.end(); | ||
| }); | ||
|
|
||
| c.on('error', function (err) { | ||
| log.warn('console net socket error for VM ' + vmobj.uuid + | ||
| ': ' + err.message); | ||
| cleanup(); | ||
| }); | ||
|
|
||
| // For non-KVM brands, perform zoneadmd console handshake | ||
| if (!isKvm) { | ||
| // Set timeout for handshake completion | ||
| handshakeTimer = setTimeout(function() { | ||
| if (!handshakeDone) { | ||
| log.error('console handshake timeout for VM ' + vmobj.uuid); | ||
| cleanup(); | ||
| c.end(); | ||
| } | ||
| }, CONSOLE_HANDSHAKE_TIMEOUT); | ||
|
|
||
| consoleSocket.once('connect', function () { | ||
| // Send zlogin-C handshake: IDENT <locale> <flags>\n | ||
| consoleSocket.write('IDENT C 0\n'); | ||
|
|
||
| // Wait for OK response before starting data flow | ||
| consoleSocket.once('data', function (data) { | ||
| clearTimeout(handshakeTimer); | ||
| handshakeTimer = null; | ||
|
|
||
| if (data.toString().trim() === 'OK') { | ||
| handshakeDone = true; | ||
| // Now start bidirectional pipe | ||
| c.pipe(consoleSocket); | ||
| consoleSocket.pipe(c); | ||
| } else { | ||
| log.error('console handshake failed for VM ' + vmobj.uuid + | ||
| ': ' + data.toString().substring(0, CONSOLE_LOG_TRUNCATE_LEN)); | ||
| cleanup(); | ||
| c.end(); | ||
| } | ||
| }); | ||
| }); | ||
| } else { | ||
| // KVM brand: pipe immediately (no handshake needed) | ||
| handshakeDone = true; | ||
| c.pipe(consoleSocket); | ||
| consoleSocket.pipe(c); | ||
| } | ||
|
|
||
| // Connect to console socket | ||
| consoleSocket.connect(consolePath); | ||
| }); | ||
|
|
||
| log.info('spawning console listener for ' + vmobj.uuid + | ||
| ' on ' + SDC.sysinfo.admin_ip + ' (brand: ' + vmobj.brand + ')'); | ||
|
|
||
| server.on('connection', function (sock) { | ||
| log.info('console connection started from [' + | ||
| sock.remoteAddress + ']:' + sock.remotePort + ' for VM ' + vmobj.uuid); | ||
| }); | ||
|
|
||
| server.on('error', function (err) { | ||
| log.error('console server error for VM ' + vmobj.uuid + ': ' + err.message); | ||
| }); | ||
|
|
||
| server.listen(port, SDC.sysinfo.admin_ip, function () { | ||
| addr = server.address(); | ||
|
|
||
| CONSOLE[vmobj.uuid] = { | ||
| 'host': SDC.sysinfo.admin_ip, | ||
| 'port': addr.port, | ||
| 'server': server, | ||
| 'type': 'socket', | ||
| 'path': consolePath | ||
| }; | ||
|
|
||
| log.info('console proxy for ' + vmobj.uuid + ' listening on ' + | ||
| SDC.sysinfo.admin_ip + ':' + addr.port + | ||
| ' (type: ' + CONSOLE[vmobj.uuid].type + ', path: ' + consolePath + ')'); | ||
| }); | ||
| } | ||
|
|
||
| function clearConsoleProxy(uuid) | ||
| { | ||
| if (CONSOLE[uuid] && CONSOLE[uuid].server) { | ||
| log.info('clearing console proxy for ' + uuid); | ||
| CONSOLE[uuid].server.close(); | ||
| } | ||
| delete CONSOLE[uuid]; | ||
| } | ||
|
|
||
| function reloadConsoleProxy(vmobj) | ||
| { | ||
| log.info('reloading console proxy for ' + vmobj.uuid); | ||
| clearConsoleProxy(vmobj.uuid); | ||
| spawnConsoleProxy(vmobj); | ||
| } | ||
|
|
||
| function clearTimer(uuid) | ||
| { | ||
| if (TIMER.hasOwnProperty(uuid)) { | ||
|
|
@@ -422,6 +607,7 @@ function clearTimer(uuid) | |
| function clearVM(uuid) | ||
| { | ||
| clearRemoteDisplay(uuid); | ||
| clearConsoleProxy(uuid); | ||
| clearTimer(uuid); | ||
| } | ||
|
|
||
|
|
@@ -516,6 +702,7 @@ function handleProvisioning(vmobj, cb) | |
| rotateKVMLog(vmobj.uuid); | ||
| } | ||
| spawnRemoteDisplay(obj); | ||
| spawnConsoleProxy(obj); | ||
| } | ||
| cb(null, 'success'); | ||
| }); | ||
|
|
@@ -1139,6 +1326,7 @@ function updateZoneStatus(ev) | |
| rotateKVMLog(vmobj.uuid); | ||
| } | ||
| spawnRemoteDisplay(vmobj); | ||
| spawnConsoleProxy(vmobj); | ||
| } else if (ev.oldstate === 'running') { | ||
| if (VNC.hasOwnProperty(ev.zonename)) { | ||
| // VMs always have zonename === uuid, so we can remove this | ||
|
|
@@ -1567,6 +1755,11 @@ function infoVM(uuid, types, callback) | |
| } | ||
| } | ||
| } | ||
| if ((types.indexOf('all') !== -1) | ||
| || (types.indexOf('console') !== -1)) { | ||
|
|
||
| infoConsole(); | ||
| } | ||
| callback(null, res); | ||
| } | ||
| }); | ||
|
|
@@ -1580,9 +1773,24 @@ function infoVM(uuid, types, callback) | |
| if (types.indexOf('all') !== -1 || types.indexOf('vnc') !== -1) { | ||
| infoVNC(); | ||
| } | ||
| if (types.indexOf('all') !== -1 || types.indexOf('console') !== -1) { | ||
| infoConsole(); | ||
| } | ||
| callback(null, res); | ||
| }; | ||
|
|
||
| // Generic callback for other brands (joyent, lx, etc.) - only console info | ||
| loadCbs.joyent = loadCbs['joyent-minimal'] = loadCbs.lx = | ||
| function loadGenericCb(vmobj) { | ||
| assert.object(vmobj); | ||
| assert.uuid(vmobj.uuid); | ||
|
|
||
| if (types.indexOf('all') !== -1 || types.indexOf('console') !== -1) { | ||
| infoConsole(); | ||
| } | ||
| callback(null, res); | ||
| }; | ||
|
|
||
| function infoVNC() { | ||
| res.vnc = {}; | ||
| if (VNC.hasOwnProperty(uuid)) { | ||
|
|
@@ -1598,6 +1806,15 @@ function infoVM(uuid, types, callback) | |
| } | ||
| } | ||
| } | ||
|
|
||
| function infoConsole() { | ||
| res.console = {}; | ||
| if (CONSOLE.hasOwnProperty(uuid)) { | ||
| res.console.host = CONSOLE[uuid].host; | ||
| res.console.port = CONSOLE[uuid].port; | ||
| res.console.type = CONSOLE[uuid].type; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function resetVM(uuid, callback) | ||
|
|
@@ -1831,6 +2048,9 @@ function loadVM(vmobj, do_autoboot) | |
|
|
||
| // Start Remote Display | ||
| spawnRemoteDisplay(vmobj); | ||
|
|
||
| // Start Console Proxy | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This and other always-on calls to spawnRemoteDisplay() have me curious about an overarching question. Will ask it in the overarching comment. |
||
| spawnConsoleProxy(vmobj); | ||
| } | ||
|
|
||
| // To help diagnose problems we write the keys we're watching to the TRACE log | ||
|
|
@@ -2528,6 +2748,10 @@ function main() | |
| }); | ||
| } else { | ||
| log.debug('ignoring non-kvm VM ' + vmobj.uuid); | ||
| // Still spawn console proxy for non-KVM running VMs | ||
| if (vmobj.state === 'running' || vmobj.zone_state === 'running') { | ||
| spawnConsoleProxy(vmobj); | ||
| } | ||
| upg_cb(); | ||
| } | ||
| } | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CONSOLE object is being populated inside the async callback, but the function returns immediately without waiting for the server to start listening. This creates a race condition where other code trying to access CONSOLE[vmobj.uuid] might find it undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A JS person should reality-check this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this moves to non-draft we'll need >= two reviewers for JS reality checking.