21038378 Nova VNC console not accessible in multi-node and multi-network configuration s11-update
authorNiall Power <niall.power@oracle.com>
Fri, 10 Jul 2015 15:45:53 -0700
branchs11-update
changeset 4628 21e8147a2b1e
parent 4627 2101fdb9d9aa
child 4629 4991a5f4f6b9
21038378 Nova VNC console not accessible in multi-node and multi-network configuration 21197115 Potential startup race conditition in zone console SMF method zone-vnc-console 20931076 nova doesn't build on s12-72 21197138 Nova VNC console should not allow popup menu access 20662640 console should show scrollbar and provide longer scrollback
components/openstack/nova/Makefile
components/openstack/nova/files/Xresources
components/openstack/nova/files/solariszones/driver.py
components/openstack/nova/files/zone-vnc-console
components/openstack/nova/files/zone-vnc-console.xml
components/openstack/nova/nova.p5m
--- a/components/openstack/nova/Makefile	Fri Jul 10 10:09:35 2015 -0700
+++ b/components/openstack/nova/Makefile	Fri Jul 10 15:45:53 2015 -0700
@@ -115,6 +115,5 @@
 REQUIRED_PACKAGES += web/novnc
 REQUIRED_PACKAGES += x11/diagnostic/x11-info-clients
 REQUIRED_PACKAGES += x11/modeline-utilities
-REQUIRED_PACKAGES += x11/server/xorg
 REQUIRED_PACKAGES += x11/server/xvnc
 REQUIRED_PACKAGES += x11/x11-server-utilities
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/nova/files/Xresources	Fri Jul 10 15:45:53 2015 -0700
@@ -0,0 +1,17 @@
+! WARNING: THIS FILE IS INSTALLED BY pkg(1)
+! DO NOT EDIT THIS FILE.  EDITS WILL BE LOST.
+! Override xterm(1) defaults appropriately for zone VNC console
+! scrollback buffer
+xterm*saveLines:	1000
+! double-click to select whole URLs
+xterm*charClass: 33:48,36-47:48,58-59:48,61:48,63-64:48,95:48,126:48
+! fix that annoying input method error message on startup
+xterm.VT100.openIm:     false
+! display right scroll bar
+xterm.VT100.scrollBar:  true
+xterm.VT100.rightScrollBar:     true
+! prevent xterm popup menu access from Ctrl + mouse buttons 1-3
+xterm.VT100.translations: #override \
+        Ctrl <Btn1Down>: ignore() \n\
+        Ctrl <Btn2Down>: ignore() \n\
+        Ctrl <Btn3Down>: ignore()
--- a/components/openstack/nova/files/solariszones/driver.py	Fri Jul 10 10:09:35 2015 -0700
+++ b/components/openstack/nova/files/solariszones/driver.py	Fri Jul 10 15:45:53 2015 -0700
@@ -38,7 +38,7 @@
 
 from eventlet import greenthread
 from lxml import etree
-from oslo.config import cfg
+from oslo_config import cfg
 
 from nova.compute import power_state
 from nova.compute import task_states
@@ -76,6 +76,7 @@
 
 CONF = cfg.CONF
 CONF.register_opts(solariszones_opts)
+CONF.import_opt('vncserver_proxyclient_address', 'nova.vnc')
 LOG = logging.getLogger(__name__)
 
 # These should match the strings returned by the zone_state_str()
