commit a4c2934d509cea638208e31658a3eb8f2fdc7f08 Author: Radoslav Gerganov Date: Fri Feb 10 16:06:57 2017 +0200 Initial public version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4b9f09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +.idea +.tox +venv/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4de6cc2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,229 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +======================================================================== + +This program includes a modified version of the telnetlib module from Python3 +which has the following license: + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights +Reserved" are retained in Python alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d793336 --- /dev/null +++ b/README.rst @@ -0,0 +1,24 @@ +vmware-vspc +=========== + +Virtual Serial Port Concentrator for use in the vSphere environment. It collects +serial console logs from VMs which have configured virtual serial port pointing +to it. + +Usage +===== + +Copy ``vspc.conf.sample`` as ``vspc.conf`` and edit as appropriate:: + + [DEFAULT] + debug = True + host = 0.0.0.0 + port = 13370 + cert = cert.pem + key = key.pem + uri = vmware-vspc + serial_log_dir = /opt/vmware/vspc + +Then start with:: + + $ vmware-vspc --config-file vspc.conf \ No newline at end of file diff --git a/etc/vspc.conf.sample b/etc/vspc.conf.sample new file mode 100644 index 0000000..e68fea1 --- /dev/null +++ b/etc/vspc.conf.sample @@ -0,0 +1,9 @@ +[DEFAULT] +debug = True +host = 0.0.0.0 +port = 13370 +cert = cert.pem +key = key.pem +uri = vmware-vspc +serial_log_dir = /opt/vmware/vspc + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..aada6b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +oslo.config>=3.7.0 # Apache-2.0 +oslo.log>=1.14.0 # Apache-2.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..48592ef --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup + +with open('README.rst') as f: + readme = f.read() + + +with open('LICENSE') as f: + license = f.read() + + +setup( + name='vmware-vspc', + version='0.0.1', + description='Virtual Serial Port Concentrator', + long_description=readme, + author='VMware Inc.', + author_email='rgerganov@vmware.com', + url='https://github.com/rgerganov/vmware-vspc', + license=license, + entry_points = { + 'console_scripts': ['vmware-vspc=vspc.server:main'], + }, + packages=['vspc'], + install_requires=['oslo.config', 'oslo.log'] +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..c59a0e9 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +flake8 + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c1c17b8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py34,pep8 +skipsdist = True + +[testenv] +deps = -rrequirements.txt + -rtest-requirements.txt +#commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:pep8] +basepython = /usr/bin/python3 +commands = flake8 + +[flake8] +ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405 +exclude = venv,.venv,.git,.tox,dist,doc,*lib/python*,*egg,build diff --git a/vspc/__init__.py b/vspc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vspc/async_telnet.py b/vspc/async_telnet.py new file mode 100644 index 0000000..deb40db --- /dev/null +++ b/vspc/async_telnet.py @@ -0,0 +1,145 @@ +# Copyright (c) 2001-2016 Python Software Foundation +# Copyright (c) 2017 VMware Inc. +# 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. +""" +Telnet client class using asyncio. +Based on the standard telnetlib module from Python3. +""" +import asyncio + +# Telnet protocol characters (don't change) +IAC = bytes([255]) # "Interpret As Command" +DONT = bytes([254]) +DO = bytes([253]) +WONT = bytes([252]) +WILL = bytes([251]) +theNULL = bytes([0]) +SE = bytes([240]) # Subnegotiation End +NOP = bytes([241]) # No Operation +SB = bytes([250]) # Subnegotiation Begin +NOOPT = bytes([0]) + + +class AsyncTelnet: + def __init__(self, reader, opt_handler): + self._reader = reader + self._opt_handler = opt_handler + self.rawq = b'' + self.irawq = 0 + self.cookedq = b'' + self.eof = 0 + self.iacseq = b'' # Buffer for IAC sequence. + self.sb = 0 # flag for SB and SE sequence. + self.sbdataq = b'' + + @asyncio.coroutine + def process_rawq(self): + """Transfer from raw queue to cooked queue. + + Set self.eof when connection is closed. + """ + buf = [b'', b''] + try: + while self.rawq: + c = yield from self.rawq_getchar() + if not self.iacseq: + if self.sb == 0 and c == theNULL: + continue + if self.sb == 0 and c == b"\021": + continue + if c != IAC: + buf[self.sb] = buf[self.sb] + c + continue + else: + self.iacseq += c + elif len(self.iacseq) == 1: + # 'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]' + if c in (DO, DONT, WILL, WONT): + self.iacseq += c + continue + + self.iacseq = b'' + if c == IAC: + buf[self.sb] = buf[self.sb] + c + else: + if c == SB: # SB ... SE start. + self.sb = 1 + self.sbdataq = b'' + elif c == SE: + self.sb = 0 + self.sbdataq = self.sbdataq + buf[1] + buf[1] = b'' + yield from self._opt_handler(c, NOOPT, + data=self.sbdataq) + elif len(self.iacseq) == 2: + cmd = self.iacseq[1:2] + self.iacseq = b'' + opt = c + if cmd in (DO, DONT): + yield from self._opt_handler(cmd, opt) + elif cmd in (WILL, WONT): + yield from self._opt_handler(cmd, opt) + except EOFError: # raised by self.rawq_getchar() + self.iacseq = b'' # Reset on EOF + self.sb = 0 + pass + self.cookedq = self.cookedq + buf[0] + self.sbdataq = self.sbdataq + buf[1] + + @asyncio.coroutine + def rawq_getchar(self): + """Get next char from raw queue. + + Raise EOFError when connection is closed. + """ + if not self.rawq: + yield from self.fill_rawq() + if self.eof: + raise EOFError + c = self.rawq[self.irawq:self.irawq + 1] + self.irawq = self.irawq + 1 + if self.irawq >= len(self.rawq): + self.rawq = b'' + self.irawq = 0 + return c + + @asyncio.coroutine + def fill_rawq(self): + """Fill raw queue from exactly one recv() system call. + + Set self.eof when connection is closed. + """ + if self.irawq >= len(self.rawq): + self.rawq = b'' + self.irawq = 0 + # The buffer size should be fairly small so as to avoid quadratic + # behavior in process_rawq() above + buf = yield from self._reader.read(50) + self.eof = (not buf) + self.rawq = self.rawq + buf + + @asyncio.coroutine + def read_some(self): + """Read at least one byte of cooked data unless EOF is hit. + + Return b'' if EOF is hit. + """ + yield from self.process_rawq() + while not self.cookedq and not self.eof: + yield from self.fill_rawq() + yield from self.process_rawq() + buf = self.cookedq + self.cookedq = b'' + return buf diff --git a/vspc/server.py b/vspc/server.py new file mode 100755 index 0000000..b72d587 --- /dev/null +++ b/vspc/server.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 VMware Inc. +# 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. + +import asyncio +import functools +import os +import ssl +import sys + +from oslo_config import cfg +from oslo_log import log as logging + +from vspc import async_telnet +from vspc.async_telnet import IAC, SB, SE, DO, DONT, WILL, WONT + +opts = [ + cfg.StrOpt('host', + default='0.0.0.0', + help='Host on which to listen for incoming requests'), + cfg.IntOpt('port', + default=13370, + help='Port on which to listen for incoming requests'), + cfg.StrOpt('cert', help='SSL certificate file'), + cfg.StrOpt('key', help='SSL key file (if separate from cert)'), + cfg.StrOpt('uri', help='VSPC URI'), + cfg.StrOpt('serial_log_dir', help='The directory where serial logs are ' + 'saved'), + ] + +CONF = cfg.CONF +CONF.register_opts(opts) + +LOG = logging.getLogger(__name__) + +BINARY = bytes([0]) # 8-bit data path +SGA = bytes([3]) # suppress go ahead +VMWARE_EXT = bytes([232]) + +KNOWN_SUBOPTIONS_1 = bytes([0]) +KNOWN_SUBOPTIONS_2 = bytes([1]) +VMOTION_BEGIN = bytes([40]) +VMOTION_GOAHEAD = bytes([41]) +VMOTION_NOTNOW = bytes([43]) +VMOTION_PEER = bytes([44]) +VMOTION_PEER_OK = bytes([45]) +VMOTION_COMPLETE = bytes([46]) +VMOTION_ABORT = bytes([48]) +VM_VC_UUID = bytes([80]) +GET_VM_VC_UUID = bytes([81]) +VM_NAME = bytes([82]) +GET_VM_NAME = bytes([83]) +DO_PROXY = bytes([70]) +WILL_PROXY = bytes([71]) +WONT_PROXY = bytes([73]) + +SUPPORTED_OPTS = (KNOWN_SUBOPTIONS_1 + KNOWN_SUBOPTIONS_2 + VMOTION_BEGIN + + VMOTION_GOAHEAD + VMOTION_NOTNOW + VMOTION_PEER + + VMOTION_PEER_OK + VMOTION_COMPLETE + VMOTION_ABORT + + VM_VC_UUID + GET_VM_VC_UUID + VM_NAME + GET_VM_NAME + + DO_PROXY + WILL_PROXY + WONT_PROXY) + + +class VspcServer(object): + def __init__(self): + self.sock_to_uuid = dict() + + @asyncio.coroutine + def handle_known_suboptions(self, writer, data): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.debug("<< %s KNOWN-SUBOPTIONS-1 %s", peer, data) + LOG.debug(">> %s KNOWN-SUBOPTIONS-2 %s", peer, SUPPORTED_OPTS) + writer.write(IAC + SB + VMWARE_EXT + KNOWN_SUBOPTIONS_2 + + SUPPORTED_OPTS + IAC + SE) + LOG.debug(">> %s GET-VM-VC-UUID", peer) + writer.write(IAC + SB + VMWARE_EXT + GET_VM_VC_UUID + IAC + SE) + yield from writer.drain() + + @asyncio.coroutine + def handle_do_proxy(self, writer, data): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + dir, uri = data[0], data[1:].decode('ascii') + LOG.debug("<< %s DO-PROXY %c %s", peer, dir, uri) + if chr(dir) != 'S' or uri != CONF.uri: + LOG.debug(">> %s WONT-PROXY", peer) + writer.write(IAC + SB + VMWARE_EXT + WONT_PROXY + IAC + SE) + yield from writer.drain() + writer.close() + else: + LOG.debug(">> %s WILL-PROXY", peer) + writer.write(IAC + SB + VMWARE_EXT + WILL_PROXY + IAC + SE) + yield from writer.drain() + + def handle_vm_vc_uuid(self, socket, data): + peer = socket.getpeername() + uuid = data[2:].decode('ascii') + LOG.debug("<< %s VM-VC-UUID %s", peer, uuid) + uuid = uuid.replace(' ', '') + self.sock_to_uuid[socket] = uuid + + @asyncio.coroutine + def handle_vmotion_begin(self, writer, data): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.debug("<< %s VMOTION-BEGIN %s", peer, data) + secret = os.urandom(4) + LOG.debug(">> %s VMOTION-GOAHEAD %s %s", peer, data, secret) + writer.write(IAC + SB + VMWARE_EXT + VMOTION_GOAHEAD + + data + secret + IAC + SE) + yield from writer.drain() + + @asyncio.coroutine + def handle_vmotion_peer(self, writer, data): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.debug("<< %s VMOTION-PEER %s", peer, data) + LOG.debug("<< %s VMOTION-PEER-OK %s", peer, data) + writer.write(IAC + SB + VMWARE_EXT + VMOTION_PEER_OK + data + IAC + SE) + yield from writer.drain() + + def handle_vmotion_complete(self, socket, data): + peer = socket.getpeername() + LOG.debug("<< %s VMOTION-COMPLETE %s", peer, data) + + @asyncio.coroutine + def handle_do(self, writer, opt): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.debug("<< %s DO %s", peer, opt) + if opt in (BINARY, SGA): + LOG.debug(">> %s WILL", peer) + writer.write(IAC + WILL + opt) + yield from writer.drain() + else: + LOG.debug(">> %s WONT", peer) + writer.write(IAC + WONT + opt) + yield from writer.drain() + + @asyncio.coroutine + def handle_will(self, writer, opt): + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.debug("<< %s WILL %s", peer, opt) + if opt in (BINARY, SGA, VMWARE_EXT): + LOG.debug(">> %s DO", peer) + writer.write(IAC + DO + opt) + yield from writer.drain() + else: + LOG.debug(">> %s DONT", peer) + writer.write(IAC + DONT + opt) + yield from writer.drain() + + @asyncio.coroutine + def option_handler(self, cmd, opt, writer, data=None): + socket = writer.get_extra_info('socket') + if cmd == SE and data[0:1] == VMWARE_EXT: + vmw_cmd = data[1:2] + if vmw_cmd == KNOWN_SUBOPTIONS_1: + yield from self.handle_known_suboptions(writer, data[2:]) + elif vmw_cmd == DO_PROXY: + yield from self.handle_do_proxy(writer, data[2:]) + elif vmw_cmd == VM_VC_UUID: + self.handle_vm_vc_uuid(socket, data[2:]) + elif vmw_cmd == VMOTION_BEGIN: + yield from self.handle_vmotion_begin(writer, data[2:]) + elif vmw_cmd == VMOTION_PEER: + yield from self.handle_vmotion_peer(writer, data[2:]) + elif vmw_cmd == VMOTION_COMPLETE: + self.handle_vmotion_complete(socket, data[2:]) + else: + LOG.error("Unknown VMware cmd: %s %s", vmw_cmd, data[2:]) + writer.close() + elif cmd == DO: + yield from self.handle_do(writer, opt) + elif cmd == WILL: + yield from self.handle_will(writer, opt) + + def save_to_log(self, uuid, data): + fpath = os.path.join(CONF.serial_log_dir, uuid) + with open(fpath, 'ab') as f: + f.write(data) + + @asyncio.coroutine + def handle_telnet(self, reader, writer): + opt_handler = functools.partial(self.option_handler, writer=writer) + telnet = async_telnet.AsyncTelnet(reader, opt_handler) + socket = writer.get_extra_info('socket') + peer = socket.getpeername() + LOG.info("%s connected", peer) + data = yield from telnet.read_some() + uuid = self.sock_to_uuid.get(socket) + if uuid is None: + LOG.error("%s didn't present UUID", peer) + writer.close() + return + try: + while data: + self.save_to_log(uuid, data) + data = yield from telnet.read_some() + finally: + self.sock_to_uuid.pop(socket, None) + LOG.info("%s disconnected", peer) + writer.close() + + def start(self): + loop = asyncio.get_event_loop() + ssl_context = None + if CONF.cert: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_cert_chain(certfile=CONF.cert, keyfile=CONF.key) + coro = asyncio.start_server(self.handle_telnet, + CONF.host, + CONF.port, + ssl=ssl_context, + loop=loop) + server = loop.run_until_complete(coro) + + # Serve requests until Ctrl+C is pressed + LOG.info("Serving on %s", server.sockets[0].getsockname()) + LOG.info("Log directory: %s", CONF.serial_log_dir) + try: + loop.run_forever() + except KeyboardInterrupt: + pass + + # Close the server + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() + + +def main(): + logging.register_options(CONF) + CONF(sys.argv[1:], prog='vspc') + logging.setup(CONF, "vspc") + if not CONF.serial_log_dir: + LOG.error("serial_log_dir is not specified") + sys.exit(1) + if not os.path.exists(CONF.serial_log_dir): + LOG.info("Creating log directory: %s", CONF.serial_log_dir) + os.makedirs(CONF.serial_log_dir) + srv = VspcServer() + srv.start() + + +if __name__ == '__main__': + main()