Allow xm to spawn vnc viewer
authorKeir Fraser <keir.fraser@citrix.com>
Wed, 30 Jul 2008 15:22:45 +0000 (16:22 +0100)
committerKeir Fraser <keir.fraser@citrix.com>
Wed, 30 Jul 2008 15:22:45 +0000 (16:22 +0100)
The new merged qemu no longer has the ability to spawn a vnc viewer
process in the bowels of the xend/qemu stack.  In this patch we
provide support for this use case in a different manner - one more
akin to the mechanism used for `xm console' and `xm create -c'.

We introduce new xm options:
   xm create --vncviewer [--vncviewer-autopass]
   xm vncviewer [--vncviewer-autopass]

These spawn a VNC viewer, obtaining the relevant information
(including the port number and if you tell it your viewer supports it
the password to use) directly from xenstore.

Like xm console it waits in the foreground for the vnc port to become
available; the timeout case isn't handled as well as it might be - it
just causes the whole program (xm) to die with `Alarm clock' but this
is difficult to deal with given the current structure of the xs Python
lowlevel interface, which doesn't provide a timeout on the call to
wait for a xenstore watch.

Signed-off-by: Ian Jackson <ian.jackson@eu.citrix.com>
tools/python/xen/util/utils.py
tools/python/xen/xm/console.py
tools/python/xen/xm/create.py
tools/python/xen/xm/main.py

index b6c7d2ae40f52564dc3c2e231bf9d6ab5bfb4333..e13d29bb2d99ac833734031c119dec7f11760482 100644 (file)
@@ -1,6 +1,50 @@
 import traceback
 import sys
+import os
 
 def exception_string(e):
         (ty,v,tb) = sys.exc_info()
         return traceback.format_exception_only(ty,v)
+
+def daemonize(prog, args, stdin_tmpfile=None):
+    """Runs a program as a daemon with the list of arguments.  Returns the PID
+    of the daemonized program, or returns 0 on error.
+    """
+    r, w = os.pipe()
+    pid = os.fork()
+
+    if pid == 0:
+        os.close(r)
+        w = os.fdopen(w, 'w')
+        os.setsid()
+        try:
+            pid2 = os.fork()
+        except:
+            pid2 = None
+        if pid2 == 0:
+            os.chdir("/")
+            null_fd = os.open("/dev/null", os.O_RDWR)
+            if stdin_tmpfile is not None:
+                os.dup2(stdin_tmpfile.fileno(), 0)
+            else:
+                os.dup2(null_fd, 0)
+            os.dup2(null_fd, 1)
+            os.dup2(null_fd, 2)
+            for fd in range(3, 256):
+                try:
+                    os.close(fd)
+                except:
+                    pass
+            os.execvp(prog, args)
+            os._exit(1)
+        else:
+            w.write(str(pid2 or 0))
+            w.close()
+            os._exit(0)
+    os.close(w)
+    r = os.fdopen(r)
+    daemon_pid = int(r.read())
+    r.close()
+    os.waitpid(pid, 0)
+    return daemon_pid
+
index f971644fe91c44adfd819db8becdc0674f04675c..8abcb1d9a63a346a04e6577bc8d39a8a904366ca 100644 (file)
 # Copyright (C) 2005 XenSource Ltd
 #============================================================================
 
+import xen.util.auxbin
+import xen.lowlevel.xs
+import os
+import sys
+import signal
+from xen.util import utils
 
 XENCONSOLE = "xenconsole"
 
-import xen.util.auxbin
-
 def execConsole(domid):
     xen.util.auxbin.execute(XENCONSOLE, [str(domid)])
+
+
+class OurXenstoreConnection:
+    def __init__(self):
+        self.handle = xen.lowlevel.xs.xs()
+    def read_eventually(self, path):
+        watch = None
+        trans = None
+        try:
+            signal.alarm(10)
+            watch = self.handle.watch(path, None)
+            while True:
+                result = self.handle.read('0', path)
+                if result is not None:
+                    return result
+                self.handle.read_watch()
+            self.handle.unwatch(path, watch)
+            signal.alarm(0)
+        except:
+            signal.alarm(0)
+            if watch is not None: self.handle.unwatch(path, watch)
+            raise
+    def read_maybe(self, path):
+        return self.handle.read('0', path)
+
+def runVncViewer(domid, do_autopass, do_daemonize=False):
+    xs = OurXenstoreConnection()
+    d = '/local/domain/%d/' % domid
+    vnc_port = xs.read_eventually(d + 'console/vnc-port')
+    vfb_backend = xs.read_maybe(d + 'device/vfb/0/backend')
+    vnc_listen = None
+    vnc_password = None
+    vnc_password_tmpfile = None
+    cmdl = ['vncviewer']
+    if vfb_backend is not None:
+        vnc_listen = xs.read_maybe(vfb_backend + '/vnclisten')
+        if do_autopass:
+            vnc_password = xs.read_maybe(vfb_backend + '/vncpasswd')
+            if vnc_password is not None:
+                cmdl.append('-autopass')
+                vnc_password_tmpfile = os.tmpfile()
+                print >>vnc_password_tmpfile, vnc_password
+                vnc_password_tmpfile.seek(0)
+                vnc_password_tmpfile.flush()
+    if vnc_listen is None:
+        vnc_listen = 'localhost'
+    cmdl.append('%s:%d' % (vnc_listen, int(vnc_port) - 5900))
+    if do_daemonize:
+        pid = utils.daemonize('vncviewer', cmdl, vnc_password_tmpfile)
+        if pid == 0:
+            puts >>sys.stderr, 'failed to invoke vncviewer'
+            os._exit(-1)
+    else:
+        print 'invoking ', ' '.join(cmdl)
+        if vnc_password_tmpfile is not None:
+            os.dup2(vnc_password_tmpfile.fileno(), 0)
+        os.execvp('vncviewer', cmdl)
index 3652d8c7755a55390119c48c56481f2005f86368..9d00a812266353d73edc165cfe28fb0b4ee15ca0 100644 (file)
@@ -36,10 +36,12 @@ from xen.util import blkif
 from xen.util import vscsi_util
 import xen.util.xsm.xsm as security
 from xen.xm.main import serverType, SERVER_XEN_API, get_single_vm
+from xen.util import utils
 
 from xen.xm.opts import *
 
 from main import server
+from main import domain_name_to_domid
 import console
 
 
@@ -118,6 +120,14 @@ gopts.opt('console_autoconnect', short='c',
           fn=set_true, default=0,
           use="Connect to the console after the domain is created.")
 
+gopts.opt('vncviewer',
+          fn=set_true, default=0,
+          use="Connect to the VNC display after the domain is created.")
+
+gopts.opt('vncviewer-autopass',
+          fn=set_true, default=0,
+          use="Pass VNC password to viewer via stdin and -autopass.")
+
 gopts.var('vncpasswd', val='NAME',
           fn=set_value, default=None,
           use="Password for VNC console on HVM domain.")
@@ -128,7 +138,7 @@ gopts.var('vncviewer', val='no|yes',
            "The address of the vncviewer is passed to the domain on the "
            "kernel command line using 'VNC_SERVER=<host>:<port>'. The port "
            "used by vnc is 5500 + DISPLAY. A display value with a free port "
-           "is chosen if possible.\nOnly valid when vnc=1.")
+           "is chosen if possible.\nOnly valid when vnc=1.\nDEPRECATED")
 
 gopts.var('vncconsole', val='no|yes',
           fn=set_bool, default=None,
@@ -1108,44 +1118,6 @@ def choose_vnc_display():
     return None
 vncpid = None
 
-def daemonize(prog, args):
-    """Runs a program as a daemon with the list of arguments.  Returns the PID
-    of the daemonized program, or returns 0 on error.
-    """
-    r, w = os.pipe()
-    pid = os.fork()
-
-    if pid == 0:
-        os.close(r)
-        w = os.fdopen(w, 'w')
-        os.setsid()
-        try:
-            pid2 = os.fork()
-        except:
-            pid2 = None
-        if pid2 == 0:
-            os.chdir("/")
-            for fd in range(0, 256):
-                try:
-                    os.close(fd)
-                except:
-                    pass
-            os.open("/dev/null", os.O_RDWR)
-            os.dup2(0, 1)
-            os.dup2(0, 2)
-            os.execvp(prog, args)
-            os._exit(1)
-        else:
-            w.write(str(pid2 or 0))
-            w.close()
-            os._exit(0)
-    os.close(w)
-    r = os.fdopen(r)
-    daemon_pid = int(r.read())
-    r.close()
-    os.waitpid(pid, 0)
-    return daemon_pid
-
 def spawn_vnc(display):
     """Spawns a vncviewer that listens on the specified display.  On success,
     returns the port that the vncviewer is listening on and sets the global
@@ -1154,7 +1126,7 @@ def spawn_vnc(display):
     vncargs = (["vncviewer", "-log", "*:stdout:0",
             "-listen", "%d" % (VNC_BASE_PORT + display) ])
     global vncpid
-    vncpid = daemonize("vncviewer", vncargs)
+    vncpid = utils.daemonize("vncviewer", vncargs)
     if vncpid == 0:
         return 0
 
@@ -1362,6 +1334,11 @@ def main(argv):
     elif not opts.is_xml:
         dom = make_domain(opts, config)
         
+    if opts.vals.vncviewer:
+        domid = domain_name_to_domid(sxp.child_value(config, 'name', -1))
+        vncviewer_autopass = getattr(opts.vals,'vncviewer-autopass', False)
+        console.runVncViewer(domid, vncviewer_autopass, True)
+    
 def do_console(domain_name):
     cpid = os.fork() 
     if cpid != 0:
@@ -1373,13 +1350,7 @@ def do_console(domain_name):
                 if os.WEXITSTATUS(rv) != 0:
                     sys.exit(os.WEXITSTATUS(rv))
             try:
-                # Acquire the console of the created dom
-                if serverType == SERVER_XEN_API:
-                    domid = server.xenapi.VM.get_domid(
-                               get_single_vm(domain_name))
-                else:
-                    dom = server.xend.domain(domain_name)
-                    domid = int(sxp.child_value(dom, 'domid', '-1'))
+                domid = domain_name_to_domid(domain_name)
                 console.execConsole(domid)
             except:
                 pass
index b7383d21901c49908e31968fc1790b14e9663f69..36268d66a2dd118a625a9cf3072cecdeb9423b61 100644 (file)
@@ -64,6 +64,9 @@ import inspect
 from xen.xend import XendOptions
 xoptions = XendOptions.instance()
 
+import signal
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+
 # getopt.gnu_getopt is better, but only exists in Python 2.3+.  Use
 # getopt.getopt if gnu_getopt is not available.  This will mean that options
 # may only be specified before positional arguments.
@@ -97,6 +100,8 @@ SUBCOMMAND_HELP = {
     
     'console'     : ('[-q|--quiet] <Domain>',
                      'Attach to <Domain>\'s console.'),
+    'vncviewer'   : ('[--[vncviewer-]autopass] <Domain>',
+                     'Attach to <Domain>\'s VNC server.'),
     'create'      : ('<ConfigFile> [options] [vars]',
                      'Create a domain based on <ConfigFile>.'),
     'destroy'     : ('<Domain>',
@@ -243,6 +248,10 @@ SUBCOMMAND_OPTIONS = {
     'console': (
        ('-q', '--quiet', 'Do not print an error message if the domain does not exist'),
     ),
+    'vncviewer': (
+       ('', '--autopass', 'Pass VNC password to viewer via stdin and -autopass'),
+       ('', '--vncviewer-autopass', '(consistency alias for --autopass)'),
+    ),
     'dmesg': (
        ('-c', '--clear', 'Clear dmesg buffer as well as printing it'),
     ),
@@ -260,6 +269,8 @@ SUBCOMMAND_OPTIONS = {
     'start': (
        ('-p', '--paused', 'Do not unpause domain after starting it'),
        ('-c', '--console_autoconnect', 'Connect to the console after the domain is created'),
+       ('', '--vncviewer', 'Connect to display via VNC after the domain is created'),
+       ('', '--vncviewer-autopass', 'Pass VNC password to viewer via stdin and -autopass'),
     ),
     'resume': (
        ('-p', '--paused', 'Do not unpause domain after resuming it'),
@@ -277,6 +288,7 @@ SUBCOMMAND_OPTIONS = {
 
 common_commands = [
     "console",
+    "vncviewer",
     "create",
     "new",
     "delete",
@@ -304,6 +316,7 @@ common_commands = [
 
 domain_commands = [
     "console",
+    "vncviewer",
     "create",
     "new",
     "delete",
@@ -1185,14 +1198,20 @@ def xm_start(args):
 
     paused = False
     console_autoconnect = False
+    vncviewer = False
+    vncviewer_autopass = False
 
     try:
-        (options, params) = getopt.gnu_getopt(args, 'cp', ['console_autoconnect','paused'])
+        (options, params) = getopt.gnu_getopt(args, 'cp', ['console_autoconnect','paused','vncviewer','vncviewer-autopass'])
         for (k, v) in options:
             if k in ('-p', '--paused'):
                 paused = True
             if k in ('-c', '--console_autoconnect'):
                 console_autoconnect = True
+            if k in ('--vncviewer'):
+                vncviewer = True
+            if k in ('--vncviewer-autopass'):
+                vncviewer_autopass = True
 
         if len(params) != 1:
             raise OptionError("Expects 1 argument")
@@ -1205,6 +1224,9 @@ def xm_start(args):
     if console_autoconnect:
         start_do_console(dom)
 
+    if console_autoconnect:
+        console.runVncViewer(domid, vncviewer_autopass, True)
+
     try:
         if serverType == SERVER_XEN_API:
             server.xenapi.VM.start(get_single_vm(dom), paused)
@@ -1783,6 +1805,40 @@ def xm_console(args):
     console.execConsole(domid)
 
 
+def domain_name_to_domid(domain_name):
+    if serverType == SERVER_XEN_API:
+        domid = server.xenapi.VM.get_domid(
+                   get_single_vm(domain_name))
+    else:
+        dom = server.xend.domain(domain_name)
+        domid = int(sxp.child_value(dom, 'domid', '-1'))
+    return domid
+
+def xm_vncviewer(args):
+    autopass = False;
+
+    try:
+        (options, params) = getopt.gnu_getopt(args, '', ['autopass','vncviewer-autopass'])
+    except getopt.GetoptError, opterr:
+        err(opterr)
+        usage('vncviewer')
+
+    for (k, v) in options:
+        if k in ['--autopass','--vncviewer-autopass']:
+            autopass = True
+        else:
+            assert False
+
+    if len(params) != 1:
+        err('No domain given (or several parameters specified)')
+        usage('vncviewer')
+
+    dom = params[0]
+    domid = domain_name_to_domid(dom)
+
+    console.runVncViewer(domid, autopass)
+
+
 def xm_uptime(args):
     short_mode = 0
 
@@ -2617,6 +2673,7 @@ commands = {
     "event-monitor": xm_event_monitor,
     # console commands
     "console": xm_console,
+    "vncviewer": xm_vncviewer,
     # xenstat commands
     "top": xm_top,
     # domain commands