--- vSPC.py 2014-11-21 15:22:49.000000000 -0500 +++ in-vSPC.py 2015-01-26 13:03:29.000000000 -0500 @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/local/bin/python # Copyright 2011 Isilon Systems LLC. All rights reserved. # @@ -29,6 +29,12 @@ # authors and should not be interpreted as representing official policies, either expressed # or implied, of . +# ---- +# 2014: P. Kern < pkern (at) eis.utoronto.ca > +# + inetd server mode (with SSL). +# + logging serial output to file. +# ---- + """ vSPC.py - A Virtual Serial Port Concentrator for VMware @@ -36,16 +42,29 @@ This server is based on publicly available documentation: http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/vsp41_usingproxy_virtual_serial_ports.pdf + +See also + http://code.activestate.com/recipes/114642/ """ from __future__ import with_statement __author__ = "Zachary M. Loafman" __copyright__ = "Copyright (C) 2011 Isilon Systems LLC." -__revision__ = "$Id$" +__revision__ = "$Header: /local/lib/vulture/RCS/in-vSPC.py,v 1.47 2015/01/26 18:03:27 pkern Exp $" BASENAME='vSPC.py' +WORKHOME = None +SVCIDENT = None +CONFIG = None +LISTEN_HOST = "127.0.0.1" +inetd_mode = False + +inetd_mode_ssl = False +SSLCERTFILE = None +SSLPRIVKEY = None + import logging import os import pickle @@ -60,6 +79,15 @@ from telnetlib import * from telnetlib import IAC,DO,DONT,WILL,WONT,BINARY,ECHO,SGA,SB,SE,NOOPT,theNULL +from urlparse import urlparse +import trace + +import cgi +import base64 +import signal + +import ssl + # Default for --proxy-port, the port to send VMs to. PROXY_PORT = 13370 @@ -75,6 +103,8 @@ # port number / listener open with no VMware or client connections VM_EXPIRE_TIME = 24*3600 +# + # Query protocol Q_VERS = 1 Q_NAME = 'name' @@ -248,11 +278,11 @@ self.send_buffer = '' for opt in self.server_opts: - logging.debug("sending WILL %d" % ord(opt)) + logging.debug("sv_ops: sending WILL %d" % ord(opt)) self._send_cmd(WILL + opt) self.unacked.append((WILL, opt)) for opt in self.client_opts: - logging.debug("sending DO %d" % ord(opt)) + logging.debug("cl_ops: sending DO %d" % ord(opt)) self._send_cmd(DO + opt) self.unacked.append((DO, opt)) @@ -262,7 +292,7 @@ def _option_callback(self, sock, cmd, opt): if cmd in (DO, DONT): if opt not in self.server_opts: - logging.debug("client wants us to %d, sending WONT" % ord(opt)) + logging.debug("sv_ops: remote says we should %d, sending WONT" % ord(opt)) self._send_cmd(WONT + opt) return @@ -273,20 +303,20 @@ self.unacked.remove((WILL, opt)) if cmd == DONT: - logging.debug("client doesn't want us to %d" % ord(opt)) + logging.debug("remote says we should not %d." % ord(opt)) try: self.server_opts_accepted.remove(opt) except ValueError: pass else: - logging.debug("client says we should %d" % ord(opt)) + logging.debug("remote says we should %d." % ord(opt)) if not msg_is_reply: # Remind client that we want this option self._send_cmd(WILL + opt) elif cmd in (WILL, WONT): if opt not in self.client_opts: - logging.debug("client wants to %d, sending DONT" % ord(opt)) + logging.debug("cl_ops: remote wants to %d, sending DONT" % ord(opt)) self._send_cmd(DONT + opt) return @@ -297,13 +327,13 @@ self.unacked.remove((DO, opt)) if cmd == WONT: - logging.debug("client won't %d" % ord(opt)) + logging.debug("remote won't %d" % ord(opt)) try: self.client_opts_accepted.remove(opt) except ValueError: pass else: - logging.debug("client will %d" % ord(opt)) + logging.debug("remote will %d" % ord(opt)) if not msg_is_reply: # Remind client that we want this option @@ -311,7 +341,7 @@ elif cmd == SB: pass # Don't log this, caller is processing else: - logging.debug("cmd %d %s" % (ord(cmd), opt)) + logging.debug("cmd %d %s" % (ord(cmd), repr(opt))) def process_available(self): """Process all data, but don't take anything off the cooked queue. @@ -327,10 +357,10 @@ if self.unacked: desc = map(lambda (x,y): (ord(x), ord(y)), self.unacked) if time.time() > self.last_ack + UNACK_TIMEOUT: - logging.debug("timeout waiting for commands %s" % desc) + logging.debug("neg_done: timeout waiting for commands %s" % desc) self.unacked = [] else: - logging.debug("still waiting for %s" % desc) + logging.debug("neg_done: still waiting for %s" % desc) return not self.unacked @@ -340,6 +370,7 @@ return self.read_very_lazy() def send_buffered(self, s = ''): + # logging.debug('TS send_bufd [%s]' % repr(s)) self.send_buffer += s nbytes = self.sock.send(self.send_buffer) self.send_buffer = self.send_buffer[nbytes:] @@ -373,6 +404,7 @@ self.handler = handler or VMExtHandler() self.name = None self.uuid = None + self.proxy = None def _send_vmware(self, s): self.sock.sendall(IAC + SB + VMWARE_EXT + s + IAC + SE) @@ -386,18 +418,62 @@ def _handle_do_proxy(self, data): dir = 'client' if data[:1] == "C" else 'server' uri = data[1:] - logging.debug("client wants to proxy %s to %s" % (dir, uri)) + ans = WONT_PROXY if dir == 'server' and uri == BASENAME: - self._send_vmware(WILL_PROXY) - else: - self._send_vmware(WONT_PROXY) + ans = WILL_PROXY + elif dir == 'client': + svclen = 0 + if SVCIDENT is not None: + svclen = len(SVCIDENT) + if svclen > 0 and uri.startswith( SVCIDENT, 0, svclen ): + ##### + # log serial I/O to file. + ## + # make sure options in URI string begin/end with ":" + svcopts = "" + if len(uri) > svclen: + svcopts = uri[svclen:] + uri = uri[:svclen] + if not svcopts.endswith(":"): + svcopts += ":" + if not svcopts.startswith(":"): + svcopts = ":" + svcopts + uri += svcopts + ## + # logfile = / %Y%m%d_%H%M,{remoteIP} + hn, pn = self.sock.getpeername() + uri += time.strftime("/%Y%m%d_%H%M%S,") + hn + logging.debug("uri = %s" % repr(uri)) + ans = WILL_PROXY + self.proxy = uri + else: + ou = urlparse(uri) + # d = ou._asdict() + # for k,v in d.items(): + # logging.debug("d uri %s : %s" % (k, repr(v))) + logging.debug("v uri h %s p %s f %s" % ( ou.hostname, ou.port, repr(ou.path))) + ans = WILL_PROXY + self.proxy = uri + # endif + # endif + + logging.debug("client wants to proxy %s to %s. we %s." % (dir, repr(uri), EXT_SUPPORTED[ans])) + self._send_vmware(ans) def _handle_vmotion_begin(self, data): - cookie = data + struct.pack("i", hash(self) & 0xFFFFFFFF) + cookie = data + struct.pack("i", hash(self) & 0x7FFFFFFF) if self.handler.handle_vmotion_begin(self, cookie): logging.debug("vMotion initiated: %s" % hexdump(cookie)) self._send_vmware(VMOTION_GOAHEAD + cookie) + if inetd_mode: + # + # exit now. next ESX host should make fresh connection. + # + logging.debug("vMotion begin: inetd_mode: exit now to make way for next connection.") + # + #TO DO: notify connected users (if any) to reconnect. + sys.exit(0) else: logging.debug("vMotion denied: %s" % hexdump(cookie)) self._send_vmware(VMOTION_NOTNOW + cookie) @@ -461,6 +537,7 @@ handled = False if EXT_SUPPORTED.has_key(subcmd): meth = '_handle_%s' % EXT_SUPPORTED[subcmd] + logging.debug('_opt_callback: meth = %s' % meth) if hasattr(self, meth): getattr(self, meth)(data) handled = True @@ -472,12 +549,35 @@ % (ord(subcmd), hexdump(data))) self._send_vmware(UNKNOWN_SUBOPTION_RCVD_2 + subcmd) -def openport(port): +def in_open(fd): + in_sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) + hn, pn = in_sock.getpeername() + logging.debug('in_open fd %d. host %s. port %s.' % (fd, hn, pn)) + if inetd_mode_ssl: + logging.debug('in_open + ssl') + ##### + # XXX - hack from ... + # http://stackoverflow.com/questions/16739293/python-cannot-rebuild-ssl-socket-after-putting-in-multiprocessing-queue + in_sock = socket.socket(_sock=in_sock) + ##### + sslsock = ssl.wrap_socket(in_sock, server_side = True, + keyfile = SSLPRIVKEY, certfile = SSLCERTFILE) + in_sock = sslsock + # + return in_sock + +def in_connect(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect( (host, port) ) + logging.debug('in_connect: host %s. port %s.' % (host, port)) + return sock + +def listenport(port, backlog = LISTEN_BACKLOG, host = ""): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setblocking(0) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); - sock.bind(("", port)) - sock.listen(LISTEN_BACKLOG) + sock.bind((host, port)) + sock.listen(backlog) return sock class Selector: @@ -487,18 +587,22 @@ def add_reader(self, stream, func): self.read_handlers[stream] = func + logging.debug('add reader %s' % ( func.__name__ )) def del_reader(self, stream): try: + # logging.debug('del reader %s' % repr(stream)) del self.read_handlers[stream] except KeyError: pass def add_writer(self, stream, func): self.write_handlers[stream] = func + logging.debug('add writer %s' % ( func.__name__ )) def del_writer(self, stream): try: + # logging.debug('del writer %s' % repr(stream)) del self.write_handlers[stream] except KeyError: pass @@ -737,6 +841,78 @@ TelnetServer.__init__(self, sock, server_opts, client_opts) self.uuid = None + class logRaw: + def __init__(self, fildes = None): + self.uuid = None + self.fildes = fildes + + def send_buffered(self, s = ''): + # logging.debug('vSPC logRaw v_send_bufd [%s]' % (repr(s))) + self.fildes.write(s) + self.fildes.flush() + return False + + def fileno(self): + return self.fildes.fileno() + + def negotiation_done(self): + return True + + def read_very_lazy(self): + return None + + class logHTML: + def __init__(self, fildes = None): + self.uuid = None + self.fildes = fildes + + def send_buffered(self, s = ''): + now = time.strftime("%Y-%m-%d_%a_%H:%M:%S") + span_new = '' % ( len(s) ) + span_end = '' + span_save = cgi.escape(s.replace("\r", "")) + + # logging.debug('vSPC logHTML v_send_bufd [%s]' % (repr(span_save))) + self.fildes.write( span_new + span_save + span_end ) + self.fildes.flush() + return False + + def fileno(self): + return self.fildes.fileno() + + def negotiation_done(self): + return True + + def read_very_lazy(self): + return None + + class logCSV: + def __init__(self, fildes = None): + self.uuid = None + self.fildes = fildes + + def send_buffered(self, s = ''): + csvln = time.strftime('%Y-%m-%d,%a,%H:%M:%S%z') + csvln += ",%d," % len(s) + csvln += base64.b64encode(s) + "\n" + + # logging.debug('vSPC logCSV v_send_bufd [%s]' % (repr(s))) + self.fildes.write( csvln ) + + self.fildes.flush() + return False + + def fileno(self): + return self.fildes.fileno() + + def negotiation_done(self): + return True + + def read_very_lazy(self): + return None + + def __init__(self, proxy_port, admin_port, vm_port_start, vm_expire_time, backend): Selector.__init__(self) @@ -752,21 +928,30 @@ self.ports = {} self.vmotions = {} - def send_buffered(self, ts, s = ''): + def v_send_buffered(self, ts, s = ''): + # logging.debug('vSPC v_send_bufd [%s]' % (repr(s))) if ts.send_buffered(s): - self.add_writer(ts, self.send_buffered) + self.add_writer(ts, self.v_send_buffered) else: self.del_writer(ts) + + def inetd_new_vm_conn(self, sock): + vt = VMTelnetServer(sock, handler = self) + self.add_reader(vt, self.new_vm_data) + + def new_vm_connection(self, listener): sock = listener.accept()[0] - sock.setblocking(0) sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) vt = VMTelnetServer(sock, handler = self) self.add_reader(vt, self.new_vm_data) + def new_client_connection(self, vm): - sock = vm.listener.accept()[0] + sock, addr = vm.listener.accept() + logging.debug('inetd new client conn %s' % (repr(addr))) + sock.setblocking(0) sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) @@ -796,10 +981,12 @@ self.abort_vm_connection(vt) if not neg_done: + logging.debug('new_vm_data: no neg') return # Queue VM data during vmotion if vt.uuid and self.vms[vt.uuid].vmotion: + logging.debug('new_vm_data: vmotion. waiting.') return s = None @@ -809,17 +996,19 @@ self.abort_vm_connection(vt) if not s: # May only be option data, or exception + logging.debug('new_vm_data: no s.') return if not vt.uuid or not self.vms.has_key(vt.uuid): # In limbo, no one can hear you scream + logging.debug('new_vm_data: limbo.') return # logging.debug('new_vm_data %s: %s' % (vt.uuid, repr(s))) for cl in self.vms[vt.uuid].clients: try: - self.send_buffered(cl, s) + self.v_send_buffered(cl, s) except (EOFError, IOError, socket.error), e: logging.debug('cl.socket send error: %s' % (str(e))) @@ -838,10 +1027,12 @@ self.abort_client_connection(client) if not neg_done: + logging.debug('new_client_data: no neg') return # Queue VM data during vmotion if self.vms[client.uuid].vmotion: + logging.debug('new_client_data: vmotion.') return s = None @@ -851,28 +1042,37 @@ self.abort_client_connection(client) if not s: # May only be option data, or exception + logging.debug('new_client_data: no s.') return # logging.debug('new_client_data %s: %s' % (client.uuid, repr(s))) for vt in self.vms[client.uuid].vts: try: - self.send_buffered(vt, s) + self.v_send_buffered(vt, s) except (EOFError, IOError, socket.error), e: logging.debug('cl.socket send error: %s' % (str(e))) def new_vm(self, uuid, name, port = None, vts = None): vm = self.Vm(uuid = uuid, name = name, vts = vts) - self.open_vm_port(vm, port) - self.vms[uuid] = vm - - # Only notify if we generated the port - if not port: - self.backend.notify_vm(vm.uuid, vm.name, vm.port) + logging.debug("new_vm: pid %s name %s port %s" % (os.getpid(), repr(name), repr(port))) - logging.debug('%s:%s listening on port %d' % - (vm.uuid, repr(vm.name), vm.port)) + if SVCIDENT is not None: + self.open_vm_uri(vm, name, port) + self.vms[uuid] = vm + else: + self.open_vm_port(vm, port) + + self.vms[uuid] = vm + + # Only notify if we generated the port + if not port: + self.backend.notify_vm(vm.uuid, vm.name, vm.port) + + logging.debug('%s:%s listening on port %d' % + (vm.uuid, repr(vm.name), vm.port)) + # # The clock is always ticking self.stamp_orphan(vm) @@ -883,7 +1083,11 @@ if not vt.name or not vt.uuid: return - self.new_vm(vt.uuid, vt.name, vts = [vt]) + if inetd_mode: + logging.debug('+when_ready u %s n %s' % ( repr(vt.uuid), repr(vt.name) )) + self.new_vm(vt.uuid, vt.name, vts = [vt], port = vt.proxy) + else: + self.new_vm(vt.uuid, vt.name, vts = [vt]) def handle_vc_uuid(self, vt): if not self.vms.has_key(vt.uuid): @@ -1002,6 +1206,176 @@ vm.vmotion = None del vm + def open_vm_pid(self, vmname, fpath): + workd = WORKHOME + "/" + vmname + logfn = workd + "/" + fpath + if not os.path.exists(workd): + os.makedirs(workd, 0755) + + pid = str(os.getpid()) + "\n" + pidfn = logfn + ",pid" + logging.debug("o_vm_pid %s" % repr(pidfn)) + pidf = open(pidfn, 'w') + pidf.write(pid) + pidf.close() + + # kill any existing PIDs. + vpidfn = workd + "/,pid" + if os.path.isfile(vpidfn): + vpidf = open(vpidfn, 'r') + for line in vpidf: + vpid = int(line) + logging.debug("o_vm_pid vpid# %d" % vpid) + try: + os.kill(vpid, signal.SIGQUIT) + except OSError: + pass + vpidf.close() + os.remove(vpidfn) + + os.link(pidfn, vpidfn) + + def open_vm_log(self, vmname, fpath): + workd = WORKHOME + "/" + vmname + logfn = workd + "/" + fpath + if not os.path.exists(workd): + os.makedirs(workd, 0755) + logging.debug("o_vm_log %s" % repr(logfn)) + + return open(logfn + ",log", 'w') + + def open_vm_html(self, vmname, fpath): + workd = WORKHOME + "/" + vmname + logfn = workd + "/" + fpath + if not os.path.exists(workd): + os.makedirs(workd, 0755) + logging.debug("o_vm_html %s" % repr(logfn)) + + fd = open(logfn + ".html", 'w') + fd.write("
")
+	fd.flush()
+	return fd
+
+    def open_vm_csv(self, vmname, fpath):
+	workd = WORKHOME + "/" + vmname
+	logfn = workd + "/" + fpath
+	if not os.path.exists(workd):
+		os.makedirs(workd, 0755)
+        logging.debug("o_vm_csv %s" % repr(logfn))
+
+	fd = open(logfn + ".csv", 'w')
+	fd.write("date,day,time,buflen,b64encode\n")
+	fd.flush()
+	return fd
+
+    def open_vm_uri(self, vm, vmname, uri):
+        vm.port = uri
+        logging.debug("o_vm_uri vmname %s uri %s" % ( repr(vmname), repr(uri)))
+
+	vmconf = load_conf(CONFIG, vmname)
+	if "name" not in vmconf:
+		# vmname not found. get default entry.
+        	logging.debug("o_vm_uri vmname %s not configured." %  repr(vmname) )
+		vmconf = load_conf(CONFIG, "")
+		if "name" not in vmconf:
+        		logging.debug("o_vm_uri vmname %s - no default('') config." %  repr(vmname) )
+		#
+	#
+
+        logging.debug("o_vm_uri vmname %s conf %s" % ( repr(vmname), repr(vmconf) ))
+
+	svclen = len(SVCIDENT)
+	if uri.startswith( SVCIDENT, 0, svclen ):
+		# uri includes logfile path.
+		pathi = uri.rfind("/")
+		svopts = uri[svclen:pathi].split(":")
+		logf_path = uri[pathi + 1:]
+
+		#
+		# expected svopts (termcap-style):
+		#
+		#  sp=  - VM serial port name/num.
+		#	    :-appended to vmname for config-entry lookup.
+		#
+		#  lt=	- log types. comma list: Raw,CSV,HTML(ex)
+		#
+
+# XXX- sorry, python rookie here. what is a better/cleaner way for this?
+
+		ltopts = None
+
+		for tcap in svopts:
+        		logging.debug("o_vm_uri vmname %s tcap %s" % ( repr(vmname), repr(tcap) ))
+			if tcap.startswith("sp="):
+				vmport = vmname + ":" + tcap
+				vmsp_cf = load_conf(CONFIG, vmport)
+				logging.debug("o_vm_uri vmport %s vmsp_cf %s" % ( repr(vmport), repr(vmsp_cf) ))
+				if "name" in vmsp_cf:
+					# a serial-port-specific config.
+					vmconf = vmsp_cf
+				#
+			elif tcap.startswith("lt="):
+				ltopts = tcap[3:].split(",")
+			#
+		#
+
+		vmopts = []
+		if "options" in vmconf:
+			vmopts = vmconf["options"]
+		if ltopts is not None:	
+			vmopts = ltopts
+
+		logging.debug("o_vm_uri vmname %s vmopts %s path %s svopts %s" % ( repr(vmname), repr(vmopts), repr(logf_path), repr(svopts)))
+
+
+# XXX- sorry, python rookie here. what is a better/cleaner way for this?
+
+		if "Raw" in vmopts:
+			# raw logging.
+			fd = self.open_vm_log(vmname, logf_path)
+			logcl = self.logRaw(fd)
+			logcl.uuid = vm.uuid
+			vm.clients.append(logcl)
+		#
+
+		if "HTML" in vmopts:
+			# html output.
+			fd = self.open_vm_html(vmname, logf_path)
+			logcl = self.logHTML(fd)
+			logcl.uuid = vm.uuid
+			vm.clients.append(logcl)
+		#
+
+		if "CSV" in vmopts:
+			# csv output.
+			fd = self.open_vm_csv(vmname, logf_path)
+			logcl = self.logCSV(fd)
+			logcl.uuid = vm.uuid
+			vm.clients.append(logcl)
+		#
+
+		self.open_vm_pid(vmname, logf_path)
+
+		# listen on port for user connection.
+		portnum = vmconf["port"]
+		if portnum != "-":
+			lport = int(portnum)
+			lhost = vmconf["listen"]
+        		logging.debug("o_vm_uri 1. listen port %s" % lport)
+
+			vm.listener = listenport(lport, backlog = 1, host = lhost)
+			self.add_reader(vm, self.new_client_connection)
+		#
+	else:
+		ou = urlparse(uri)
+
+		sock = in_connect(ou.hostname, ou.port)
+		client = self.Client(sock, (), ())
+		client.uuid = vm.uuid
+		self.add_reader(client, self.new_client_data)
+		vm.clients.append(client)
+	# endif
+
     def open_vm_port(self, vm, port):
         self.collect_orphans()
 
