Xu He Jie has uploaded a new change for review. Change subject: [WIP]Add text-based console support ......................................................................
[WIP]Add text-based console support This patch use ssh protocol providing a text-based console for user. User can have another choice except spice and vnc. Text-based console consumes less bandwidth and generic ssh client can connect it. This patch try to keep same behaviour as vnc. Two new params for VM creation: consoleEnable: if true, it will enable console support for this VM consolePort: the port used by ssh server. if -1 specified, the port will be allocated automatically. The allocated port will show at getVmStats(). setVmTicket: password: used to connect this console. ttl: after expired, the console will be closed. existingConnAction: if 'disconnect' specified, the current connection will be closed, and reopen with new params. TODO: * Make console sharable. Current console only can connect by only one user * setTicket will be used by vnc and console seperately Change-Id: I69904bf7aafd4f764a256d4075c9bf71d988e7c5 Signed-off-by: Xu He Jie <[email protected]> --- M vdsm.spec.in M vdsm/Makefile.am A vdsm/consoleServer.py M vdsm/constants.py.in M vdsm/libvirtvm.py M vdsm/supervdsmServer.py M vdsm/utils.py M vdsm/vm.py 8 files changed, 377 insertions(+), 2 deletions(-) git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/65/7165/1 diff --git a/vdsm.spec.in b/vdsm.spec.in index bf8684a..33c8d6f 100644 --- a/vdsm.spec.in +++ b/vdsm.spec.in @@ -545,6 +545,7 @@ %{_datadir}/%{vdsm_name}/blkid.py* %{_datadir}/%{vdsm_name}/caps.py* %{_datadir}/%{vdsm_name}/clientIF.py* +%{_datadir}/%{vdsm_name}/consoleServer.py* %{_datadir}/%{vdsm_name}/API.py* %{_datadir}/%{vdsm_name}/hooking.py* %{_datadir}/%{vdsm_name}/hooks.py* diff --git a/vdsm/Makefile.am b/vdsm/Makefile.am index 80befdb..dbb4dc3 100644 --- a/vdsm/Makefile.am +++ b/vdsm/Makefile.am @@ -31,6 +31,7 @@ caps.py \ clientIF.py \ configNetwork.py \ + consoleServer.py \ debugPluginClient.py \ guestIF.py \ hooking.py \ diff --git a/vdsm/consoleServer.py b/vdsm/consoleServer.py new file mode 100755 index 0000000..172fcae --- /dev/null +++ b/vdsm/consoleServer.py @@ -0,0 +1,279 @@ +#! /usr/bin/python +# +# Copyright (C) 2012, IBM Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Refer to the README and COPYING files for full details of the license +# + +import sys +import logging +import logging.config +import socket +import select +import os +import subprocess + +import paramiko + +from vdsm import constants +from vdsm import utils + + +class PortAllocator(): + + def __init__(self, minPort, maxPort): + self._minPort = minPort + self._maxPort = maxPort + self._bitmap = utils.Bitmap(self._maxPort - self._minPort) + self._addrs = {} + + def getPort(self): + for i in range(0, self._maxPort - self._minPort): + if self._bitmap[i] == 0x0: + servSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + servSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + servSock.bind(("", i + self._minPort)) + except socket.error: + continue + finally: + servSock.close() + + self._bitmap[i] = 0x1 + return i + self._minPort + + raise Exception("Can not free port on address: %s", addr) + + def freePort(self, port): + print("free", port - self._minPort) + self._bitmap[port - self._minPort] = 0x0 + +portAllocator = PortAllocator(constants.SSH_CONSOLE_MIN_PORT, + constants.SSH_CONSOLE_MAX_PORT) + + +class SSHConsole(object): + AUTO_PORT = -1 + logger = logging.getLogger("console") + + def __init__(self, pty="", addr="", port=AUTO_PORT, + passwd="", timeout=-1, hostKeyFile="", + portAlloc=portAllocator): + self._prog = None + self._pty = pty + self._addr = addr + + self._portAlloc = portAllocator + self._port = port + if self._port == self.AUTO_PORT: + self._port = self._portAlloc.getPort() + + self._passwd = passwd + self._hostKeyFile = hostKeyFile + self._timeout = timeout + + def setPty(self, pty): + self._pty = pty + + def setPasswd(self, passwd): + self._passwd = passwd + + def setTimeout(self, timeout): + self._timeout = timeout + + def getPort(self): + return self._port + + def open(self): + self.logger.info("open console with: %s", {"pty": self._pty, + "addr": self._addr, + "port": self._port, + "timeout": self._timeout, + "hostKeyFile": self._hostKeyFile}) + f = open(self._pty, "rwa+") + self._prog = subprocess.Popen([constants.EXT_PYTHON, + os.path.join(constants.P_VDSM, __file__), + self._addr, + str(self._port), + self._passwd, + str(self._timeout), + self._hostKeyFile], + stdin=f, + stdout=f, + stderr=subprocess.STDOUT) + + def close(self): + if self._prog: + self._prog.kill() + self._prog.wait() + self._portAlloc.freePort(self._port) + + def isOpen(self): + return self._prog != None and self._prog.poll() == None + + +class ParamikoServerInf(paramiko.ServerInterface): + + def __init__(self, passwd): + self._passwd = passwd + + def check_channel_request(self, kind, chanid): + if kind == 'session': + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_UNKNOWN_CHANNEL_TYPE + + def check_auth_password(self, username, password): + if password != self._passwd: + return paramiko.AUTH_FAILED + return paramiko.AUTH_SUCCESSFUL + + def check_channel_shell_request(self, channel): + return True + + def check_channel_pty_request(self, channel, term, width, height, + pixelwidth, pixelheight, modes): + return True + + def check_auth_none(self, username): + return paramiko.AUTH_FAILED + + +class SSHConsoleServer(object): + + def __init__(self, logger, inputf, outputf, + addr, port, passwd, timeout, hostKeyFile): + self._logger = logger + self._output = outputf + self._input = inputf + self._addr = addr + self._port = port + self._passwd = passwd + self._timeout = timeout + self._hostKeyFile = hostKeyFile + + def start(self): + try: + hostKey = paramiko.RSAKey(filename=self._hostKeyFile) + except (paramiko.SSHException, IOError) as e: + self._logger.error("Can not load key from %s", + self._hostKeyFile, + exc_info=True) + return + + try: + servSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + servSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except socket.error as e: + self._logger.error("create socket failed: %s", + e.message, exc_info=True) + return + + try: + servSock.bind((self._addr, self._port)) + servSock.listen(1) + except socket.error as e: + self._logger.error("failed binding address on %s:%s", + self._addr, self._port, exc_info=True) + servSock.close() + return + + while True: + self._logger.info("ssh console server waiting for connection") + + rd, wr, er = select.select([servSock, self._input], + [], [], self._timeout) + + if len(rd) == 0: + self._logger.info("connection timeout") + break + elif self._input in rd: + # if 0 returned means input was closed + if len(self._input.read()) == 0: + self._logger.info("console closed") + break + + clientSock, addr = servSock.accept() + self._logger.info("accept connection from %s", addr) + + trans = paramiko.Transport(clientSock) + trans.add_server_key(hostKey) + serverInf = ParamikoServerInf(self._passwd) + + try: + trans.start_server(server=serverInf) + except (paramiko.SSHException, EOFError) as e: + self._logger.error("start ssh server failed: %s", e.message) + clientSock.close() + continue + + chan = trans.accept() + + if chan is None: + self._logger.error("get SSH channel failed") + continue + + while True: + try: + rd, wr, er = select.select([chan, self._output], [], []) + + if chan in rd: + buf = chan.recv(1024) + + if len(buf) == 0: + self._logger.info("ssh channel closed") + break + + os.write(self._input.fileno(), buf) + elif self._output in rd: + buf = os.read(self._output.fileno(), 1) + + if chan.send(buf) == 0: + self._logger.info("ssh channel closed") + break + else: + self._logger.error("could not be happened", + exc_info=True) + + except IOError as e: + self._logger.error("got io error", exc_info=True) + break + + self._logger.info("ssh closed") + chan.close() + clientSock.close() + trans.close() + + servSock.close() + +if __name__ == '__main__': + logging.config.fileConfig(constants.P_VDSM_CONF + 'logger.conf') + logger = logging.getLogger("consoleServer") + + try: + addr = sys.argv[1] + port = int(sys.argv[2]) + passwd = sys.argv[3] + timeout = int(sys.argv[4]) + hostKey = sys.argv[5] + + logger.info("start a ssh server") + s = SSHConsoleServer(logger, sys.stdin, sys.stdout, + addr, port, passwd, timeout, hostKey) + s.start() + except Exception as e: + logger.error("ssh server failed: %s: %s", e.message, type(e)) diff --git a/vdsm/constants.py.in b/vdsm/constants.py.in index 3a5fdce..53ae56f 100644 --- a/vdsm/constants.py.in +++ b/vdsm/constants.py.in @@ -167,3 +167,9 @@ max_fds 4096 } """ + +# +# ssh console server port constants +# +SSH_CONSOLE_MAX_PORT = 65535 +SSH_CONSOLE_MIN_PORT = 12200 diff --git a/vdsm/libvirtvm.py b/vdsm/libvirtvm.py index ddd030e..ea3b1b8 100644 --- a/vdsm/libvirtvm.py +++ b/vdsm/libvirtvm.py @@ -24,6 +24,7 @@ import time import threading import json +import os.path import vm from vdsm.define import ERROR, doneCode, errCode @@ -37,6 +38,7 @@ import caps from vdsm import netinfo import supervdsm +import consoleServer _VMCHANNEL_DEVICE_NAME = 'com.redhat.rhevm.vdsm' @@ -1232,6 +1234,7 @@ self._getUnderlyingVideoDeviceInfo() self._getUnderlyingControllerDeviceInfo() self._getUnderlyingBalloonDeviceInfo() + self._getUnderlyingConsoleDeviceInfo() # Obtain info of all unknown devices. Must be last! self._getUnderlyingUnknownDeviceInfo() @@ -1260,6 +1263,23 @@ self.guestAgent = guestIF.GuestAgent(self._guestSocketFile, self.cif.channelListener, self.log, connect=utils.tobool(self.conf.get('vmchannel', 'true'))) + + if utils.tobool(self.conf.get("enableConsole", False)): + self.log.info("vm console enabled") + + supervdsm.getProxy().changePtyOwner( + int(os.path.basename(self.conf["consolePath"]))) + + self._console = consoleServer.SSHConsole( + self.conf['consolePath'], + "", + int(self.conf.get("consolePort", -1)), + "", + -1, + os.path.join( + constants.P_VDSM_KEYS, 'vdsmkey.pem')) + + self.conf["consolePort"] = self._console.getPort() self._guestCpuRunning = self._dom.info()[0] == libvirt.VIR_DOMAIN_RUNNING if self.lastStatus not in ('Migration Destination', @@ -1931,6 +1951,21 @@ graphics.setAttribute('passwdValidTo', validto) if graphics.getAttribute('type') == 'spice': graphics.setAttribute('connected', connAct) + + if self._console: + self.log.debug("setticket for console") + if connAct == "disconnect": + if self._console.isOpen(): + self._console.close() + self._console.setPasswd(otp) + self._console.setTimeout(seconds) + self._console.open() + else: + if not self._console.isOpen(): + self._console.setPasswd(otp) + self._console.setTimeout(seconds) + self._console.open() + hooks.before_vm_set_ticket(self._lastXMLDesc, self.conf, params) self._dom.updateDeviceFlags(graphics.toxml(), 0) hooks.after_vm_set_ticket(self._lastXMLDesc, self.conf, params) @@ -2041,6 +2076,8 @@ self._vmStats.stop() if self.guestAgent: self.guestAgent.stop() + if self._console: + self._console.close() if self._dom: try: self._dom.destroyFlags(libvirt.VIR_DOMAIN_DESTROY_GRACEFUL) @@ -2219,6 +2256,17 @@ dev['address'] = address dev['alias'] = alias + def _getUnderlyingConsoleDeviceInfo(self): + """ + Obtain console device info from libvirt + """ + consolexml = xml.dom.minidom.parseString(self._lastXMLDesc) \ + .childNodes[0].getElementsByTagName('devices')[0] \ + .getElementsByTagName('console')[0] + + self.conf["consolePath"] = str(consolexml.getElementsByTagName( + 'source')[0].getAttribute('path')) + def _getUnderlyingVideoDeviceInfo(self): """ Obtain video devices info from libvirt. diff --git a/vdsm/supervdsmServer.py b/vdsm/supervdsmServer.py index 8d9ada4..e3966be 100755 --- a/vdsm/supervdsmServer.py +++ b/vdsm/supervdsmServer.py @@ -43,8 +43,8 @@ from supervdsm import _SuperVdsmManager, PIDFILE, ADDRESS from storage.fileUtils import chown, resolveGid, resolveUid from storage.fileUtils import validateAccess as _validateAccess -from vdsm.constants import METADATA_GROUP, METADATA_USER, EXT_UDEVADM, \ - DISKIMAGE_USER, DISKIMAGE_GROUP, P_LIBVIRT_VMCHANNELS +from vdsm.constants import METADATA_GROUP, METADATA_USER, EXT_CHOWN, \ + EXT_UDEVADM, DISKIMAGE_USER, DISKIMAGE_GROUP, P_LIBVIRT_VMCHANNELS from storage.devicemapper import _removeMapping, _getPathsStatus import storage.misc import configNetwork @@ -268,6 +268,18 @@ def removeFs(self, path): return mkimage.removeFs(path) + @logDecorator + def changePtyOwner(self, ptyNum): + if type(ptyNum) != int: + raise TypeError("ptyNum must be integer") + + cmd = [EXT_CHOWN, "vdsm:kvm", "/dev/pts/" + str(ptyNum)] + rc, out, err = storage.misc.execCmd(cmd, sudo=False) + if rc: + raise OSError(errno.EINVAL, "can not change own of /dev/pts/%d, " \ + "out: %s\nerr: %s" % + (ptyNum, out, err)) + def __pokeParent(parentPid): try: diff --git a/vdsm/utils.py b/vdsm/utils.py index 5e2d4e5..f0ebf79 100644 --- a/vdsm/utils.py +++ b/vdsm/utils.py @@ -36,6 +36,7 @@ import fcntl import functools import stat +from array import array import ethtool @@ -829,3 +830,27 @@ def __unicode__(self): return unicode(self.cmd) + + +class Bitmap(object): + + def __init__(self, size): + self._size = size + self._array = array('B', [0] * ((size / 8) + 1)) + + def __len__(self): + return self._size + + def __getitem__(self, key): + if key >= self._size or key < 0: + raise IndexError + return (self._array[key / 8] >> (key % 8)) & 0x1 + + def __setitem__(self, key, value): + if key >= self._size or key < 0: + raise IndexError + p = key / 8 + if value == 0x1: + self._array[p] = self._array[p] | (0x1 << (key % 8)) + else: + self._array[p] = self._array[p] & ~(0x1 << (key % 8)) \ No newline at end of file diff --git a/vdsm/vm.py b/vdsm/vm.py index 44796c3..f2b8e04 100644 --- a/vdsm/vm.py +++ b/vdsm/vm.py @@ -311,6 +311,7 @@ SOUND_DEVICES: [], VIDEO_DEVICES: [], CONTROLLER_DEVICES: [], GENERAL_DEVICES: [], BALLOON_DEVICES: [], REDIR_DEVICES: []} + self._console = None def _get_lastStatus(self): SHOW_PAUSED_STATES = ('Powering down', 'RebootInProgress', 'Up') @@ -971,6 +972,8 @@ stats['cdrom'] = self.conf['cdrom'] if 'boot' in self.conf: stats['boot'] = self.conf['boot'] + if utils.tobool(self.conf.get('enableConsole', False)): + stats['consolePort'] = self.conf['consolePort'] decStats = {} try: -- To view, visit http://gerrit.ovirt.org/7165 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I69904bf7aafd4f764a256d4075c9bf71d988e7c5 Gerrit-PatchSet: 1 Gerrit-Project: vdsm Gerrit-Branch: master Gerrit-Owner: Xu He Jie <[email protected]> _______________________________________________ vdsm-patches mailing list [email protected] https://lists.fedorahosted.org/mailman/listinfo/vdsm-patches
