1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
|
# Copyright 2015, 2018 IBM Corp.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Manage LPAR virtual terminals."""
import re
import select
import six
import socket
import ssl
import struct
import subprocess
import threading
import time
from oslo_concurrency import lockutils as lock
from oslo_log import log as logging
from oslo_utils import encodeutils
import pypowervm.const as c
from pypowervm import exceptions as pvm_exc
from pypowervm.i18n import _
from pypowervm.wrappers import job
import pypowervm.wrappers.logical_partition as pvm_lpar
LOG = logging.getLogger(__name__)
_SUFFIX_PARM_CLOSE_VTERM = 'CloseVterm'
# Used to track of the mapping between the ports and the Listeners/Repeaters
# that we construct for those and also keeping track of which local port
# is for a given LPAR UUID and want VNC Path String is provided for the LPAR.
#
# These are global variables used below. Since they are defined up here, need
# to use global as a way for modification of the fields to stick. We do this
# so that we keep track of all of the connections.
_VNC_REMOTE_PORT_TO_LISTENER = {}
_VNC_LOCAL_PORT_TO_REPEATER = {}
_VNC_UUID_TO_LOCAL_PORT = {}
_VNC_PATH_TO_UUID = {}
# For the single remote port case, we will hard-code that to 5901 for now
_REMOTE_PORT = 5901
def close_vterm(adapter, lpar_uuid):
"""Close the vterm associated with an lpar
:param adapter: The adapter to talk over the API.
:param lpar_uuid: partition uuid
"""
if adapter.traits.local_api:
_close_vterm_local(adapter, lpar_uuid)
else:
_close_vterm_non_local(adapter, lpar_uuid)
def _close_vterm_non_local(adapter, lpar_uuid):
"""Job to force the close of the terminal when the API is remote.
:param adapter: The adapter to talk over the API.
:param lpar_uuid: partition uuid
"""
# Close vterm on the lpar
resp = adapter.read(pvm_lpar.LPAR.schema_type, lpar_uuid,
suffix_type=c.SUFFIX_TYPE_DO,
suffix_parm=_SUFFIX_PARM_CLOSE_VTERM)
job_wrapper = job.Job.wrap(resp.entry)
try:
job_wrapper.run_job(lpar_uuid)
except Exception:
LOG.exception(_('Unable to close vterm.'))
raise
def _close_vterm_local(adapter, lpar_uuid):
"""Forces the close of the terminal on a local system.
Will check for a VNC server as well in case it was started via that
mechanism.
:param adapter: The adapter to talk over the API.
:param lpar_uuid: partition uuid
"""
lpar_id = _get_lpar_id(adapter, lpar_uuid)
_run_proc(['rmvterm', '--id', lpar_id])
# Stop the port.
with lock.lock('powervm_vnc_term'):
vnc_port = _VNC_UUID_TO_LOCAL_PORT.get(lpar_uuid, 0)
if vnc_port in _VNC_LOCAL_PORT_TO_REPEATER:
_VNC_LOCAL_PORT_TO_REPEATER[vnc_port].stop()
def open_localhost_vnc_vterm(adapter, lpar_uuid, force=False):
"""Opens a VNC vTerm to a given LPAR. Always binds to localhost.
:param adapter: The adapter to drive the PowerVM API
:param lpar_uuid: Partition UUID.
:param force: (Optional, Default: False) If set to true will force the
console to be opened as VNC even if it is already opened
via some other means.
:return: The VNC Port that the terminal is running on.
"""
# This API can only run if local.
if not adapter.traits.local_api:
raise pvm_exc.ConsoleNotLocal()
lpar_id = _get_lpar_id(adapter, lpar_uuid)
def _run_mkvterm_cmd(lpar_uuid, force):
cmd = ['mkvterm', '--id', str(lpar_id), '--vnc', '--local']
ret_code, std_out, std_err = _run_proc(cmd)
# If the vterm was already started, the mkvterm command will always
# return an error message with a return code of 3. However, there
# are 2 scenarios here, one where it was started with the VNC option
# previously, which we will get a valid port number back (which is
# the good path scenario), and one where it was started out-of-band
# where we will get no port. If it is the out-of-band scenario and
# they asked us to force the connection, then we will attempt to
# terminate the old vterm session so we can start up one with VNC.
if force and ret_code == 3 and not _parse_vnc_port(std_out):
LOG.warning(_("Invalid output on vterm open. Trying to reset the "
"vterm. Error was %s"), std_err)
close_vterm(adapter, lpar_uuid)
ret_code, std_out, std_err = _run_proc(cmd)
# The only error message that is fine is a return code of 3 that a
# session is already started, where we got back the port back meaning
# that it was started as VNC. Else, raise up the error message.
if ret_code != 0 and ret_code != 3:
raise pvm_exc.VNCBasedTerminalFailedToOpen(err=std_err)
# Parse the VNC Port out of the stdout returned from mkvterm
return _parse_vnc_port(std_out)
return _run_mkvterm_cmd(lpar_uuid, force)
def open_remotable_vnc_vterm(
adapter, lpar_uuid, local_ip, remote_ips=None, vnc_path=None,
use_x509_auth=False, ca_certs=None, server_cert=None, server_key=None,
force=False):
"""Opens a VNC vTerm to a given LPAR. Wraps in some validation.
Must run on the management partition.
:param adapter: The adapter to drive the PowerVM API
:param lpar_uuid: Partition UUID.
:param local_ip: The IP Address to bind the VNC server to. This would be
the IP of the management network on the system.
:param remote_ips: (Optional, Default: None) A binding to only accept
clients that are from a specific list of IP addresses
through. Default is None, and therefore will allow any
remote IP to connect.
:param vnc_path: (Optional, Default: None) If provided, the vnc client must
pass in this path (in HTTP format) to connect to the
VNC server.
The path is in HTTP format. So if the vnc_path is 'Test'
the first packet request into the VNC must be:
"CONNECT Test HTTP/1.1\r\n\r\n"
If the client passes in an invalid request, a 400 Bad
Request will be returned. If the client sends in the
correct path a 200 OK will be returned.
If no vnc_path is specified, then no path is expected
to be passed in by the VNC client and it will listen
on the same remote port as local port. If the path is
specified then it will listen on the on a single remote
port of 5901 and determine the LPAR based on this path.
:param use_x509_auth: (Optional, Default: False) If enabled, uses X509
Authentication for the VNC sessions started for VMs.
:param ca_certs: (Optional, Default: None) Path to CA certificate to
use for verifying VNC X509 Authentication. Only used
if use_x509_auth is set to True.
:param server_cert: (Optional, Default: None) Path to Server certificate
to use for verifying VNC X509 Authentication. Only
used if use_x509_auth is set to True.
:param server_key: (Optional, Default: None) Path to Server private key
to use for verifying VNC X509 Authentication. Only
used if use_x509_auth is set to True.
:param force: (Optional, Default: False) If set to true will force the
console to be opened as VNC even if it is already opened
via some other means.
:return: The VNC Port that the terminal is running on.
"""
# This API can only run if local.
if not adapter.traits.local_api:
raise pvm_exc.ConsoleNotLocal()
# Open the VNC Port. If already open, it will just return the same port,
# so no harm re-opening. The stdout will just print out the existing port.
local_port = open_localhost_vnc_vterm(adapter, lpar_uuid, force=force)
# If a VNC path is provided then we have a way to map an incoming
# connection to a given LPAR and will use the single 5901 port, otherwise
# we need to listen for remote connections on the same port as the local
# one so we know which VNC session to forward the connection's data to
remote_port = _REMOTE_PORT if vnc_path is not None else local_port
if local_port:
_VNC_UUID_TO_LOCAL_PORT[lpar_uuid] = local_port
# We will use a flag to the Socket Listener to tell it whether the
# user provided us a VNC Path we should use to look up the UUID from
if vnc_path is not None:
verify_vnc_path = True
_VNC_PATH_TO_UUID[vnc_path] = lpar_uuid
else:
verify_vnc_path = False
# See if we have a VNC repeater already...if so, nothing to do. If not,
# start it up.
with lock.lock('powervm_vnc_term'):
if remote_port not in _VNC_REMOTE_PORT_TO_LISTENER:
listener = _VNCSocketListener(
adapter, remote_port, local_ip, verify_vnc_path,
remote_ips=remote_ips)
# If we are doing x509 Authentication, then setup the certificates
if use_x509_auth:
listener.set_x509_certificates(
ca_certs, server_cert, server_key)
_VNC_REMOTE_PORT_TO_LISTENER[remote_port] = listener
listener.start()
return remote_port
def _run_proc(cmd):
"""Simple wrapper to run a process.
Will return the return code along with the stdout and stderr. It is the
decision of the caller if it wishes to honor or ignore the return code.
:return: The return code, stdout and stderr from the command.
"""
process = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True, env=None)
process.wait()
stdout, stderr = process.communicate()
# Convert the stdout/stderr output from a byte-string to a unicode-string
# so it doesn't blow up later on anything doing an implicit conversion
stdout = encodeutils.safe_decode(stdout)
stderr = encodeutils.safe_decode(stderr)
return process.returncode, stdout, stderr
def _get_lpar_id(adapter, lpar_uuid):
lpar_resp = adapter.read(pvm_lpar.LPAR.schema_type, root_id=lpar_uuid,
suffix_type='quick', suffix_parm='PartitionID')
return lpar_resp.body
def _parse_vnc_port(std_out):
"""Parse the VNC port number out of the standard output from mkvterm.
:return: The port number parsed otherwise None if no valid port
"""
# The first line of the std_out should be the VNC port
line = std_out.splitlines()[0] if std_out else None
return int(line) if line and line.isdigit() else None
class _VNCSocketListener(threading.Thread):
"""Provides a listener bound to a remote-accessible port for VNC access.
The VNC sessions set up by mkvterm only allow access from the localhost, so
this listener provides an additional listener on a remote-accessible port
to all incoming connections for VNC sessions.
This listener may be setup by the caller in a way so that there is only a
single remote port for all VNC sessions or that there is one port per VM.
This listener will accept incoming connections, establish authentication of
the requester (if x509 authentication is enabled), and will determine what
LPAR UUID the request is for and establish connections to the local port
and setup a repeater to forward the data between the two sides.
"""
def __init__(self, adapter, remote_port, local_ip, verify_vnc_path,
remote_ips=None):
"""Creates the listener bound to a remote-accessible port.
:param adapter: The pypowervm adapter
:param remote_port: The port to bind to for remote connections.
:param local_ip: The IP address to bind the VNC server to. This would
be the IP of the management network on the system.
:param verify_vnc_path: Boolean to determine whether we verify the
vnc_path.
:param remote_ips: (Optional, Default: None) A binding to only accept
clients that are from a specific list of IP
addresses through. Default is None, and therefore
will allow any remote IP to connect.
"""
super(_VNCSocketListener, self).__init__()
self.adapter = adapter
self.remote_port = remote_port
self.local_ip = local_ip
self.verify_vnc_path = verify_vnc_path
self.remote_ips = remote_ips
self.x509_certs = None
self.alive = True
self.vnc_killer = None
def set_x509_certificates(self, ca_certs=None,
server_cert=None, server_key=None):
"""Set the x509 Certificates to use for TLS authentication.
:param ca_certs: (Optional, Default: None) Path to CA certificate to
use for verifying VNC X509 Authentication.
:param server_cert: (Optional, Default: None) Path to Server cert
to use for verifying VNC X509 Authentication.
:param server_key: (Optional, Default: None) Path to Server private key
to use for verifying VNC X509 Authentication.
"""
self.x509_certs = dict(
ca_certs=ca_certs, server_cert=server_cert, server_key=server_key)
def stop(self):
"""Stops the listener from running."""
# This will stop listening for all clients
self.alive = False
# Remove ourselves from the VNC listeners.
if self.remote_port in _VNC_REMOTE_PORT_TO_LISTENER:
del _VNC_REMOTE_PORT_TO_LISTENER[self.remote_port]
def run(self):
"""Used by the thread to run the listener."""
family = socket.AF_INET6 if ':' in self.local_ip else socket.AF_INET
server = socket.socket(family, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((self.local_ip, self.remote_port))
LOG.info(_("VNCSocket Listener Listening on ip=%(ip)s port=%(port)s") %
{'ip': self.local_ip, 'port': self.remote_port})
server.listen(10)
while self.alive:
# Listen on the server socket for incoming connections
s_inputs = select.select([server], [], [], 1)[0]
for s_input in s_inputs:
# Establish a new client connection & repeater between the two
self._new_client(s_input)
server.close()
def _new_client(self, server):
"""Listens for a new client.
:param server: The server socket.
"""
# This is the socket FROM the client side. client_addr is a tuple
# of format ('1.2.3.4', '5678') - ip and port.
client_socket, client_addr = server.accept()
LOG.debug("New Client socket accepted client_addr=%s" % client_addr[0])
# If only select IPs are allowed through, validate
if (self.remote_ips is not None and
client_addr[0] not in self.remote_ips):
# Close the connection, exit.
client_socket.close()
return
# If they gave use a VNC Path to look for in the connection string
# then we will do that now otherwise just skip over the header info
if self.verify_vnc_path:
# Check to ensure that there is output waiting.
c_input = select.select([client_socket], [], [], 1)[0]
# If no input, then just assume a close. We waited a second.
if not c_input:
# Assume HTTP 1.1. All clients should support. We have no
# input, so we don't know what protocol they would like.
client_socket.sendall("HTTP/1.1 400 Bad Request\r\n\r\n")
client_socket.close()
return
# We know we had data waiting. Receive (at max) the vnc_path
# string. All data after this validation string is the
# actual VNC data.
lpar_uuid, http_code = self._check_http_connect(client_socket)
if lpar_uuid:
# Send back the success message.
client_socket.sendall("HTTP/%s 200 OK\r\n\r\n" % http_code)
else:
# Was not a success, exit.
client_socket.sendall("HTTP/%s 400 Bad Request\r\n\r\n" %
http_code)
client_socket.close()
return
# If we had no VNC Path to match against, then the local port is
# going to be the same as the remote port and we need to figure
# out what the LPAR UUID is for that given local port VNC session
else:
lpar_uuid = next(k for k, v in _VNC_UUID_TO_LOCAL_PORT.items()
if v == self.remote_port)
# Setup the forwarding socket to the local LinuxVNC session
self._setup_forwarding_socket(lpar_uuid, client_socket)
def _setup_forwarding_socket(self, lpar_uuid, client_socket):
"""Setup the forwarding socket to the local LinuxVNC session.
:param lpar_uuid: The UUID of the lpar for which we are forwarding.
:param client_socket: The client-side socket to receive data from.
"""
local_port = _VNC_UUID_TO_LOCAL_PORT.get(lpar_uuid)
# If for some reason no mapping to a local port, then give up
if local_port is None:
client_socket.close()
# Get the forwarder. This will be the socket we read FROM the
# localhost. When this receives data, it will be sent to the client
# socket.
fwd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
fwd.connect(('127.0.0.1', local_port))
# If we were told to enable VeNCrypt using X509 Authentication, do so
if self.x509_certs is not None:
ssl_socket = self._enable_x509_authentication(client_socket, fwd)
# If there was an error enabling SSL, then close the sockets
if ssl_socket is None:
client_socket.close()
fwd.close()
return
client_socket = ssl_socket
# See if we need to start up a new repeater for the given local port
if local_port not in _VNC_LOCAL_PORT_TO_REPEATER:
_VNC_LOCAL_PORT_TO_REPEATER[local_port] = _VNCRepeaterServer(
self.adapter, lpar_uuid, local_port, client_socket, fwd)
_VNC_LOCAL_PORT_TO_REPEATER[local_port].start()
else:
repeater = _VNC_LOCAL_PORT_TO_REPEATER[local_port]
repeater.add_socket_connection_pair(client_socket, fwd)
def _enable_x509_authentication(self, client_socket, server_socket):
"""Enables and Handshakes VeNCrypt using X509 Authentication.
:param client_socket: The client-side socket to receive data from.
:param server_socket: The server-side socket to forward data to.
:return ssl_socket: A client-side socket wrappered for SSL or None
if there is an error.
"""
try:
# First perform the RFB Version negotiation between client/server
self._version_negotiation(client_socket, server_socket)
# Next perform the Security Authentication Type Negotiation
if not self._auth_type_negotiation(client_socket):
return None
# Next perform the Security Authentication SubType Negotiation
if not self._auth_subtype_negotiation(client_socket):
return None
# Now that the VeNCrypt handshake is done, do the SSL wrapper
ca_certs = self.x509_certs.get('ca_certs')
server_key = self.x509_certs.get('server_key')
server_cert = self.x509_certs.get('server_cert')
return ssl.wrap_socket(
client_socket, server_side=True, ca_certs=ca_certs,
certfile=server_cert, keyfile=server_key,
ssl_version=ssl.PROTOCOL_TLSv1_2, cert_reqs=ssl.CERT_REQUIRED)
# If we got an error, log and handle to not take down the thread
except Exception as exc:
LOG.warning(_("Error negotiating SSL for VNC Repeater: %s") % exc)
LOG.exception(exc)
return None
def _version_negotiation(self, client_socket, server_socket):
"""Performs the RFB Version negotiation between client/server.
:param client_socket: The client-side socket to receive data from.
:param server_socket: The server-side socket to forward data to.
"""
# Do a pass-thru of the RFB Version negotiation up-front
# The length of the version is 12, such as 'RFB 003.007\n'
client_socket.sendall(self._socket_receive(server_socket, 12))
server_socket.sendall(self._socket_receive(client_socket, 12))
# Since we are doing our own additional authentication
# just tell the server we are doing No Authentication (1) to it
auth_size = self._socket_receive(server_socket, 1)
self._socket_receive(server_socket, six.byte2int(auth_size))
server_socket.sendall(six.int2byte(1))
def _auth_type_negotiation(self, client_socket):
"""Performs the VeNCrypt Authentication Type Negotiation.
:param client_socket: The client-side socket to receive data from.
:return success: Boolean whether the handshake was successful.
"""
# Do the VeNCrypt handshake next before establishing SSL
# Say we only support VeNCrypt (19) authentication version 0.2
client_socket.sendall(six.int2byte(1))
client_socket.sendall(six.int2byte(19))
client_socket.sendall("\x00\x02")
authtype = self._socket_receive(client_socket, 1)
# Make sure the Client supports the VeNCrypt (19) authentication
if len(authtype) < 1 or six.byte2int(authtype) != 19:
# Send a 1 telling the client the type wasn't accepted
client_socket.sendall(six.int2byte(1))
return False
vers = self._socket_receive(client_socket, 2)
# Make sure the Client supports at least version 0.2 of it
if ((len(vers) < 2 or six.byte2int(vers) != 0
or six.byte2int(vers[1:]) < 2)):
# Send a 1 telling the client the type wasn't accepted
client_socket.sendall(six.int2byte(1))
return False
# Tell the Client we have accepted the authentication type
# In this particular case 0 means the type was accepted
client_socket.sendall(six.int2byte(0))
return True
def _auth_subtype_negotiation(self, client_socket):
"""Performs the x509None Authentication Sub-Type Negotiation.
:param client_socket: The client-side socket to receive data from.
:return success: Boolean whether the handshake was successful.
"""
# Tell the client the authentication sub-type is x509None (260)
client_socket.sendall(six.int2byte(1))
client_socket.sendall(struct.pack('!I', 260))
subtyp_raw = self._socket_receive(client_socket, 4)
# Make sure that the client also supports sub-type x509None (260)
if 260 not in struct.unpack('!I', subtyp_raw):
# Send a 0 telling the client the sub-type wasn't accepted
client_socket.sendall(six.int2byte(0))
return False
# Tell the Client we have accepted the authentication handshake
# In this particular case 1 means the sub-type was accepted
client_socket.sendall(six.int2byte(1))
return True
def _socket_receive(self, asocket, bufsize):
"""Helper method to add a timeout on each receive call.
This method will raise a timeout exception if it takes > 30 seconds.
:param asocket: The socket to do the receive on.
:param bufsize: The number of bytes to receive.
:return data: The data returned from the socket receive.
"""
# Add a 30 second timeout around the receive so that we don't
# block forever if for some reason it never received the packet
if not select.select([asocket], [], [], 30)[0]:
raise socket.timeout('30 second timeout on handshake receive')
return asocket.recv(bufsize)
def _check_http_connect(self, client_socket):
"""Parse the HTTP connect string to find the LPAR UUID.
:param client_socket: The client socket sending the data.
:returns lpar_uuid: The LPAR UUID parsed from the connect string.
:returns http_code: The HTTP Connection code used for the client
connection.
"""
# Get the expected header.
# We don't know how large the identifier will be, so use 500 as max.
# If the identifier is less than 500, it will not return as many bytes.
header_len = len("CONNECT %s HTTP/1.1\r\n\r\n" % ('x' * 500))
value = client_socket.recv(header_len)
# Find the HTTP Code (if you can...)
pat = r'^CONNECT\s+(\S+)\s+HTTP/(.*)\r\n\r\n$'
res = re.match(pat, value)
vnc_path = res.groups()[0] if res else None
http_code = res.groups()[1] if res else '1.1'
return _VNC_PATH_TO_UUID.get(vnc_path), http_code
class _VNCRepeaterServer(threading.Thread):
"""Repeats a VNC connection from localhost to a given client.
This class is separated out from the Socket Listener so that there can
be one thread doing the actual repeating/forwarded of the data for the
VNC sessions for a single LPAR. Otherwise if there are sessions to a lot
of LPAR's with sessions, one overall thread might get overloaded.
This class will be provided a pair of peer socket connections and will
listen for data from each of them and forward to the other until the
connection on one side goes down in which it will close the connection
to the other side.
Also, if no connections are open for a given local port VNC session,
after a 5 minute window it will run rmvterm to close the terminal console
to clean up sessions that are no longer being used.
"""
def __init__(self, adapter, lpar_uuid, local_port, client_socket=None,
local_socket=None):
"""Creates the repeater.
:param adapter: The pypowervm adapter
:param lpar_uuid: Partition UUID.
:param local_port: The local port bound to by the VNC session.
:param client_socket: (Optional, Default: None) The socket descriptor
of the incoming client connection.
:param local_socket: (Optional, Default: None) The socket descriptor of
the VNC session connection forwarding data to.
"""
super(_VNCRepeaterServer, self).__init__()
self.peers = dict()
self.adapter = adapter
self.lpar_uuid = lpar_uuid
self.local_port = local_port
self.alive = True
self.vnc_killer = None
# Add the connection passed into us to the forwarding list
if client_socket is not None and local_socket is not None:
self.add_socket_connection_pair(client_socket, local_socket)
def stop(self):
"""Stops the repeater from running."""
# This will stop listening for all clients
self.alive = False
# Remove ourselves from the VNC listeners.
if self.local_port in _VNC_LOCAL_PORT_TO_REPEATER:
del _VNC_LOCAL_PORT_TO_REPEATER[self.local_port]
def run(self):
"""Used by the thread to run the repeater."""
while self.alive:
# Do a select to wait for data on each of the socket connections
input_list = list(self.peers)
s_inputs = select.select(input_list, [], [], 1)[0]
for s_input in s_inputs:
# At this point, we need to read the data. We know that data
# is ready. However, if that data that is ready is length
# 0, then we know that we're ready to close this.
data = s_input.recv(4096)
if len(data) == 0:
self._close_client(s_input)
# Note that we have to break here. We do that because the
# peer dictionary has changed with the close. So the list
# to iterate over should be re-evaluated.
# The remaining inputs will just be picked up on the next
# pass, so nothing to worry about.
break
# Just process the data.
self.peers[s_input].send(data)
# At this point, force a close on all remaining inputs.
for input_socket in self.peers:
input_socket.close()
def add_socket_connection_pair(self, client_socket, local_socket):
"""Adds the pair of socket connections to the list to forward data for.
:param client_socket: The client-side incoming socket.
:param local_socket: The local socket for the VNC session.
"""
self.peers[local_socket] = client_socket
self.peers[client_socket] = local_socket
# If for some reason the VNC was being killed, abort it
if self.vnc_killer is not None:
self.vnc_killer.abort()
self.vnc_killer = None
def _close_client(self, s_input):
"""Closes down a client.
:param s_input: The socket that has received a close.
"""
# Close the sockets
peer = self.peers[s_input]
peer.close()
s_input.close()
# And remove from the peer list, so that we've removed all pointers to
# them
del self.peers[peer]
del self.peers[s_input]
# If this was the last port, close the local connection
if len(self.peers) == 0:
self.vnc_killer = _VNCKiller(self.adapter, self.lpar_uuid)
self.vnc_killer.start()
class _VNCKiller(threading.Thread):
"""The VNC Killer is a thread that will eventually close the VNC.
The VNC Repeater could run indefinitely, whether clients are connected to
it or not. This class will wait a period of time (5 minutes) and if
the abort has not been called, will fully close the vterm.
This is used in orchestration with the VNCRepeaterServer. The intention
is, if the user quickly navigates off the VNC, they can come back without
losing their whole session. But if they wait up to 5 minutes, then the
session will be closed out and the memory will be reclaimed.
"""
def __init__(self, adapter, lpar_uuid):
super(_VNCKiller, self).__init__()
self.adapter = adapter
self.lpar_uuid = lpar_uuid
self._abort = False
def abort(self):
"""Call to stop the killer from completing its job."""
self._abort = True
def run(self):
count = 0
# Wait up to 5 minutes to see if any new negotiations came in
while count < 300 and not self._abort:
time.sleep(1)
if self._abort:
break
count += 1
if not self._abort:
_close_vterm_local(self.adapter, self.lpar_uuid)
|