@@ -1018,7 +1392,7 @@
         assert not self.ports.has_key(vm.port)
         self.ports[vm.port] = vm.uuid
 
-        vm.listener = openport(vm.port)
+        vm.listener = listenport(vm.port)
         self.add_reader(vm, self.new_client_connection)
 
     def create_old_vms(self, vms):
@@ -1032,10 +1406,16 @@
 
         self.create_old_vms(self.backend.get_observed_vms())
 
-        self.add_reader(openport(self.proxy_port), self.new_vm_connection)
-        self.add_reader(openport(self.admin_port), self.new_admin_connection)
+        self.add_reader(listenport(self.proxy_port), self.new_vm_connection)
+        self.add_reader(listenport(self.admin_port), self.new_admin_connection)
         self.run_forever()
 
+    def inetd_run(self):
+	logging.info('in vSPC')
+	self.sock = in_open(sys.stdin.fileno())
+	self.inetd_new_vm_conn(self.sock)
+	self.run_forever()
+
 def do_query(host, port):
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     s.connect((host, port))
@@ -1174,12 +1554,158 @@
     --stdout: Log to stdout instead of syslog
     --no-fork: Don't daemonize
     -d|--debug: Debug mode (turns up logging and implies --stdout --no-fork)
+  Explanation of inetd options:
+    -i|--inetd-conf:  the inetd-mode configuration file.
+    -I|--inetd-vmID:  VM name to search in the inetd-mode config.
+    -V|--inetd-vmVar:
+		parse the inetd-mode config and display a Var value.
+		current Vars: 'name', 'port', 'listen', 'options'.
+
 ''' % (BASENAME, __revision__, BASENAME, ADMIN_PORT, PROXY_PORT,
        VM_PORT_START, BASENAME, BASENAME, BASENAME,
        socket.gethostname(), PROXY_PORT,
        BASENAME, BASENAME, VM_EXPIRE_TIME, BASENAME, BASENAME, BASENAME,
        ADMIN_PORT, PROXY_PORT, VM_PORT_START))
 
+##
+def load_conf(file, vmid = None):
+	'''
+	sample config:
+
+	#| #
+	#| # global settings:
+	#| #
+	#| 
+	#| #
+	#| # SVCIDENT:  the ID string for "Port URI:" in VM settings.
+	#| #
+	#| #  we expect "Port URI:" strings to include termcap-style fields ...
+	#| #
+	#| #	sp=  - indicate the VM serial-port name/number.
+	#| #
+	#| #	lt=  - set the logfile type list. [ Raw,CSV ]
+	#| #		can override the config file entry.
+	#| # 
+	#| #  for example:
+	#| #	if VM "Websrv-nut" has a Port URI:  "@vulture:sp=3:lt=CSV" then
+	#| #
+	#| #		- check this config file for "Websrv-nut:sp=3" first
+	#| #		  then check this config file for just "Websrv-nut"
+	#| #
+	#| #		- write any output to a CSV-style logfile, only.
+	#| #
+	#| #
+	#| $SVCIDENT=@vulture
+	#| 
+	#| # WORKHOME:  base spool directory for serial-port log files.
+	#| $WORKHOME=/var/spool/vulture
+	#| 
+	#| # LISTEN_HOST:  default IP/host for "listen" (see below).
+	#| $LISTEN_HOST=127.0.0.1
+	#| 
+	#| # SSL cert info file(s):
+	#| $SSLCERTFILE=/etc/ssl/mycert.pem
+	#| $SSLPRIVKEY=/etc/ssl/myprivkey.pem
+	#| # $SSLCERTFILE=/etc/ssl/mycombined.pem
+	#| # $SSLPRIVKEY=/etc/ssl/mycombined.pem
+	#| 
+	#| #================
+	#| #
+	#| # settings per VM:	vmName, listen, log-type-list
+	#| #
+	#| #  vmName:  VM name as sent by VMware.
+	#| #	must match the repr()-encoded version of the VM name.
+	#| #
+	#| #  listen:  where to connect in order to access the serial port.
+	#| #	tcp-port-num [ '@' IP-addr/hostname ]
+	#| #
+	#| #  log-type-list: serial port data recording types.
+	#| # 	Raw  = serial data written as-is.
+	#| # 	CSV  = data b64encoded, written in CSV format, with timestamps
+	#| #
+	#| #vmName		listen			log-type-list
+	#| #------		------			-------------
+	#| 
+	#| 'DBsrv-alpha'	 5004			Raw,CSV
+	#| 'Syslog-stor'	 5032			Raw,CSV
+	#| 
+	#| 'Websrv-nut'		 5140			Raw,CSV
+	#| 'Websrv-nut:sp=2'	 5141			CSV
+	#| 'Websrv-nut:sp=3'	 5142			CSV
+	#| 
+	#| 
+	#| 'xfile-srv'		 5992@10.4.3.99		Raw
+	#| 
+	#| # default (''): do not listen, only save raw output.
+	#| 
+	#| ''			 -			Raw
+	#| 
+	#| #####
+
+	'''
+
+	cf = open(file)
+	lines = cf.read().splitlines()
+	cf.close()
+
+	x = []
+
+	hunt = ""
+	huntlen = 0
+
+	if vmid is not None:
+		# VMware discourages - but does not restrict - use of
+		# special chars in VM names. so use repr() here to
+		# try to help simplify matching against VM names.
+		hunt = repr(vmid)
+		huntlen = len(hunt)
+
+	for s in lines:
+		# strip out comments.
+		line = s.partition('#')[0].strip()
+
+		if len(line) == 0:
+			# skip empty lines.
+			continue
+
+		if line[:1] == "$":
+			# set a general var.
+			x += line[1:].split("=")
+			#print x
+			continue
+
+		found = None
+		cl = []
+
+		if line[:huntlen] == hunt:
+			found = hunt
+			cl = line[huntlen:].split()
+
+		#print cl
+
+		if len(cl) < 2:
+			#print len(cl)
+			continue
+
+		if found is not None:
+			#print "found %s" % repr(found)
+			lhost = LISTEN_HOST
+			port = cl[0]
+			if "@" in port:
+				port, lhost = port.split("@")
+			x += [ "name",	found ]
+			x += [ "port",	port ]
+			x += [ "listen", lhost ]
+			x += [ "options", ''.join(cl[1:]).split(",") ]
+			break
+		#
+	#
+
+	work = dict([(k, v) for k,v in zip (x[::2], x[1::2])])
+	# print work
+	return work
+##
+
 if __name__ == '__main__':
     import getopt
 
@@ -1193,10 +1719,15 @@
     server_mode = False
     backend_type_name = 'Memory'
     backend_args = ''
+    #
+    inetd_conf = None
+    vm_conf = None
+    #
 
     try:
-        opts, args = getopt.gnu_getopt(sys.argv[1:], 'a:f:hdp:r:s',
+        opts, args = getopt.gnu_getopt(sys.argv[1:], 'a:f:hi:I:dp:r:sSV:',
                                        ['help', 'debug', 'admin-port=',
+                                        'inetd-conf=', 'inetd-vmID=', 'inetd-vmVar=', 'inetd-SSL',
                                         'proxy-port=', 'port-range-start=',
                                         'server', 'stdout', 'no-fork',
                                         'vm-expire-time=',
@@ -1213,6 +1744,35 @@
                 fork = False
             elif o in ['-a', '--admin-port']:
                 admin_port = int(a)
+            elif o in ['-i', '--inetd-conf']:
+		inetd_mode = True
+                CONFIG = a
+		inetd_conf = load_conf(CONFIG)
+		SVCIDENT = inetd_conf["SVCIDENT"]
+		WORKHOME = inetd_conf["WORKHOME"]
+            elif o in ['-I', '--inetd-vmID']:
+		inetd_mode = True
+		if SVCIDENT is not None:
+		    vm_conf = load_conf(CONFIG, a)
+		else:
+		    usage()
+		    sys.exit(11)
+            elif o in ['-V', '--inetd-vmVar']:
+		inetd_mode = True
+		if vm_conf is not None:
+		    print vm_conf[a]
+		    sys.exit(0)
+		else:
+		    usage()
+		    sys.exit(12)
+            elif o in ['-S', '--inetd-SSL']:
+		if SVCIDENT is not None:
+			SSLCERTFILE = inetd_conf["SSLCERTFILE"]
+			SSLPRIVKEY = inetd_conf["SSLPRIVKEY"]
+			inetd_mode_ssl = True
+		else:
+		    usage()
+		    sys.exit(12)
             elif o in ['-p', '--proxy-port']:
                 proxy_port = int(a)
             elif o in ['-r', '--port-range-start']:
@@ -1243,6 +1803,11 @@
         usage()
         sys.exit(2)
 
+    if inetd_mode:        
+	debug = True
+	fork = False
+	server_mode = True
+
     if not server_mode:        
         if len(args) != 1:
             print "Expected 1 argument, found %d" % len(args)
@@ -1271,9 +1836,11 @@
         daemonize()
 
     try:
-        backend.start()
-
-        vSPC(proxy_port, admin_port, vm_port_start, vm_expire_time, backend).run()
+        if inetd_mode:
+            vSPC(proxy_port, admin_port, vm_port_start, vm_expire_time, backend).inetd_run()
+        else:
+            backend.start()
+            vSPC(proxy_port, admin_port, vm_port_start, vm_expire_time, backend).run()
     except KeyboardInterrupt:
         logging.info("Shutdown requested on keyboard, exiting")
         sys.exit(0)