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

Reply via email to