components/openstack/nova/files/zone-vnc-console
changeset 4553 13705ca3643b
parent 4049 150852e281c4
child 6854 52081f923019
--- a/components/openstack/nova/files/zone-vnc-console	Thu Jun 25 08:10:54 2015 -0700
+++ b/components/openstack/nova/files/zone-vnc-console	Thu Jun 25 15:41:31 2015 -0700
@@ -14,22 +14,28 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import ConfigParser
+import contextlib
 import errno
+import fcntl
 import os
 import pwd
 import smf_include
+import socket
 import subprocess
 import sys
+import tempfile
 import time
 
-from subprocess import CalledProcessError, check_call, Popen
-from tempfile import mkstemp
+from oslo_config import cfg
 
 GTF = "/usr/bin/gtf"
+SVCADM = "/usr/sbin/svcadm"
 SVCCFG = "/usr/sbin/svccfg"
 SVCPROP = "/usr/bin/svcprop"
 VNCSERVER = "/usr/bin/vncserver"
 XRANDR = "/usr/bin/xrandr"
+NOVACFG = "/etc/nova/nova.conf"
 XSTARTUPHDR = "# WARNING: THIS FILE GENERATED BY SMF.\n" + \
               "#   DO NOT EDIT THIS FILE.  EDITS WILL BE LOST.\n"
 XRESOURCES = "[[ -f ~/.Xresources ]] && /usr/bin/xrdb -merge ~/.Xresources\n"
@@ -39,34 +45,80 @@
 XTERMOPTS = ' -b 0 -fa Monospace -fs 14 -fg white -bg black -title ' + \
             '"Zone Console: $ZONENAME"'
 XWININFO = "/usr/bin/xwininfo"
+
+# Port ranges allocated for VNC and X11 sockets.
+VNCPORT_START = 5900
+VNCPORT_END = 5999
+X11PORT_START = 6000
+
 # Enclose command in comments to prevent xterm consuming zlogin opts
 ZLOGINOPTS = ' -e "/usr/bin/pfexec /usr/sbin/zlogin -C -E $ZONENAME"\n'
 XSTARTUP = XSTARTUPHDR + XRESOURCES + XTERM + XTERMOPTS + ZLOGINOPTS
 
+CONF = cfg.CONF
+CONF.import_opt('vncserver_listen', 'nova.vnc')
+
 
 def start():
+    fmri = os.environ['SMF_FMRI']
+    # This is meant to be an on-demand service.
+    # Determine if nova-compute requested enablement of this instance.
+    # Exit with SMF_EXIT_TEMP_DISABLE if not true.
+    cmd = [SVCPROP, '-p', 'vnc/nova-enabled', fmri]
+    svcprop = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                               stderr=subprocess.PIPE)
+    out, err = svcprop.communicate()
+    retcode = svcprop.wait()
+    if retcode != 0:
+        print "Error reading 'vnc/nova-enabled' property: " + err
+        return smf_include.SMF_EXIT_ERR_FATAL
+    enabled = out.strip() == 'true'
+    if not enabled:
+        smf_include.smf_method_exit(
+            smf_include.SMF_EXIT_TEMP_DISABLE,
+            "nova_enabled",
+            "nova-compute starts this service on demand")
+
     check_vncserver()
     homedir = os.environ.get('HOME')
     if not homedir:
         homedir = pwd.getpwuid(os.getuid()).pw_dir
         os.putenv("HOME", homedir)
     set_xstartup(homedir)
+    display = None
+    vncport = None
 
     try:
-        fmri = os.environ['SMF_FMRI']
         zonename = fmri.rsplit(':', 1)[1]
         os.putenv("ZONENAME", zonename)
         desktop_name = zonename + ' console'
-        # NOTE: 'geometry' below is that which matches the size of standard
-        # 80 character undecorated xterm window using font style specified in
-        # XTERMOPTS. The geometry doesn't matter too much because the display
-        # will be resized using xrandr once the xterm geometry is established.
-        cmd = [VNCSERVER, "-name", desktop_name, "-SecurityTypes=None",
-               "-geometry", "964x580", "-localhost", "-autokill"]
-        vnc = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-                    env=None)
-        out, err = vnc.communicate()
-        vncret = vnc.wait()
+        novacfg = ConfigParser.RawConfigParser()
+        novacfg.readfp(open(NOVACFG))
+        try:
+            vnc_listenip = novacfg.get("DEFAULT", "vncserver_listen")
+        except ConfigParser.NoOptionError:
+            vnc_listenip = CONF.vncserver_listen
+
+        with lock_available_port(vnc_listenip, VNCPORT_START, VNCPORT_END,
+                                 homedir) as n:
+            # NOTE: 'geometry' is that which matches the size of standard
+            # 80 character undecorated xterm window using font style specified
+            # in XTERMOPTS. The geometry doesn't matter too much because the
+            # display will be resized using xrandr once the xterm geometry is
+            # established.
+            display = ":%d" % n
+            cmd = [VNCSERVER, display, "-name", desktop_name,
+                   "-SecurityTypes=None", "-geometry", "964x580",
+                   "-interface", vnc_listenip, "-autokill"]
+
+            vncport = VNCPORT_START + n
+            x11port = X11PORT_START + n
+            print "Using VNC server port: " + str(vncport)
+            print "Using X11 server port: %d, display %s" % (x11port, display)
+            vnc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE, env=None)
+            out, err = vnc.communicate()
+            vncret = vnc.wait()
         if vncret != 0:
             print "Error starting VNC server: " + err
             return smf_include.SMF_EXIT_ERR_FATAL
