4623N/A#!/usr/bin/python2.7
3809N/A
4070N/A# Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
4070N/A#
4070N/A# Licensed under the Apache License, Version 2.0 (the "License"); you may
4070N/A# not use this file except in compliance with the License. You may obtain
4070N/A# a copy of the License at
4070N/A#
4070N/A# http://www.apache.org/licenses/LICENSE-2.0
4070N/A#
4070N/A# Unless required by applicable law or agreed to in writing, software
4070N/A# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
4070N/A# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
4070N/A# License for the specific language governing permissions and limitations
4070N/A# under the License.
4070N/A
4626N/Aimport ConfigParser
4626N/Aimport contextlib
3809N/Aimport errno
4626N/Aimport fcntl
3809N/Aimport os
3809N/Aimport pwd
3809N/Aimport smf_include
4626N/Aimport socket
3809N/Aimport subprocess
3809N/Aimport sys
4626N/Aimport tempfile
4070N/Aimport time
3809N/A
4626N/Afrom oslo_config import cfg
3809N/A
4070N/AGTF = "/usr/bin/gtf"
4626N/ASVCADM = "/usr/sbin/svcadm"
3809N/ASVCCFG = "/usr/sbin/svccfg"
3809N/ASVCPROP = "/usr/bin/svcprop"
3809N/AVNCSERVER = "/usr/bin/vncserver"
4070N/AXRANDR = "/usr/bin/xrandr"
4626N/ANOVACFG = "/etc/nova/nova.conf"
3809N/AXSTARTUPHDR = "# WARNING: THIS FILE GENERATED BY SMF.\n" + \
3809N/A "# DO NOT EDIT THIS FILE. EDITS WILL BE LOST.\n"
4070N/AXRESOURCES = "[[ -f ~/.Xresources ]] && /usr/bin/xrdb -merge ~/.Xresources\n"
3809N/AXTERM = "/usr/bin/xterm"
4070N/A# Borderless, Monospsce font, point size 14, white foreground on black
4070N/A# background are reasonable defaults.
4070N/AXTERMOPTS = ' -b 0 -fa Monospace -fs 14 -fg white -bg black -title ' + \
4070N/A '"Zone Console: $ZONENAME"'
4070N/AXWININFO = "/usr/bin/xwininfo"
4626N/A
4626N/A# Port ranges allocated for VNC and X11 sockets.
4626N/AVNCPORT_START = 5900
4626N/AVNCPORT_END = 5999
4626N/AX11PORT_START = 6000
4626N/A
3809N/A# Enclose command in comments to prevent xterm consuming zlogin opts
3809N/AZLOGINOPTS = ' -e "/usr/bin/pfexec /usr/sbin/zlogin -C -E $ZONENAME"\n'
4070N/AXSTARTUP = XSTARTUPHDR + XRESOURCES + XTERM + XTERMOPTS + ZLOGINOPTS
3809N/A
4626N/ACONF = cfg.CONF
4626N/ACONF.import_opt('vncserver_listen', 'nova.vnc')
4626N/A
3809N/A
3809N/Adef start():
4626N/A fmri = os.environ['SMF_FMRI']
4626N/A # This is meant to be an on-demand service.
4626N/A # Determine if nova-compute requested enablement of this instance.
4626N/A # Exit with SMF_EXIT_TEMP_DISABLE if not true.
4626N/A cmd = [SVCPROP, '-p', 'vnc/nova-enabled', fmri]
4626N/A svcprop = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4626N/A stderr=subprocess.PIPE)
4626N/A out, err = svcprop.communicate()
4626N/A retcode = svcprop.wait()
4626N/A if retcode != 0:
4626N/A print "Error reading 'vnc/nova-enabled' property: " + err
4626N/A return smf_include.SMF_EXIT_ERR_FATAL
4626N/A enabled = out.strip() == 'true'
4626N/A if not enabled:
4626N/A smf_include.smf_method_exit(
4626N/A smf_include.SMF_EXIT_TEMP_DISABLE,
4626N/A "nova_enabled",
4626N/A "nova-compute starts this service on demand")
4626N/A
3809N/A check_vncserver()
3809N/A homedir = os.environ.get('HOME')
3809N/A if not homedir:
3809N/A homedir = pwd.getpwuid(os.getuid()).pw_dir
3809N/A os.putenv("HOME", homedir)
3809N/A set_xstartup(homedir)
4626N/A display = None
4626N/A vncport = None
3809N/A
3809N/A try:
3809N/A zonename = fmri.rsplit(':', 1)[1]
3809N/A os.putenv("ZONENAME", zonename)
3809N/A desktop_name = zonename + ' console'
4626N/A novacfg = ConfigParser.RawConfigParser()
4626N/A novacfg.readfp(open(NOVACFG))
4626N/A try:
4626N/A vnc_listenip = novacfg.get("DEFAULT", "vncserver_listen")
4626N/A except ConfigParser.NoOptionError:
4626N/A vnc_listenip = CONF.vncserver_listen
4626N/A
4626N/A with lock_available_port(vnc_listenip, VNCPORT_START, VNCPORT_END,
4626N/A homedir) as n:
4626N/A # NOTE: 'geometry' is that which matches the size of standard
4626N/A # 80 character undecorated xterm window using font style specified
4626N/A # in XTERMOPTS. The geometry doesn't matter too much because the
4626N/A # display will be resized using xrandr once the xterm geometry is
4626N/A # established.
4626N/A display = ":%d" % n
4626N/A cmd = [VNCSERVER, display, "-name", desktop_name,
4626N/A "-SecurityTypes=None", "-geometry", "964x580",
4626N/A "-interface", vnc_listenip, "-autokill"]
4626N/A
4626N/A vncport = VNCPORT_START + n
4626N/A x11port = X11PORT_START + n
4626N/A print "Using VNC server port: " + str(vncport)
4626N/A print "Using X11 server port: %d, display %s" % (x11port, display)
4626N/A vnc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4626N/A stderr=subprocess.PIPE, env=None)
4626N/A out, err = vnc.communicate()
4626N/A vncret = vnc.wait()
3809N/A if vncret != 0:
3809N/A print "Error starting VNC server: " + err
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A except Exception as e:
3809N/A print e
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A
4626N/A # set SMF instance port num prop
4626N/A cmd = [SVCCFG, '-s', fmri, 'setprop', 'vnc/port', '=', 'integer:',
4626N/A str(vncport)]
3809N/A
4626N/A svccfg = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4626N/A stderr=subprocess.PIPE)
4626N/A out, err = svccfg.communicate()
4626N/A retcode = svccfg.wait()
4626N/A if retcode != 0:
4626N/A print "Error updating 'vnc/port' property: " + err
4626N/A return smf_include.SMF_EXIT_ERR_FATAL
4070N/A resize_xserver(display, zonename)
4070N/A
3809N/A return smf_include.SMF_EXIT_OK
3809N/A
3809N/A
3809N/Adef stop():
3809N/A try:
3809N/A # first kill the SMF contract
4626N/A subprocess.check_call(["/usr/bin/pkill", "-c", sys.argv[2]])
4626N/A except subprocess.CalledProcessError as cpe:
3809N/A # 1 is returncode if no SMF contract processes were matched,
3809N/A # meaning they have already terminated.
3809N/A if cpe.returncode != 1:
3809N/A print "failed to kill the SMF contract: %s" % cpe
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A
3809N/A try:
3809N/A fmri = os.environ['SMF_FMRI']
3809N/A # reset port num prop to initial zero value
3809N/A cmd = [SVCCFG, '-s', fmri, 'setprop', 'vnc/port', '=', 'integer:',
3809N/A '0']
3809N/A svccfg = subprocess.Popen(cmd, stdout=subprocess.PIPE,
3809N/A stderr=subprocess.PIPE,)
3809N/A out, err = svccfg.communicate()
3809N/A retcode = svccfg.wait()
3809N/A if retcode != 0:
3809N/A print "Error resetting 'vnc/port' property: " + err
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A except Exception as e:
3809N/A print e
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A
3809N/A
3809N/Adef check_vncserver():
3809N/A if not os.path.exists(VNCSERVER):
3809N/A print("VNC console service not available on this compute node. "
3809N/A "%s is missing. Run 'pkg install x11/server/xvnc'"
3809N/A % VNCSERVER)
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A if not os.path.exists(XTERM):
3809N/A print("VNC console service not available on this compute node. "
3809N/A "%s is missing. Run 'pkg install terminal/xterm'"
3809N/A % XTERM)
3809N/A return smf_include.SMF_EXIT_ERR_FATAL
3809N/A
3809N/A
3809N/Adef set_xstartup(homedir):
3809N/A vncdir = os.path.join(homedir, '.vnc')
3809N/A xstartup_path = os.path.join(vncdir, 'xstartup')
3809N/A
3809N/A try:
3809N/A os.mkdir(vncdir)
3809N/A except OSError as ose:
3809N/A if ose.errno != errno.EEXIST:
3809N/A raise
3809N/A
3809N/A # Always clobber xstartup
3809N/A # stemp tuple = [fd, path]
4626N/A stemp = tempfile.mkstemp(dir=vncdir)
3809N/A os.write(stemp[0], XSTARTUP)
3809N/A os.close(stemp[0])
3809N/A os.chmod(stemp[1], 0700)
3809N/A os.rename(stemp[1], xstartup_path)
3809N/A
3809N/A
4070N/Adef resize_xserver(display, zonename):
4070N/A """ Try to determine xterm window geometry and resize the Xvnc display
4070N/A to match using XRANDR. Treat failure as non-fatal since an
4070N/A incorrectly sized console is arguably better than none.
4070N/A """
4070N/A class UninitializedWindowError(Exception):
4070N/A pass
4070N/A
4070N/A class UnmappedWindowError(Exception):
4070N/A pass
4070N/A
4070N/A def _get_window_geometry(display, windowname):
4070N/A """ Find the xterm xwindow by name/title and extract its geometry
4070N/A Returns: tuple of window [width, height]
4070N/A Raises:
4070N/A UninitializedWindowError if window not yet initialized
4070N/A UnmappedWindowError if window is not viewable/mapped
4070N/A """
4070N/A cmd = [XWININFO, '-d', display, '-name', windowname]
4070N/A xwininfo = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4070N/A stderr=subprocess.PIPE)
4070N/A out, err = xwininfo.communicate()
4070N/A retcode = xwininfo.wait()
4070N/A if retcode != 0:
4070N/A print "Error finding console xwindow info: " + err
4070N/A raise UninitializedWindowError
4070N/A
4070N/A width = None
4070N/A height = None
4070N/A mapped = False
4070N/A for line in out.splitlines():
4070N/A line = line.strip()
4070N/A if line.startswith("Map State:"):
4070N/A if line.split()[-1] != "IsViewable":
4070N/A # Window is not mapped yet.
4070N/A raise UnmappedWindowError
4070N/A else:
4070N/A mapped = True
4070N/A if line.startswith("Width:"):
4070N/A width = int(line.split()[1])
4070N/A elif line.startswith("Height:"):
4070N/A height = int(line.split()[1])
4070N/A if width and height and mapped:
4070N/A return [width, height]
4070N/A else:
4070N/A # What, no width and height???
4070N/A print "No window geometry info returned by " + XWINFINFO
4070N/A raise UnmappedWindowError
4070N/A
4070N/A retries = 10
4070N/A sleep = 1
4070N/A uninit_count = 0
4070N/A unmap_count = 0
4070N/A width = 0
4070N/A height = 0
4070N/A while uninit_count < retries and unmap_count < retries:
4070N/A try:
4070N/A width, height = _get_window_geometry(display,
4070N/A 'Zone Console: ' + zonename)
4070N/A print "Discovered xterm geometry: %d x %d" % (width, height)
4070N/A break
4070N/A except UninitializedWindowError:
4070N/A if uninit_count < retries:
4070N/A print "xterm window not initialized yet. Retrying in %ds" \
4070N/A % sleep
4070N/A uninit_count += 1
4070N/A time.sleep(sleep)
4070N/A continue
4070N/A else:
4070N/A print "xterm window is taking too long to initialize"
4070N/A break
4070N/A except UnmappedWindowError:
4070N/A if unmap_count < retries:
4070N/A print "Discovered xterm not mapped yet. Retrying in %ds" \
4070N/A % sleep
4070N/A unmap_count += 1
4070N/A time.sleep(sleep)
4070N/A continue
4070N/A else:
4070N/A print "Discovered xterm window is taking too long to map"
4070N/A break
4070N/A else:
4070N/A print "Too many failed attempts to discover xterm window geometry"
4070N/A return
4070N/A
4070N/A # Generate a mode line for width and height, with a refresh of 60.0Hz
4070N/A cmd = [GTF, str(width), str(height), '60.0', '-x']
4070N/A gtf = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4070N/A stderr=subprocess.PIPE)
4070N/A out, err = gtf.communicate()
4070N/A retcode = gtf.wait()
4070N/A if retcode != 0:
4070N/A print "Error creating new modeline for VNC display: " + err
4070N/A return
4070N/A
4070N/A for line in out.splitlines():
4070N/A line = line.strip()
4070N/A if line.startswith('Modeline'):
4070N/A modeline = line.split('Modeline')[1]
4070N/A print "New optimal modeline for Xvnc server: " + modeline
4070N/A mode = modeline.split()
4070N/A break
4070N/A
4070N/A # Create a new mode for the Xvnc server using the modeline generated by gtf
4070N/A cmd = [XRANDR, '-d', display, '--newmode']
4070N/A cmd.extend(mode)
4070N/A newmode = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4070N/A stderr=subprocess.PIPE)
4070N/A out, err = newmode.communicate()
4070N/A retcode = newmode.wait()
4070N/A if retcode != 0:
4070N/A print "Error creating new xrandr modeline for VNC display: " + err
4070N/A return
4070N/A
4070N/A # Add the new mode to the default display output
4070N/A modename = mode[0]
4070N/A cmd = [XRANDR, '-d', display, '--addmode', 'default', modename]
4070N/A addmode = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4070N/A stderr=subprocess.PIPE)
4070N/A out, err = addmode.communicate()
4070N/A retcode = addmode.wait()
4070N/A if retcode != 0:
4070N/A print "Error adding new xrandr modeline for VNC display: " + err
4070N/A return
4070N/A
4070N/A # Activate the new mode on the default display output
4070N/A cmd = [XRANDR, '-d', display, '--output', 'default', '--mode', modename]
4070N/A addmode = subprocess.Popen(cmd, stdout=subprocess.PIPE,
4070N/A stderr=subprocess.PIPE)
4070N/A out, err = addmode.communicate()
4070N/A retcode = addmode.wait()
4070N/A if retcode != 0:
4070N/A print "Error setting new xrandr modeline for VNC display: " + err
4070N/A return
4070N/A
4626N/A
4626N/A@contextlib.contextmanager
4626N/Adef lock_available_port(address, port_start, port_end, lockdir):
4626N/A """Ensures instance exclusive use of VNC, X11 service ports
4626N/A and related resources.
4626N/A Generator yields an integer of the port relative to port_start to use.
4626N/A eg. 32: VNC port 5932, X11 port 6032, X11 display :32
4626N/A lockfile is port specific and prevents multiple instances from
4626N/A attempting to use the same port number during SMF start method
4626N/A execution.
4626N/A Socket binding on address:port establishes that the port is not
4626N/A already in use by another Xvnc process
4626N/A """
4626N/A for n in range(port_end - port_start):
4626N/A vncport = port_start + n
4626N/A x11port = X11PORT_START + n
4626N/A lockfile = os.path.join(lockdir, '.port-%d.lock' % vncport)
4626N/A try:
4626N/A # Acquire port file lock first to lock out other instances trying
4626N/A # to come online in parallel. They will grab the next available
4626N/A # port lock.
4626N/A lock = open(lockfile, 'w')
4626N/A fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
4626N/A
4626N/A try:
4626N/A # Check the VNC/RFB and X11 ports.
4626N/A for testport in [vncport, x11port]:
4626N/A sock = socket.socket(socket.AF_INET)
4626N/A sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
4626N/A try:
4626N/A sock.bind((address, testport))
4626N/A finally:
4626N/A sock.close()
4626N/A
4626N/A # Ensure the standard X11 locking files are not present
4626N/A # /tmp/.X<n>-lock
4626N/A # /tmp/X11-unix/X<n>
4626N/A xfiles = ['/tmp/.X%d-lock' % n,
4626N/A '/tmp/X11-unix/X%d' % n]
4626N/A for xfile in xfiles:
4626N/A if os.path.exists(xfile):
4626N/A print ("Warning: X11 display :{0} is taken because of "
4626N/A "{1}\nRemove this file if there is no X "
4626N/A "server on display :{0}".format(str(n), xfile))
4626N/A raise Exception
4626N/A
4626N/A except (socket.error, Exception):
4626N/A lock.close()
4626N/A os.remove(lockfile)
4626N/A continue
4626N/A # Yay, we found a free VNC/X11 port pair.
4626N/A yield n
4626N/A lock.close()
4626N/A os.remove(lockfile)
4626N/A break
4626N/A except IOError:
4626N/A print "Port %d already reserved, skipping" % vncport
4626N/A
3809N/Aif __name__ == "__main__":
3809N/A os.putenv("LC_ALL", "C")
3809N/A smf_include.smf_main()