@@ -1220,7 +1221,8 @@
         # TODO(npower): investigate using RAD instead of CLI invocation
         try:
             out, err = utils.execute('/usr/sbin/svccfg', '-s',
-                                     VNC_CONSOLE_BASE_FMRI, 'delete', name)
+                                     VNC_CONSOLE_BASE_FMRI, 'delete', '-f',
+                                     name)
         except processutils.ProcessExecutionError as err:
             if not self._has_vnc_console_service(instance):
                 LOG.debug(_("Ignoring attempt to delete a non-existent zone "
@@ -1240,11 +1242,18 @@
         console_fmri = VNC_CONSOLE_BASE_FMRI + ':' + name
         # TODO(npower): investigate using RAD instead of CLI invocation
         try:
+            # The console SMF service exits with SMF_TEMP_DISABLE to prevent
+            # unnecessarily coming online at boot. Tell it to really bring
+            # it online.
+            out, err = utils.execute('/usr/sbin/svccfg', '-s', console_fmri,
+                                     'setprop', 'vnc/nova-enabled=true')
+            out, err = utils.execute('/usr/sbin/svccfg', '-s', console_fmri,
+                                     'refresh')
             out, err = utils.execute('/usr/sbin/svcadm', 'enable',
                                      console_fmri)
         except processutils.ProcessExecutionError as err:
             if not self._has_vnc_console_service(instance):
-                LOG.error(_("Ignoring attempt to enable a non-existent zone "
+                LOG.debug(_("Ignoring attempt to enable a non-existent zone "
                             "VNC console SMF service for instance '%s'")
                           % name)
             LOG.error(_("Unable to start zone VNC console SMF service "
@@ -1273,6 +1282,19 @@
                 LOG.error(_("Error querying state of zone VNC console SMF "
                             "service '%s': %s") % (console_fmri, err))
                 raise
+        # TODO(npower): investigate using RAD instead of CLI invocation
+        try:
+            # The console SMF service exits with SMF_TEMP_DISABLE to prevent
+            # unnecessarily coming online at boot. Make that happen.
+            out, err = utils.execute('/usr/sbin/svccfg', '-s', console_fmri,
+                                     'setprop', 'vnc/nova-enabled=false')
+            out, err = utils.execute('/usr/sbin/svccfg', '-s', console_fmri,
+                                     'refresh')
+        except processutils.ProcessExecutionError as err:
+            LOG.error(_("Unable to update 'vnc/nova-enabled' property for "
+                        "zone VNC console SMF service "
+                        "'%s': %s") % (console_fmri, err))
+            raise
 
     def _disable_vnc_console_service(self, instance):
         """Disable a zone VNC console SMF service"""
@@ -1677,11 +1699,12 @@
                         "'%s': %s" % (console_fmri, err)))
             raise
 
+        host = CONF.vncserver_proxyclient_address
         try:
             out, err = utils.execute('/usr/bin/svcprop', '-p', 'vnc/port',
                                      console_fmri)
             port = int(out.strip())
-            return ctype.ConsoleVNC(host='127.0.0.1',
+            return ctype.ConsoleVNC(host=host,
                                     port=port,
                                     internal_access_path=None)
         except processutils.ProcessExecutionError as err:
--- a/components/openstack/nova/files/zone-vnc-console	Fri Jul 10 10:09:35 2015 -0700
+++ b/components/openstack/nova/files/zone-vnc-console	Fri Jul 10 15:45:53 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()
--- a/components/openstack/nova/files/zone-vnc-console.xml	Fri Jul 10 10:09:35 2015 -0700
+++ b/components/openstack/nova/files/zone-vnc-console.xml	Fri Jul 10 15:45:53 2015 -0700
@@ -70,6 +70,8 @@
         override='true'/>
       <propval name='port' type='integer' value='0'
         override='true'/>
+      <propval name='nova-enabled' type='boolean' value='false'
+        override='true'/>
       <propval name='action_authorization' type='astring'
         value='solaris.smf.manage.nova' />
       <propval name='value_authorization' type='astring'
--- a/components/openstack/nova/nova.p5m	Fri Jul 10 10:09:35 2015 -0700
+++ b/components/openstack/nova/nova.p5m	Fri Jul 10 15:45:53 2015 -0700
@@ -850,6 +850,7 @@
 file path=usr/lib/python$(PYVER)/vendor-packages/nova/weights.py
 file path=usr/lib/python$(PYVER)/vendor-packages/nova/wsgi.py
 dir  path=var/lib/nova owner=nova group=nova mode=0700
+file files/Xresources path=var/lib/nova/.Xresources owner=nova group=nova
 #
 group groupname=nova gid=85
 user username=nova ftpuser=false gcos-field="OpenStack Nova" group=nova \