@@ -74,25 +126,17 @@
         print e
         return smf_include.SMF_EXIT_ERR_FATAL
 
-    output = err.splitlines()
-    for line in output:
-        if line.startswith("New '%s' desktop is" % desktop_name):
-            display = line.rpartition(' ')[2]
-            host, display_num = display.split(':', 1)
-            # set host prop
-            port = 5900 + int(display_num)
-            print "VNC port: %d" % port
-            # set port num prop
-            cmd = [SVCCFG, '-s', fmri, 'setprop', 'vnc/port', '=', 'integer:',
-                   str(port)]
+    # set SMF instance port num prop
+    cmd = [SVCCFG, '-s', fmri, 'setprop', 'vnc/port', '=', 'integer:',
+           str(vncport)]
 
-            svccfg = subprocess.Popen(cmd, stdout=subprocess.PIPE,
-                                      stderr=subprocess.PIPE)
-            out, err = svccfg.communicate()
-            retcode = svccfg.wait()
-            if retcode != 0:
-                print "Error updating 'vnc/port' property: " + err
-                return smf_include.SMF_EXIT_ERR_FATAL
+    svccfg = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE)
+    out, err = svccfg.communicate()
+    retcode = svccfg.wait()
+    if retcode != 0:
+        print "Error updating 'vnc/port' property: " + err
+        return smf_include.SMF_EXIT_ERR_FATAL
     resize_xserver(display, zonename)
 
     return smf_include.SMF_EXIT_OK
@@ -101,8 +145,8 @@
 def stop():
     try:
         # first kill the SMF contract
-        check_call(["/usr/bin/pkill", "-c", sys.argv[2]])
-    except CalledProcessError as cpe:
+        subprocess.check_call(["/usr/bin/pkill", "-c", sys.argv[2]])
+    except subprocess.CalledProcessError as cpe:
         # 1 is returncode if no SMF contract processes were matched,
         # meaning they have already terminated.
         if cpe.returncode != 1:
@@ -151,7 +195,7 @@
 
     # Always clobber xstartup
     # stemp tuple = [fd, path]
-    stemp = mkstemp(dir=vncdir)
+    stemp = tempfile.mkstemp(dir=vncdir)
     os.write(stemp[0], XSTARTUP)
     os.close(stemp[0])
     os.chmod(stemp[1], 0700)
@@ -293,6 +337,64 @@
         print "Error setting new xrandr modeline for VNC display: " + err
         return
 
+
[email protected]
+def lock_available_port(address, port_start, port_end, lockdir):
+    """Ensures instance exclusive use of VNC, X11 service ports
+       and related resources.
+       Generator yields an integer of the port relative to port_start to use.
+       eg. 32: VNC port 5932, X11 port 6032, X11 display :32
+       lockfile is port specific and prevents multiple instances from
+       attempting to use the same port number during SMF start method
+       execution.
+       Socket binding on address:port establishes that the port is not
+       already in use by another Xvnc process
+    """
+    for n in range(port_end - port_start):
+        vncport = port_start + n
+        x11port = X11PORT_START + n
+        lockfile = os.path.join(lockdir, '.port-%d.lock' % vncport)
+        try:
+            # Acquire port file lock first to lock out other instances trying
+            # to come online in parallel. They will grab the next available
+            # port lock.
+            lock = open(lockfile, 'w')
+            fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
+
+            try:
+                # Check the VNC/RFB and X11 ports.
+                for testport in [vncport, x11port]:
+                    sock = socket.socket(socket.AF_INET)
+                    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+                    try:
+                        sock.bind((address, testport))
+                    finally:
+                        sock.close()
+
+                # Ensure the standard X11 locking files are not present
+                # /tmp/.X<n>-lock
+                # /tmp/X11-unix/X<n>
+                xfiles = ['/tmp/.X%d-lock' % n,
+                          '/tmp/X11-unix/X%d' % n]
+                for xfile in xfiles:
+                    if os.path.exists(xfile):
+                        print ("Warning: X11 display :{0} is taken because of "
+                               "{1}\nRemove this file if there is no X "
+                               "server on display :{0}".format(str(n), xfile))
+                        raise Exception
+
+            except (socket.error, Exception):
+                lock.close()
+                os.remove(lockfile)
+                continue
+            # Yay, we found a free VNC/X11 port pair.
+            yield n
+            lock.close()
+            os.remove(lockfile)
+            break
+        except IOError:
+            print "Port %d already reserved, skipping" % vncport
+
 if __name__ == "__main__":
     os.putenv("LC_ALL", "C")
     smf_include.smf_main()