This is an automated email from the git hooks/post-receive script. x2go pushed a commit to branch master in repository x2gobroker. commit c571e891e4203eedd1b4f68346e66eb49b09c7b5 Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de> Date: Thu Mar 26 09:57:39 2015 +0100 New feature: x2gobroker-loadchecker daemon. --- bin/x2gobroker-ssh | Bin 10328 -> 10328 bytes debian/control | 33 ++ debian/x2gobroker-agent.postrm | 6 +- debian/x2gobroker-authservice.postrm | 6 +- debian/x2gobroker-daemon.postrm | 6 +- debian/x2gobroker-loadchecker.default | 1 + debian/x2gobroker-loadchecker.init | 1 + debian/x2gobroker-loadchecker.install | 3 + debian/x2gobroker-loadchecker.manpages | 1 + debian/x2gobroker-loadchecker.postinst | 76 +++++ ...ervice.postrm => x2gobroker-loadchecker.postrm} | 8 +- debian/x2gobroker-loadchecker.service | 1 + debian/x2gobroker-wsgi.postrm | 6 +- defaults/python-x2gobroker.default | 3 + defaults/x2gobroker-loadchecker.default | 20 ++ etc/broker/defaults.conf | 3 + etc/broker/x2gobroker-loadchecker-logger.conf | 51 ++++ etc/x2gobroker.conf | 49 +++ init/x2gobroker-loadchecker.init | 104 +++++++ lib/x2gobroker-agent.pl | 49 +++ logrotate/x2gobroker-loadchecker | 14 + man/man8/x2gobroker-authservice.8 | 2 +- man/man8/x2gobroker-loadchecker.8 | 93 ++++++ sbin/x2gobroker-loadchecker | 319 ++++++++++++++++++++ x2gobroker-loadchecker.service | 12 + x2gobroker.spec | 119 ++++++++ x2gobroker/agent.py | 25 ++ x2gobroker/brokers/base_broker.py | 115 +++++++ x2gobroker/defaults.py | 18 ++ x2gobroker/loadchecker.py | 219 ++++++++++++++ 30 files changed, 1346 insertions(+), 17 deletions(-) diff --git a/bin/x2gobroker-ssh b/bin/x2gobroker-ssh index 10f24a3..ad1a492 100755 Binary files a/bin/x2gobroker-ssh and b/bin/x2gobroker-ssh differ diff --git a/debian/control b/debian/control index aca8fce..1a90ed5 100644 --- a/debian/control +++ b/debian/control @@ -115,6 +115,37 @@ Description: X2Go Session Broker (PAM authentication service) This package contains the authentication service against the PAM system. +Package: x2gobroker-loadchecker +Architecture: all +Depends: + ${python:Depends}, + ${misc:Depends}, + adduser, + python, + python-argparse, + python-setproctitle, + python-x2gobroker (>= ${source:Version}), python-x2gobroker (<< ${source:Version}.1~), +Suggests: + x2gobroker-daemon, +Description: X2Go Session Broker (load checker service) + X2Go is a server based computing environment with + - session resuming + - low bandwidth support + - session brokerage support + - client side mass storage mounting support + - client side printing support + - audio support + - authentication by smartcard and USB stick + . + The session broker is a server tool for X2Go that tells your X2Go Client + application in a terminal server cluster what servers and session types are + most appropriate for the user in front of the X2Go terminal. + . + A session broker is most useful in load balanced X2Go server farms. + . + This package contains the load checker service required for broker setups + with dynamic load balancing. + Package: x2gobroker-daemon Architecture: all Depends: @@ -123,6 +154,8 @@ Depends: x2gobroker (>= ${source:Version}), x2gobroker (<< ${source:Version}.1~), Recommends: x2gobroker-authservice, +Suggests: + x2gobroker-loadchecker, Description: X2Go Session Broker (standalone daemon) X2Go is a server based computing environment with - session resuming diff --git a/debian/x2gobroker-agent.postrm b/debian/x2gobroker-agent.postrm index 0739da6..0f5c579 100755 --- a/debian/x2gobroker-agent.postrm +++ b/debian/x2gobroker-agent.postrm @@ -23,15 +23,15 @@ case "$1" in dpkg-statoverride --remove /usr/lib/x2go/x2gobroker-agent fi - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then if dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then dpkg-statoverride --remove /var/log/x2gobroker fi rm -Rf /var/log/x2gobroker fi - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then - # remove user/group x2gobroker from system (if not in use by x2gobroker-daemon, x2gobroker-authservice, x2gobroker-wsgi) + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then + # remove user/group x2gobroker from system (if not in use by x2gobroker-daemon, x2gobroker-authservice, x2gobroker-wsgi, x2gobroker-loadchecker) getent passwd x2gobroker 1>/dev/null && deluser x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker diff --git a/debian/x2gobroker-authservice.postrm b/debian/x2gobroker-authservice.postrm index 851bfa5..6db5213 100755 --- a/debian/x2gobroker-authservice.postrm +++ b/debian/x2gobroker-authservice.postrm @@ -19,15 +19,15 @@ set -e case "$1" in purge) - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-agent ] ; then + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then if dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then dpkg-statoverride --remove /var/log/x2gobroker fi rm -Rf /var/log/x2gobroker fi - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then - # remove user/group x2gobroker from system (only if not in use by x2gobroker-daemon, x2gobroker-agent, x2gobroker-wsgi) + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then + # remove user/group x2gobroker from system (only if not in use by x2gobroker-daemon, x2gobroker-agent, x2gobroker-wsgi, x2gobroker-loadchecker) getent passwd x2gobroker 1>/dev/null && deluser x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker diff --git a/debian/x2gobroker-daemon.postrm b/debian/x2gobroker-daemon.postrm index 5d76a1b..ab50ca0 100755 --- a/debian/x2gobroker-daemon.postrm +++ b/debian/x2gobroker-daemon.postrm @@ -19,15 +19,15 @@ set -e case "$1" in purge) - if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then + if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then if dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then dpkg-statoverride --remove /var/log/x2gobroker fi rm -Rf /var/log/x2gobroker fi - if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then - # remove user/group x2gobroker from system (only if not in use by x2gobroker-agent, x2gobroker-authservice, x2gobroker-wsgi) + if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then + # remove user/group x2gobroker from system (only if not in use by x2gobroker-agent, x2gobroker-authservice, x2gobroker-wsgi, x2gobroker-loadchecker) getent passwd x2gobroker 1>/dev/null && deluser x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker diff --git a/debian/x2gobroker-loadchecker.default b/debian/x2gobroker-loadchecker.default new file mode 120000 index 0000000..25ba295 --- /dev/null +++ b/debian/x2gobroker-loadchecker.default @@ -0,0 +1 @@ +../defaults/x2gobroker-loadchecker.default \ No newline at end of file diff --git a/debian/x2gobroker-loadchecker.init b/debian/x2gobroker-loadchecker.init new file mode 120000 index 0000000..b970565 --- /dev/null +++ b/debian/x2gobroker-loadchecker.init @@ -0,0 +1 @@ +../init/x2gobroker-loadchecker.init \ No newline at end of file diff --git a/debian/x2gobroker-loadchecker.install b/debian/x2gobroker-loadchecker.install new file mode 100644 index 0000000..769abbc --- /dev/null +++ b/debian/x2gobroker-loadchecker.install @@ -0,0 +1,3 @@ +sbin/x2gobroker-loadchecker usr/sbin/ +logrotate/x2gobroker-loadchecker etc/logrotate.d/ +etc/broker/x2gobroker-loadchecker-logger.conf etc/x2go/broker \ No newline at end of file diff --git a/debian/x2gobroker-loadchecker.manpages b/debian/x2gobroker-loadchecker.manpages new file mode 100644 index 0000000..1832989 --- /dev/null +++ b/debian/x2gobroker-loadchecker.manpages @@ -0,0 +1 @@ +man/man8/x2gobroker-loadchecker.8 \ No newline at end of file diff --git a/debian/x2gobroker-loadchecker.postinst b/debian/x2gobroker-loadchecker.postinst new file mode 100755 index 0000000..c2f42ce --- /dev/null +++ b/debian/x2gobroker-loadchecker.postinst @@ -0,0 +1,76 @@ +#!/bin/sh +# postinst script for x2gobroker-loadchecker +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * <postinst> `configure' <most-recently-configured-version> +# * <old-postinst> `abort-upgrade' <new version> +# * <conflictor's-postinst> `abort-remove' `in-favour' <package> +# <new-version> +# * <postinst> `abort-remove' +# * <deconfigured's-postinst> `abort-deconfigure' `in-favour' +# <failed-install-package> <version> `removing' +# <conflicting-package> <version> +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + configure) + + # setup user/group x2gobroker + if ! getent group x2gobroker 1>/dev/null; then + echo "Creating x2gobroker group." 1>&2 + addgroup --system x2gobroker + else + echo "Group x2gobroker already exists." 1>&2 + fi + if ! getent passwd x2gobroker 1>/dev/null; then + echo "Creating x2gobroker user." 1>&2 + adduser --system \ + --disabled-password --disabled-login \ + --shell /bin/bash --group --home /var/lib/x2gobroker x2gobroker + else + echo "User x2gobroker already exists." 1>&2 + + # make sure the home directory exists belongs to x2gobroker:x2gobroker + mkdir -p /var/lib/x2gobroker + chown x2gobroker:x2gobroker /var/lib/x2gobroker -f + + # make sure all settings are appropriate + if getent passwd x2gobroker | grep /dev/null 1>/dev/null 2>/dev/null; then + usermod --home /var/lib/x2gobroker x2gobroker + fi + if getent passwd x2gobroker | grep /bin/false 1>/dev/null 2>/dev/null; then + usermod --shell /bin/bash x2gobroker + fi + fi + + # the x2gobroker-daemon needs special permissions on its log directory + if ! dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then + dpkg-statoverride --add --update x2gobroker adm 2750 /var/log/x2gobroker + fi + + mkdir -p /var/log/x2gobroker && chown x2gobroker:adm /var/log/x2gobroker && chmod 2755 /var/log/x2gobroker + touch /var/log/x2gobroker/authservice.log && chown x2gobroker:adm /var/log/x2gobroker/authservice.log + touch /var/log/x2gobroker/error.log && chown x2gobroker:adm /var/log/x2gobroker/error.log + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" 1>&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/debian/x2gobroker-authservice.postrm b/debian/x2gobroker-loadchecker.postrm similarity index 86% copy from debian/x2gobroker-authservice.postrm copy to debian/x2gobroker-loadchecker.postrm index 851bfa5..588f3a1 100755 --- a/debian/x2gobroker-authservice.postrm +++ b/debian/x2gobroker-loadchecker.postrm @@ -1,5 +1,5 @@ #! /bin/sh -# postrm script for x2gobroker-authservice +# postrm script for x2gobroker-loadchecker # # see: dh_installdeb(1) # summary of how this script can be called: @@ -19,15 +19,15 @@ set -e case "$1" in purge) - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-agent ] ; then + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ]; then if dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then dpkg-statoverride --remove /var/log/x2gobroker fi rm -Rf /var/log/x2gobroker fi - if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ]; then - # remove user/group x2gobroker from system (only if not in use by x2gobroker-daemon, x2gobroker-agent, x2gobroker-wsgi) + if [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-wsgi ] && [ ! -d /usr/share/doc/x2gobroker-authservice ]; then + # remove user/group x2gobroker from system (only if not in use by x2gobroker-daemon, x2gobroker-agent, x2gobroker-wsgi, x2gobroker-authservice) getent passwd x2gobroker 1>/dev/null && deluser x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker diff --git a/debian/x2gobroker-loadchecker.service b/debian/x2gobroker-loadchecker.service new file mode 120000 index 0000000..426059a --- /dev/null +++ b/debian/x2gobroker-loadchecker.service @@ -0,0 +1 @@ +../x2gobroker-authservice.service \ No newline at end of file diff --git a/debian/x2gobroker-wsgi.postrm b/debian/x2gobroker-wsgi.postrm index 4c7c120..1299a7a 100755 --- a/debian/x2gobroker-wsgi.postrm +++ b/debian/x2gobroker-wsgi.postrm @@ -35,15 +35,15 @@ case "$1" in apacheconf_remove - if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-daemon ]; then + if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then if dpkg-statoverride --list /var/log/x2gobroker 1>/dev/null; then dpkg-statoverride --remove /var/log/x2gobroker fi rm -Rf /var/log/x2gobroker fi - if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-daemon ]; then - # remove user/group x2gobroker from system (only if not in use by x2gobroker-agent, x2gobroker-authservice, x2gobroker-daemon) + if [ ! -d /usr/share/doc/x2gobroker-agent ] && [ ! -d /usr/share/doc/x2gobroker-authservice ] && [ ! -d /usr/share/doc/x2gobroker-daemon ] && [ ! -d /usr/share/doc/x2gobroker-loadchecker ]; then + # remove user/group x2gobroker from system (only if not in use by x2gobroker-agent, x2gobroker-authservice, x2gobroker-daemon, x2gobroker-loadchecker) getent passwd x2gobroker 1>/dev/null && deluser x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker getent group x2gobroker 1>/dev/null && delgroup x2gobroker diff --git a/defaults/python-x2gobroker.default b/defaults/python-x2gobroker.default index 73ee9ac..d31c113 100644 --- a/defaults/python-x2gobroker.default +++ b/defaults/python-x2gobroker.default @@ -37,3 +37,6 @@ # The unix socket file for communication between the broker and the authentication service. #X2GOBROKER_AUTHSERVICE_SOCKET=/run/x2gobroker/x2gobroker-authservice.socket + +# The unix socket file for communication between the broker and the authentication service. +#X2GOBROKER_LOADCHECKER_SOCKET=/run/x2gobroker/x2gobroker-loadchecker.socket diff --git a/defaults/x2gobroker-loadchecker.default b/defaults/x2gobroker-loadchecker.default new file mode 100644 index 0000000..b5bcd3e --- /dev/null +++ b/defaults/x2gobroker-loadchecker.default @@ -0,0 +1,20 @@ +# X2Go Session Broker (Load Checker Service) configuration +# SystemV-like init systems + +# For PAM authentication the X2Go Session Broker needs its authentication +# service. The session broker itself runs as a non-privileged user (see below) +# whereas the authentication service must run as super-user root. +# +# If you do not use PAM as authentication mechanism with the X2Go Session Broker, +# you can disable the authentication service here. +START_LOADCHECKER=false + +# Control debug mode (0=disable, 1=enable) of the X2Go Broker Authentication +# Service. +# +# Logging is (by default) written to /var/log/x2gobroker/*log. +# +# This option can also be configured in /etc/default/python-x2go. +# The value configured here overrides the value from python-x2go +# defaults and only sets the x2gobroker-authservice into debug mode. +#X2GOBROKER_DEBUG=0 diff --git a/etc/broker/defaults.conf b/etc/broker/defaults.conf index 01ca667..38dfc2d 100644 --- a/etc/broker/defaults.conf +++ b/etc/broker/defaults.conf @@ -35,6 +35,9 @@ # The unix socket file for communication between the broker and the authentication service. #X2GOBROKER_AUTHSERVICE_SOCKET=/run/x2gobroker/x2gobroker-authservice.socket +# The unix socket file for communication between the broker and the authentication service. +#X2GOBROKER_LOADCHECKER_SOCKET=/run/x2gobroker/x2gobroker-loadchecker.socket + [daemon] # X2Go Session Broker configuration for Debian diff --git a/etc/broker/x2gobroker-loadchecker-logger.conf b/etc/broker/x2gobroker-loadchecker-logger.conf new file mode 100644 index 0000000..e18a143 --- /dev/null +++ b/etc/broker/x2gobroker-loadchecker-logger.conf @@ -0,0 +1,51 @@ +# This file is part of the X2Go Project - http://www.x2go.org +# Copyright (C) 2012-2015 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# +# X2Go Session Broker is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# X2Go Session Broker 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +# WARNING: only modify this file if you _exactly_ know what you are doing!!! + +[loggers] +keys=root,loadchecker + +[logger_root] +level=NOTSET +handlers=stderrHandler + +[handlers] +keys=stderrHandler,loadcheckerFileHandler + +[formatters] +keys=loadcheckerFormatter + +[handler_stderrHandler] +class=StreamHandler +args=(sys.stderr,) + +[logger_loadchecker] +level=DEBUG +handlers=loadcheckerFileHandler +qualname=loadchecker +propagate=0 + +[handler_loadcheckerFileHandler] +class=FileHandler +formatter=loadcheckerFormatter +args=('/var/log/x2gobroker/loadchecker.log',) + +[formatter_loadcheckerFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= diff --git a/etc/x2gobroker.conf b/etc/x2gobroker.conf index 1901673..8d57ae1 100644 --- a/etc/x2gobroker.conf +++ b/etc/x2gobroker.conf @@ -247,6 +247,53 @@ # false in the session profile configuration). #default-portscan-x2goservers = true +# Use load checker for querying X2Go Servers' loads in regular intervals +# +# When load-balancing shall be used, the simplest way to detect "server load" +# is counting the numbers of running and suspended sessions. No extra daemon +# nor service is required for this. +# +# However, simply counting running and suspended sessions per X2Go Server +# as a representative for the server load can be highly inaccurate. A better +# approach is checking each X2Go Server's load in regular intervals by a +# separate daemon (running on the broker host) and querying this load checker +# service before selecting the best server on session startup requests. +# +# The load factor calculation uses this algorithm: +# +# ( memAvail/1000 ) * numCPUs * typeCPUs +# load-factor = -------------------------------------- +# loadavg*100 * numSessions +# +# (memAvail in MByte, typeCPUs in MHz) +# +# The higher the load-factor, the more likely that a server will be chosen +# for the next to be allocated X2Go session. +# +# If you set the default-use-load-checker option here, the queries to the +# x2gobroker-loadchecker daemon will be performed for all broker backends by +# defaults. +# +# The x2gobroker-loadchecker only gets consulted, if: +# +# o if enabled here for all backends +# o or if enabled on a per broker backend basis (see below) +# +# and +# +# o the session profile defines more than one host +# o the session profile does not block queries to the load checker daemon +# on a per profile basis +# +#default-use-load-checker = false + +# If the x2gobroker-loadchecker daemon gets used, define here how +# many seconds to sleep between querying system load from the +# associated X2Go Servers. +# +#load-checker-intervals = 300 + + ### ### Auth Mechs section ### @@ -291,6 +338,7 @@ [broker_inifile] #enable = true #session-profiles = /etc/x2go/broker/x2gobroker-sessionprofiles.conf +#use-load-checker = false #[broker_ldap] -> MUSIC OF THE FUTURE #enable = false @@ -304,4 +352,5 @@ #group-search-filter = (&(objectClass=posifxGroup)(cn=*)) #starttls = false #agent-query-mode = SSH +#use-load-checker = true diff --git a/init/x2gobroker-loadchecker.init b/init/x2gobroker-loadchecker.init new file mode 100755 index 0000000..de8f2d0 --- /dev/null +++ b/init/x2gobroker-loadchecker.init @@ -0,0 +1,104 @@ +#!/bin/sh +# +# Start the X2Go Session Broker PAM Authentication Service +# +# Copyright © 2014 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# Distributable under the terms of the GNU AGPL version 3+. +# +### BEGIN INIT INFO +# Provides: x2gobroker-loadchecker +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: X2Go Session Broker PAM Authentication Service +# Description: PAM authentication service for X2Go Session Broker +### END INIT INFO +# + +set -eu + +LOADCHECKER=/usr/sbin/x2gobroker-loadchecker +test -d /run && RUNDIR=/run || RUNDIR=/var/run +PIDFILE_LOADCHECKER=$RUNDIR/x2gobroker/x2gobroker-loadchecker.pid +DEBIANCONFIG_COMMON=/etc/default/python-x2gobroker +DEBIANCONFIG_LOADCHECKER=/etc/default/x2gobroker-loadchecker + +test -x "$LOADCHECKER" || exit 0 + +START_LOADCHECKER=false +X2GOBROKER_DEBUG=0 +X2GOBROKER_DAEMON_USER='x2gobroker' +X2GOBROKER_DAEMON_GROUP='x2gobroker' +X2GOBROKER_LOADCHECKER_SOCKET="$RUNDIR/x2gobroker/x2gobroker-loadchecker.socket" +test -f $DEBIANCONFIG_COMMON && . $DEBIANCONFIG_COMMON +test -f $DEBIANCONFIG_LOADCHECKER && . $DEBIANCONFIG_LOADCHECKER + +if ! getent passwd $X2GOBROKER_DAEMON_USER 1>/dev/null 2>/dev/null; then + X2GOBROKER_DAEMON_USER=nobody +fi +if ! getent group $X2GOBROKER_DAEMON_GROUP 1>/dev/null 2>/dev/null; then + X2GOBROKER_DAEMON_GROUP=nogroup +fi + +# create PID directory +mkdir -p $RUNDIR/x2gobroker +chown $X2GOBROKER_DAEMON_USER:$X2GOBROKER_DAEMON_GROUP $RUNDIR/x2gobroker +chmod 0770 $RUNDIR/x2gobroker + +export X2GOBROKER_DEBUG +export X2GOBROKER_DAEMON_USER +export X2GOBROKER_DAEMON_GROUP +export X2GOBROKER_LOADCHECKER_SOCKET + +. /lib/lsb/init-functions + +is_true() +{ + case "${1:-}" in + [Yy]es|[Yy]|1|[Tt]|[Tt]rue) return 0;; + *) return 1; + esac +} + +case "${1:-}" in + start) + if [ -f $PIDFILE_LOADCHECKER ]; then + if ps a -u root | grep -v egrep | egrep ".*$(basename $LOADCHECKER).*$X2GOBROKER_LOADCHECKER_SOCKET.*" 1>/dev/null 2>/dev/null; then + log_warning_msg "X2Go Broker Authentication Service already running" + else + log_warning_msg "X2Go Broker Authentication Service: stale PID file ($PIDFILE_LOADCHECKER). Delete it manually!" + fi + START_LOADCHECKER=no + fi + if is_true $START_LOADCHECKER; then + set +e + # once we are here, we have to make sure the loadchecker.socket does not exist + rm -f $X2GOBROKER_LOADCHECKER_SOCKET + # and now we can start the auth service + log_daemon_msg "Starting X2Go Broker Authentication Service" "$(basename $LOADCHECKER)" + start-stop-daemon -b -m -S -p $PIDFILE_LOADCHECKER -x $LOADCHECKER -- -s $X2GOBROKER_LOADCHECKER_SOCKET -o $X2GOBROKER_DAEMON_USER -g $X2GOBROKER_DAEMON_GROUP -p 0660 + log_end_msg $? + set -e + fi + ;; + stop) + if [ -f $PIDFILE_LOADCHECKER ] ; then + log_daemon_msg "Stopping X2Go Broker Authentication Service" "$(basename $LOADCHECKER)" + set +e + start-stop-daemon -K -p $PIDFILE_LOADCHECKER && rm -f $PIDFILE_LOADCHECKER + log_end_msg $? + set -e + fi + ;; + restart|reload|force-reload) + ${0:-} stop + ${0:-} start + ;; + *) + echo "Usage: ${0:-} {start|stop|restart|reload|force-reload}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/lib/x2gobroker-agent.pl b/lib/x2gobroker-agent.pl index 242f78a..9463018 100755 --- a/lib/x2gobroker-agent.pl +++ b/lib/x2gobroker-agent.pl @@ -36,6 +36,7 @@ if ( system("x2goversion x2goserver 1>/dev/null") == 0 ) "listsessions", "findbusyservers", "findbusyservers_by_sessionstats", + "checkload", "getservers", "suspendsession", "terminatesession", @@ -133,6 +134,54 @@ if($mode eq 'ping') exit; } +if ( $mode eq 'checkload' ) { + print "OK\n"; + + # Read the values from /proc/loadavg and combine all three values in a linear + # way and make a percent value out of the result: + open FILE, "< /proc/loadavg" or die return ("Cannot open /proc/loadavg: $!"); + my ($avg1, $avg5, $avg15, undef, undef) = split / /, <FILE>; + close FILE; + my $loadavgXX = ( $avg1 + $avg5 + $avg15 ) * 100/3; + if ( $loadavgXX == 0 ) { + # load may not be ZERO + $loadavgXX = 1; + } + + # calculate total memory vs. free memory + my $memAvail; + open FILE, "< /proc/meminfo" or die return ("Cannot open /proc/meminfo: $!"); + foreach(<FILE>) { + if ( m/^MemAvailable:\s+(\S+)/ ) { + $memAvail = $1/1000; + last; + } + } + close(FILE); + + # check number and type of available CPU cores + my $numCPU = 0; + my $typeCPU; + open FILE, "< /proc/cpuinfo" or die return ("Cannot open /proc/cpuinfo: $!"); + foreach(<FILE>) { + if ( m/model name.*CPU\ \@\ ([\d\.]+)/ ) { + $typeCPU = $1*1000; + $numCPU += 1; + } + } + close(FILE); + + print sprintf 'loadavgXX:%1$d', $loadavgXX; + print "\n"; + print sprintf 'memAvail:%1$d', $memAvail; + print "\n"; + print sprintf 'numCPU:%1$d', $numCPU; + print "\n"; + print sprintf 'typeCPU:%1$d', $typeCPU; + print "\n"; + exit; +} + if($mode eq 'availabletasks') { print "OK\n"; diff --git a/logrotate/x2gobroker-loadchecker b/logrotate/x2gobroker-loadchecker new file mode 100644 index 0000000..e5c8c65 --- /dev/null +++ b/logrotate/x2gobroker-loadchecker @@ -0,0 +1,14 @@ +/var/log/x2gobroker/loadchecker.log { + weekly + missingok + rotate 52 + compress + delaycompress + notifempty + create 640 root adm + su root adm + sharedscripts + postrotate + invoke-rc.d x2gobroker-loadchecker restart > /dev/null + endscript +} diff --git a/man/man8/x2gobroker-authservice.8 b/man/man8/x2gobroker-authservice.8 index eff8569..8981ee6 100644 --- a/man/man8/x2gobroker-authservice.8 +++ b/man/man8/x2gobroker-authservice.8 @@ -31,7 +31,7 @@ Thus, the PAM authentication has been moved into a separate service. The communi between X2Go Session Broker and PAM Authentication Service is handled through a unix domain socket file (\fI<RUNDIR>/x2gobroker/x2gobroker-authservice.socket\fR). .PP -This command is normally started through an init script. +This command is normally started through the host's init system. .SH COMMON OPTIONS \fBx2gobroker-authservice\fR accepts the following common options: .TP diff --git a/man/man8/x2gobroker-loadchecker.8 b/man/man8/x2gobroker-loadchecker.8 new file mode 100644 index 0000000..f025f5e --- /dev/null +++ b/man/man8/x2gobroker-loadchecker.8 @@ -0,0 +1,93 @@ +'\" -*- coding: utf-8 -*- +.if \n(.g .ds T< \\FC +.if \n(.g .ds T> \\F[\n[.fam]] +.de URL +\\$2 \(la\\$1\(ra\\$3 +.. +.if \n(.g .mso www.tmac +.TH x2gobroker-loadchecker 8 "Mar 2015" "Version 0.0.3.x" "X2Go Session Broker" +.SH NAME +x2gobroker-loadchecker \- Load checker service for X2Go Session Broker +.SH SYNOPSIS +'nh +.fi +.ad l +\fBx2gobroker-loadchecker\fR \kx +.if (\nx>(\n(.l/2)) .nr x (\n(.l/5) +'in \n(.iu+\nxu +[ +\fIoptions\fR +] +'in \n(.iu-\nxu +.ad b +'hy +.SH DESCRIPTION +\fBx2gobroker-loadchecker\fR is a service that collects system load metrics from +broker-associated X2Go Servers. +.PP +When load-balancing shall be used, the simplest way to detect "server load" +is counting the numbers of running and suspended sessions. No extra daemon +nor service is required for this. The server with the least amount of sessions +will be selected for starting the next X2Go session. +.PP +However, simply counting running and suspended sessions per X2Go Server +as a representative for the server load can be highly inaccurate. A better +approach is checking each X2Go Server's load in regular intervals by the +\fBx2gobroker-loadchecker\fR daemon (running on the broker host) and querying +the \fBx2gobroker-loadchecker\fR daemon before selecting the best server on +session startup requests. +.PP +The \fBx2gobroker-loadchecker\fR collects server metrics of all +associated X2Go Servers and keeps the latest load factors in RAM. Once +the broker needs load factors of a certain session profile (i.e., of all +servers configured in that session profile), the +\fBx2gobroker-loadchecker\fR delivers that info immediately. The +remembered load factors may not be 100% up-to-date (default: collected +within the last five minutes), but the response time of the load checker +is much faster than a query to all possible X2Go Servers would be. +.PP +The load factor calculation uses this algorithm: +.PP + ( memAvail[MByte]/1000 ) * numCPUs * typeCPUs[MHz] + load-factor = -------------------------------------------------- + loadavg*100 * numSessions +.PP +The higher the load-factor, the more likely that a server will be chosen +for the next to be allocated X2Go session. +.PP +The communication +between X2Go Session Broker and the Load Checker Service is handled through a +unix domain socket file (\fI<RUNDIR>/x2gobroker/x2gobroker-loadchecker.socket\fR). +.PP +This command is normally started through the host's init system. +.SH COMMON OPTIONS +\fBx2gobroker-loadchecker\fR accepts the following common options: +.TP +\*(T<\fB\-h, \-\-help\fR\*(T> +Display a help with all available command line options and exit. +.TP +\*(T<\fB\-D, \-\-daemonize\fR\*(T> +Fork this application to background and detach from the running terminal. +.TP +\*(T<\fB\-P, \-\-pidfile\fR\*(T> +Custom PID file location when daemonizing (default: \fI<RUNDIR>/x2gobroker/x2gobroker-loadchecker.pid\fR). +.TP +\*(T<\fB\-L, \-\-logdir\fR\*(T> +Directory where stdout/stderr will be redirected after having daemonized (default: \fI/var/log/x2gobroker/\fR). +.TP +\*(T<\fB\-s <LOADCHECKERSOCKET>, \-\-socket <LOADCHECKERSOCKET>\fR\*(T> +File name of the unix domain socket file used for communication between broker and load checker service. +.TP +\*(T<\fB\-o <OWNER>, \-\-owner <OWNER>\fR\*(T> +User ownership of the \fI<LOADCHECKERSOCKET>\fR file. +.TP +\*(T<\fB\-g <GROUP>, \-\-group <GROUP>\fR\*(T> +Group ownership of the \fI<LOADCHECKERSOCKET>\fR file. +.TP +\*(T<\fB\-p <PERMISSIONS>, \-\-permissions <PERMISSIONS>\fR\*(T> +Set these file permissions for the \fI<LOADCHECKERSOCKET>\fR file. Use numerical permissions (e.g. 0640). +.SH "FILES" +<RUNDIR>/x2gobroker/x2gobroker-loadchecker.socket +.SH AUTHOR +This manual has been written for the X2Go project by +Mike Gabriel <mike.gabriel@das-netzwerkteam.de>. diff --git a/sbin/x2gobroker-loadchecker b/sbin/x2gobroker-loadchecker new file mode 100755 index 0000000..8d6ae32 --- /dev/null +++ b/sbin/x2gobroker-loadchecker @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the X2Go Project - http://www.x2go.org +# Copyright (C) 2012-2015 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# +# X2Go Session Broker is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# X2Go Session Broker 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +import os +import sys +import setproctitle +import argparse +import logging +import asyncore +import socket +import getpass +import logging.config +import atexit +import ConfigParser + +if os.path.isdir('/run'): + RUNDIR = '/run' +else: + RUNDIR = '/var/run' + +try: + import daemon + import lockfile + CAN_DAEMONIZE = True + pidfile = '{run}/x2gobroker/x2gobroker-loadchecker.pid'.format(run=RUNDIR) + daemon_logdir = '/var/log/x2gobroker/' +except ImportError: + CAN_DAEMONIZE = False + +from pwd import getpwnam +from grp import getgrnam + +PROG_NAME = os.path.basename(sys.argv[0]) +PROG_OPTIONS = sys.argv[1:] +setproctitle.setproctitle("%s %s" % (PROG_NAME, " ".join(PROG_OPTIONS))) + +from x2gobroker import __VERSION__ +from x2gobroker import __AUTHOR__ +import x2gobroker.loadchecker + +global load_checker + +class LoadCheckerServiceHandler(asyncore.dispatcher_with_send): + + def __init__(self, sock, logger=None): + self.logger = logger + asyncore.dispatcher_with_send.__init__(self, sock) + self._buf = '' + + def handle_read(self): + data = self._buf + self.recv(1024) + if not data: + self.close() + return + reqs, data = data.rsplit('\n', 1) + self._buf = data + output = "" + for req in reqs.split('\n'): + backend, profile_id, hostname = req.split('\r') + if self.logger: self.logger.debug('LoadCheckServiceHandler.handle_read(): received load check query: backend={backend}, profile_id={profile_id}, hostname={hostname}'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + if hostname: + load_factor = load_checker.get_server_load(backend, profile_id, hostname) + if load_factor is not None: + output += "{lf}".format(lf=load_factor) + if self.logger: self.logger.info('LoadCheckServiceHandler.handle_read(): load check result for backend={backend}, profile_id={profile_id}, hostname={hostname}: {lf}'.format(backend=backend, profile_id=profile_id, hostname=hostname, lf=load_factor)) + else: + output += "LOAD-UNAVAILABLE" + if self.logger: self.logger.warning('LoadCheckServiceHandler.handle_read(): load check failure for backend={backend}, profile_id={profile_id}, hostname={hostname}: LOAD-UNAVAILABLE'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + else: + load_factors = load_checker.get_profile_load(backend, profile_id) + if load_factors: + for h in load_factors.keys(): + if load_factors[h] is not None: + output +="{hostname}:{loadfactor}".format(hostname=h, loadfactor=load_factors[h]) + if self.logger: self.logger.info('LoadCheckServiceHandler.handle_read(): load check result for backend={backend}, profile_id={profile_id}, hostname={hostname}: {lf}'.format(backend=backend, profile_id=profile_id, hostname=h, lf=load_factors[h])) + else: + output += "{hostname}:LOAD-UNAVAILABLE".formate(hostname=h) + if self.logger: self.logger.warning('LoadCheckServiceHandler.handle_read(): load check failure for backend={backend}, profile_id={profile_id}, hostname={hostname}: LOAD-UNAVAILABLE'.format(backend=backend, profile_id=profile_id, hostname=h)) + else: + output += "LOAD-UNAVAILABLE" + if self.logger: self.logger.warning('LoadCheckServiceHandler.handle_read(): load check failure for backend={backend}, profile_id={profile_id}: LOAD-UNAVAILABLE'.format(backend=backend, profile_id=profile_id)) + self.send(output) + + def handle_close(self): + self.close() + + +class LoadCheckerService(asyncore.dispatcher_with_send): + + def __init__(self, socketfile, owner='root', group_owner='root', permissions='0660', logger=None): + self.logger = logger + asyncore.dispatcher_with_send.__init__(self) + self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind(socketfile) + os.chown(socketfile, getpwnam(owner).pw_uid, getgrnam(group_owner).gr_gid) + os.chmod(socketfile, int(permissions, 8)) + self.listen(1) + + def handle_accept(self): + conn, _ = self.accept() + LoadCheckerServiceHandler(conn, logger=self.logger) + + +def loop(): + ### the "loop" has two tasks... + + # 1. Do regular queries to remote X2Go Broker Agent instances to collect + # load average, CPU usage and type, memory usage, etc. + load_checker.start() + + # 2. Provide a listening UNIX domain socket file that can be used for querying + # server states. + asyncore.loop() + + +def cleanup_on_exit(): + os.remove(X2GOBROKER_LOADCHECKER_SOCKET) + try: os.remove(pidfile) + except: pass + + +# load the defaults.conf file, if present +iniconfig_loaded = None +iniconfig_section = '-'.join(PROG_NAME.split('-')[1:]) +X2GOBROKER_DEFAULTS = "/etc/x2go/broker/defaults.conf" +if os.path.isfile(X2GOBROKER_DEFAULTS) and os.access(X2GOBROKER_DEFAULTS, os.R_OK): + iniconfig = ConfigParser.SafeConfigParser() + iniconfig.optionxform = str + iniconfig_loaded = iniconfig.read(X2GOBROKER_DEFAULTS) + +# normally this would go into defaults.py, however, we do not want to pull in defaults.py here as that will create +# unwanted logfiles (access.log, broker.log, error.log) when x2gobroker-loadchecker is installed as standalone service +if os.environ.has_key('X2GOBROKER_DEBUG'): + X2GOBROKER_DEBUG = ( os.environ['X2GOBROKER_DEBUG'].lower() in ('1', 'on', 'true', 'yes', ) ) +elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_DEBUG'): + X2GOBROKER_DEBUG=iniconfig.get(iniconfig_section, 'X2GOBROKER_DEBUG') +elif iniconfig_loaded and iniconfig.has_option('common', 'X2GOBROKER_DEBUG'): + X2GOBROKER_DEBUG=iniconfig.get('common', 'X2GOBROKER_DEBUG') +else: + X2GOBROKER_DEBUG = False + +if os.environ.has_key('X2GOBROKER_DAEMON_USER'): + X2GOBROKER_DAEMON_USER=os.environ['X2GOBROKER_DAEMON_USER'] +elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_DAEMON_USER'): + X2GOBROKER_DAEMON_USER=iniconfig.get(iniconfig_section, 'X2GOBROKER_DAEMON_USER') +elif iniconfig_loaded and iniconfig.has_option('common', 'X2GOBROKER_DAEMON_USER'): + X2GOBROKER_DAEMON_USER=iniconfig.get('common', 'X2GOBROKER_DAEMON_USER') +else: + X2GOBROKER_DAEMON_USER="x2gobroker" + +if os.environ.has_key('X2GOBROKER_LOADCHECKER_LOGCONFIG'): + X2GOBROKER_LOADCHECKER_LOGCONFIG=os.environ['X2GOBROKER_LOADCHECKER_LOGCONFIG'] +elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_LOADCHECKER_LOGCONFIG'): + X2GOBROKER_LOADCHECKER_LOGCONFIG=iniconfig.get(iniconfig_section, 'X2GOBROKER_LOADCHECKER_LOGCONFIG') +elif iniconfig_loaded and iniconfig.has_option('common', 'X2GOBROKER_LOADCHECKER_LOGCONFIG'): + X2GOBROKER_LOADCHECKER_LOGCONFIG=iniconfig.get('common', 'X2GOBROKER_LOADCHECKER_LOGCONFIG') +else: + X2GOBROKER_LOADCHECKER_LOGCONFIG="/etc/x2go/broker/x2gobroker-loadchecker-logger.conf" + +if os.environ.has_key('X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=os.environ['X2GOBROKER_LOADCHECKER_SOCKET'] +elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=iniconfig.get(iniconfig_section, 'X2GOBROKER_LOADCHECKER_SOCKET') +elif iniconfig_loaded and iniconfig.has_option('common', 'X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=iniconfig.get('common', 'X2GOBROKER_LOADCHECKER_SOCKET') +else: + X2GOBROKER_LOADCHECKER_SOCKET="{run}/x2gobroker/x2gobroker-loadchecker.socket".format(run=RUNDIR) + + +if __name__ == '__main__': + + common_options = [ + {'args':['-s','--socket-file'], 'default': X2GOBROKER_LOADCHECKER_SOCKET, 'metavar': 'LOADCHECKERSOCKET', 'help': 'socket file for LoadChecker communication', }, + {'args':['-o','--owner'], 'default': 'root', 'help': 'owner of the LoadChecker socket file', }, + {'args':['-g','--group'], 'default': 'root', 'help': 'group ownership of the LoadChecker socket file', }, + {'args':['-p','--permissions'], 'default': '0660', 'help': 'set these file permissions for the LoadChecker socket file', }, + {'args':['-d','--debug'], 'default': False, 'action': 'store_true', 'help': 'enable debugging code', }, + {'args':['-i','--debug-interactively'], 'default': False, 'action': 'store_true', 'help': 'force output of log message to the stderr (rather than to the log files)', }, + + ] + if CAN_DAEMONIZE: + common_options.extend([ + {'args':['-D', '--daemonize'], 'default': False, 'action': 'store_true', 'help': 'Detach the X2Go Broker process from the current terminal and fork to background', }, + {'args':['-P', '--pidfile'], 'default': pidfile, 'help': 'Alternative file path for the daemon\'s PID file', }, + {'args':['-L', '--logdir'], 'default': daemon_logdir, 'help': 'Directory where log files for the process\'s stdout and stderr can be created', }, + ]) + p = argparse.ArgumentParser(description='X2Go Session Broker (Load Checker Service)',\ + formatter_class=argparse.RawDescriptionHelpFormatter, \ + add_help=True, argument_default=None) + p_common = p.add_argument_group('common parameters') + + for (p_group, opts) in ( (p_common, common_options), ): + for opt in opts: + args = opt['args'] + del opt['args'] + p_group.add_argument(*args, **opt) + + cmdline_args = p.parse_args() + + # standalone daemon mode (x2gobroker-loadchecker as daemon) or interactive mode (called from the cmdline)? + if getpass.getuser() in (X2GOBROKER_DAEMON_USER, 'root') and not cmdline_args.debug_interactively: + + # we run in standalone daemon mode, so let's use the system configuration for logging + logging.config.fileConfig(X2GOBROKER_LOADCHECKER_LOGCONFIG) + + # create loadchecker logger + logger_loadchecker = logging.getLogger('loadchecker') + + else: + logger_root = logging.getLogger() + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setFormatter(logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='')) + + # all loggers stream to stderr... + logger_root.addHandler(stderr_handler) + + logger_loadchecker = logging.getLogger('loadchecker') + logger_loadchecker.addHandler(stderr_handler) + logger_loadchecker.propagate = 0 + + if cmdline_args.debug_interactively: + cmdline_args.debug = True + + # raise log level to DEBUG if requested... + if cmdline_args.debug or X2GOBROKER_DEBUG: + X2GOBROKER_DEBUG = True + logger_loadchecker.setLevel(logging.DEBUG) + + logger_loadchecker.info('X2Go Session Broker ({version}), written by {author}'.format(version=__VERSION__, author=__AUTHOR__)) + logger_loadchecker.info('Setting up the Load Checker service\'s environment...') + logger_loadchecker.info(' X2GOBROKER_DEBUG: {value}'.format(value=X2GOBROKER_DEBUG)) + logger_loadchecker.info(' X2GOBROKER_LOADCHECKER_SOCKET: {value}'.format(value=X2GOBROKER_LOADCHECKER_SOCKET)) + + load_checker = x2gobroker.loadchecker.LoadChecker(logger=logger_loadchecker) + + if CAN_DAEMONIZE and cmdline_args.daemonize: + + # create directory for the PID file + pidfile = os.path.expanduser(cmdline_args.pidfile) + if not os.path.isdir(os.path.dirname(pidfile)): + try: + os.makedirs(os.path.dirname(pidfile)) + except: + pass + if not os.access(os.path.dirname(pidfile), os.W_OK) or (os.path.exists(pidfile) and not os.access(pidfile, os.W_OK)): + print("") + p.print_usage() + print("Insufficent privileges. Cannot create PID file {pidfile} path".format(pidfile=pidfile)) + print("") + sys.exit(-3) + + # create directory for logging + daemon_logdir = os.path.expanduser(cmdline_args.logdir) + if not os.path.isdir(daemon_logdir): + try: + os.makedirs(daemon_logdir) + except: + pass + if not os.access(daemon_logdir, os.W_OK): + print("") + p.print_usage() + print("Insufficent privileges. Cannot create directory for stdout/stderr log files: {logdir}".format(logdir=daemon_logdir)) + print("") + sys.exit(-3) + else: + if not daemon_logdir.endswith('/'): + daemon_logdir += '/' + + socket_file = cmdline_args.socket_file + + if os.path.exists(socket_file): + os.remove(socket_file) + + if not os.path.exists(os.path.dirname(socket_file)): + os.makedirs(os.path.dirname(socket_file)) + + runtimedir_permissions = int(cmdline_args.permissions, 8) + if runtimedir_permissions & 0400: runtimedir_permissions = runtimedir_permissions | 0100 + if runtimedir_permissions & 0040: runtimedir_permissions = runtimedir_permissions | 0010 + if runtimedir_permissions & 0004: runtimedir_permissions = runtimedir_permissions | 0001 + try: + os.chown(os.path.dirname(socket_file), getpwnam(cmdline_args.owner).pw_uid, getpwnam(cmdline_args.group).pw_gid) + os.chmod(os.path.dirname(socket_file), runtimedir_permissions) + except OSError: + pass + + LoadCheckerService(socket_file, owner=cmdline_args.owner, group_owner=cmdline_args.group, permissions=cmdline_args.permissions, logger=logger_loadchecker) + atexit.register(cleanup_on_exit) + try: + if CAN_DAEMONIZE and cmdline_args.daemonize: + keep_fds = [int(fd) for fd in os.listdir('/proc/self/fd') if fd not in (0,1,2) ] + daemon_stdout = file(daemon_logdir+'x2gobroker-loadchecker.stdout', 'w+') + daemon_stderr = file(daemon_logdir+'x2gobroker-loadchecker.stderr', 'w+') + with daemon.DaemonContext(stdout=daemon_stdout, stderr=daemon_stderr, files_preserve=keep_fds, umask=0o027, pidfile=lockfile.FileLock(pidfile), detach_process=True): + file(pidfile, 'w+').write(str(os.getpid())+"\n") + loop() + else: + loop() + except KeyboardInterrupt: + pass diff --git a/x2gobroker-loadchecker.service b/x2gobroker-loadchecker.service new file mode 100644 index 0000000..692e15f --- /dev/null +++ b/x2gobroker-loadchecker.service @@ -0,0 +1,12 @@ +[Unit] +Description=X2Go Session Broker Load Checker Service + +[Service] +User=root +Group=root +Type=forking +ExecStart=/usr/sbin/x2gobroker-loadchecker --socket-file=/run/x2gobroker/x2gobroker-loadchecker.socket --daemonize -o root -g x2gobroker -p 0660 --pidfile=/run/x2gobroker/x2gobroker-loadchecker.pid +PIDFile=/run/x2gobroker/x2gobroker-loadchecker.pid + +[Install] +WantedBy=multi-user.target diff --git a/x2gobroker.spec b/x2gobroker.spec index bcb398f..55b1fdb 100644 --- a/x2gobroker.spec +++ b/x2gobroker.spec @@ -14,6 +14,7 @@ Url: http://www.x2go.org/ Source0: http://code.x2go.org/releases/source/%name/%name-%version.tar.gz Source1: x2gobroker-daemon.init Source2: x2gobroker-authservice.init +Source2: x2gobroker-loadchecker.init Source3: x2gobroker-rpmlintrc %if 0%{?el5} @@ -147,6 +148,46 @@ A session broker is most useful in load balanced X2Go server farms. This package contains the authentication service against the PAM system. +%package loadchecker +Summary: X2Go Session Broker (load checker service) +%if 0%{?suse_version} +Group: Productivity/Networking/Remote Desktop +%else +Group: Applications/Communications +%endif +%if 0%{?suse_version} +Requires: python +%else +Requires: python2 +%endif +Requires: python-argparse +Requires: python-setproctitle +Requires: logrotate +Requires(pre): python-x2gobroker = %{version}-%{release} +%if 0%{?suse_version} +Requires(pre): permissions +%endif + +%description loadchecker +X2Go is a server based computing environment with + - session resuming + - low bandwidth support + - session brokerage support + - client side mass storage mounting support + - client side printing support + - audio support + - authentication by smartcard and USB stick + +The session broker is a server tool for X2Go that tells your X2Go Client +application in a terminal server cluster what servers and session types are +most appropriate for the user in front of the X2Go terminal. + +A session broker is most useful in load balanced X2Go server farms. + +This package contains the load checker service required for broker setups +with dynamic load balancing. + + %package daemon Summary: X2Go Session Broker (standalone daemon) %if 0%{?suse_version} @@ -346,6 +387,8 @@ grep -l -r -E '^#!/usr/bin/env python$' | while read file; do \ %endif sed -i logrotate/x2gobroker-authservice \ -e 's/adm/root/' +sed -i logrotate/x2gobroker-loadchecker \ + -e 's/adm/root/' sed -i logrotate/x2gobroker-daemon \ -e 's/adm/root/' sed -i logrotate/x2gobroker-wsgi \ @@ -379,14 +422,18 @@ ln -s "%_sysconfdir/x2go/x2gobroker-wsgi.apache.vhost" \ mkdir -p %{buildroot}%{_unitdir} install -pm0644 x2gobroker-daemon.service %{buildroot}%{_unitdir} install -pm0644 x2gobroker-authservice.service %{buildroot}%{_unitdir} +install -pm0644 x2gobroker-loadchecker.service %{buildroot}%{_unitdir} rm -f %{buildroot}%{_sysconfdir}/default/x2gobroker-daemon rm -f %{buildroot}%{_sysconfdir}/default/x2gobroker-authservice +rm -f %{buildroot}%{_sysconfdir}/default/x2gobroker-loadchecker rm -f %{buildroot}%{_sysconfdir}/default/python-x2gobroker %else # SysV session cleanup script %if 0%{?el5} rm -f %{buildroot}%{_sysconfdir}/x2go/broker/defaults.conf mkdir -p %{buildroot}%{_initrddir} +install -pm0755 %SOURCE3 \ + "$b/%_initrddir/x2gobroker-loadchecker" install -pm0755 %SOURCE2 \ "$b/%_initrddir/x2gobroker-authservice" install -pm0755 %SOURCE1 \ @@ -395,6 +442,8 @@ install -pm0755 %SOURCE1 \ %if 0%{?el6} || ( 0%{?suse_version} && 0%{?suse_version} < 1140) rm -f %{buildroot}%{_sysconfdir}/x2go/broker/defaults.conf mkdir -p "$b/%_initddir" +install -pm0755 %SOURCE3 \ + "$b/%_initddir/x2gobroker-loadchecker" install -pm0755 %SOURCE2 \ "$b/%_initddir/x2gobroker-authservice" install -pm0755 %SOURCE1 \ @@ -480,6 +529,56 @@ fi %if 0%{?suse_version} >= 1230 +%pre loadchecker +%service_add_pre x2gobroker-loadchecker.service +%endif + +%post loadchecker +%if 0%{?fedora} || 0%{?el7} || 0%{?suse_version} >= 1230 +%if 0%{?suse_version} +%service_add_post x2gobroker-loadchecker.service +%else +%systemd_post x2gobroker-loadchecker.service +%endif +%else +/sbin/chkconfig --add x2gobroker-loadchecker +if [ "$1" -ge "1" ] ; then + /sbin/service x2gobroker-loadchecker condrestart >/dev/null 2>&1 || : +fi +%endif +%if 0%{?suse_version} +%set_permissions %{_localstatedir}/log/x2gobroker + + +%verifyscript loadchecker +%verify_permissions -e %{_localstatedir}/log/x2gobroker +%endif + +%preun loadchecker +%if 0%{?fedora} || 0%{?el7} || 0%{?suse_version} >= 1230 +%if 0%{?suse_version} +%service_del_preun x2gobroker-loadchecker.service +%else +%systemd_preun x2gobroker-loadchecker.service +%endif +%else +if [ "$1" = 0 ]; then + /sbin/service x2gobroker-loadchecker stop >/dev/null 2>&1 + /sbin/chkconfig --del x2gobroker-loadchecker +fi +%endif + +%if 0%{?fedora} || 0%{?el7} || 0%{?suse_version} >= 1230 +%postun loadchecker +%if 0%{?suse_version} +%service_del_postun x2gobroker-loadchecker.service +%else +%systemd_postun x2gobroker-loadchecker.service +%endif +%endif + + +%if 0%{?suse_version} >= 1230 %pre daemon %service_add_pre x2gobroker-daemon.service %endif @@ -620,6 +719,26 @@ fi %attr(02750,x2gobroker,x2gobroker) %_localstatedir/log/x2gobroker +%files loadchecker +%defattr(-,root,root) +%if 0%{?el5} +%_initrddir/x2gobroker-loadchecker +%endif +%if 0%{?el6} +%_initddir/x2gobroker-loadchecker +%endif +%if 0%{?fedora} || 0%{?el7} || 0%{?suse_version} >= 1230 +%{_unitdir}/x2gobroker-loadchecker.service +%endif +%if 0%{?el5} || 0%{?el6} || (0%{?suse_version} && 0%{?suse_version} < 1140) +%config %_sysconfdir/default/x2gobroker-loadchecker +%endif +%config %_sysconfdir/logrotate.d/x2gobroker-loadchecker +%_sbindir/x2gobroker-loadchecker +%_mandir/man8/x2gobroker-loadchecker.8* +%attr(02750,x2gobroker,x2gobroker) %_localstatedir/log/x2gobroker + + %files daemon %defattr(-,root,root) %_bindir/x2gobroker-daemon diff --git a/x2gobroker/agent.py b/x2gobroker/agent.py index 5364825..462f188 100644 --- a/x2gobroker/agent.py +++ b/x2gobroker/agent.py @@ -338,6 +338,31 @@ def find_busy_servers(username, remote_agent=None, **kwargs): tasks['findbusyservers'] = find_busy_servers +def checkload(remote_agent=None, **kwargs): + """\ + Query X2Go Broker Agent for a summary of system load specific + parameters. + + @param remote_agent: information about the remote agent that is to be called. + @type remote_agent: C{dict} + + """ + _success, _load_params = call_broker_agent('foo', task='checkload', remote_agent=remote_agent, **kwargs) + + p = {} + for _param in _load_params: + if ':' in _param: + key, val = _param.split(':', 1) + p[key] = float(val) + + load_factor = None + try: + load_factor = long( ( (p['memAvail']/1000) * p['numCPU'] * p['typeCPU'] * 100 ) / p['loadavgXX'] ) + except KeyError: + pass + + return load_factor + def add_authorized_key(username, pubkey_hash, authorized_keys_file='%h/.x2go/authorized_keys', remote_agent=None, **kwargs): """\ Add a public key hash to the user's authorized_keys file. diff --git a/x2gobroker/brokers/base_broker.py b/x2gobroker/brokers/base_broker.py index fe9ecc5..ad22803 100644 --- a/x2gobroker/brokers/base_broker.py +++ b/x2gobroker/brokers/base_broker.py @@ -708,6 +708,59 @@ class X2GoBroker(object): return unicode(_group_db) + def get_use_load_checker(self): + """\ + Is this broker backend configured to access an X2Go Broker LoadChecker daemon. + + @return: C{True} if there should a load checker daemon running. + @rtype: C{bool} + + """ + _use_load_checker = False + if self.config.has_value('global', 'default-use-load-checker'): + _use_load_checker = self.config.get_value('global', 'default-use-load-checker') or _use_load_checker + + if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker'): + _use_load_checker = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker') or _use_load_checker + + return _use_load_checker + + def use_load_checker(self, profile_id): + """\ + Actually query the load checker daemon for the given session profile ID. + This method will check: + + - broker backend configured to use load checker daemon? + - more than one host configured? + - load checker queries not explicitly disabled in session profile? + + @param profile_id: choose remote agent for this profile ID + @type profile_id: C{unicode} + + @return: C{True} if there is a load checker daemon running. + @rtype: C{bool} + + """ + # only use load checker if... + + # it is enabled for the broker backend... + if self.get_use_load_checker(): + + _profile = self.get_profile(profile_id) + + # it is not explicitly disabled per session profile definition + if _profile and _profile.has_key(u'broker-use-load-checker') and _profile['broker-use-load-checker'] is False: + return False + + # more than one host is defined in the session profile + if len(_profile[u'host']) < 2: + return False + + else: + return False + + return True + def _import_nameservice_module(self, service='libnss'): try: if self.nameservice_module is None: @@ -1046,6 +1099,49 @@ class X2GoBroker(object): return remote_agent + def get_all_remote_agents(self, profile_id): + """\ + Get all remote agents. + + @param profile_id: choose remote agent for this profile ID + @type profile_id: C{unicode} + + @return: C{list} of remote agents for the given profile ID + @rtype: C{list} + + """ + remote_agents = [] + + # no remote agent needed for shadow sessions + if self.is_shadow_profile(profile_id): + return remote_agents + + agent_query_mode = self.get_agent_query_mode(profile_id).upper() + if agent_query_mode == u'SSH' and x2gobroker.agent.has_remote_broker_agent_setup(): + + profile = self.get_profile(profile_id) + server_list = profile[u'host'] + + while server_list: + + remote_agent_hostname = server_list[-1] + remote_agent_hostaddr = remote_agent_hostname + remote_agent_port = profile[u'sshport'] + if profile.has_key('sshport={hostname}'.format(hostname=remote_agent_hostname)): + remote_agent_port = profile["sshport={hostname}".format(hostname=remote_agent_hostname)] + if profile.has_key('host={hostname}'.format(hostname=remote_agent_hostname)): + remote_agent_hostaddr = profile["host={hostname}".format(hostname=remote_agent_hostname)] + + remote_agents.append({ + u'hostname': remote_agent_hostname, + u'hostaddr': remote_agent_hostaddr, + u'port': remote_agent_port, } + ) + + server_list = server_list[0:-1] + + return remote_agents + def is_shadow_profile(self, profile_id): """\ Detect from the session profile, if it defines a desktop sharing (shadow) @@ -1357,6 +1453,25 @@ class X2GoBroker(object): if busy_server not in server_list: del busy_servers[busy_server] + + # dynamic load-balancing + if self.use_load_checker(self, profile_id): + busy_servers_temp = copy.deepcopy(busy_servers) + load_factors = x2gobroker.loadchecker.checkload(self.backend_name, profile_id) + if load_factors: + for busy_server in busy_servers_temp.keys(): + # FIXME: this may need love later for peculiar multi-farm setups with non-FQDN hostnames in session profile configurations + if busy_server in load_factors.keys() and type(load_factors[busy_server]) == types.LongType: + # do the load-factor / relX2GoServerUsage calculation here... + busy_servers_temp[busy_server] = load_factors[busy_server] / busy_servers[busy_server] + else: + # ignore the load checker, results are garbage... + busy_servers_temp = None + break + + if busy_servers_temp is not None: + busy_servers = copy.deepcopy(busy_servers_temp) + busy_server_list = [ (load, server) for server, load in busy_servers.items() ] busy_server_list.sort() diff --git a/x2gobroker/defaults.py b/x2gobroker/defaults.py index c7d4dbf..a6d4830 100644 --- a/x2gobroker/defaults.py +++ b/x2gobroker/defaults.py @@ -131,6 +131,19 @@ else: RUNDIR = '/var/run/x2gobroker' X2GOBROKER_AUTHSERVICE_SOCKET="{run}/x2gobroker/x2gobroker-authservice.socket".format(run=RUNDIR) +if os.environ.has_key('X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=os.environ['X2GOBROKER_LOADCHECKER_SOCKET'] +elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=iniconfig.get(iniconfig_section, 'X2GOBROKER_LOADCHECKER_SOCKET') +elif iniconfig_loaded and iniconfig.has_option('common', 'X2GOBROKER_LOADCHECKER_SOCKET'): + X2GOBROKER_LOADCHECKER_SOCKET=iniconfig.get('common', 'X2GOBROKER_LOADCHECKER_SOCKET') +else: + if os.path.isdir('/run/x2gobroker'): + RUNDIR = '/run' + else: + RUNDIR = '/var/run/x2gobroker' + X2GOBROKER_LOADCHECKER_SOCKET="{run}/x2gobroker/x2gobroker-loadchecker.socket".format(run=RUNDIR) + if os.environ.has_key('X2GOBROKER_DEFAULT_BACKEND'): X2GOBROKER_DEFAULT_BACKEND = os.environ['X2GOBROKER_DEFAULT_BACKEND'] elif iniconfig_loaded and iniconfig.has_option(iniconfig_section, 'X2GOBROKER_DEFAULT_BACKEND'): @@ -210,6 +223,8 @@ X2GOBROKER_CONFIG_DEFAULTS = { u'default-sshproxy-authorized-keys': u'%h/.x2go/authorized_keys', u'default-agent-query-mode': u'NONE', u'default-portscan-x2goservers': True, + u'default-use-load-checker': False, + u'load-checker-intervals': 300, }, 'broker_base': { u'enable': False, @@ -220,6 +235,7 @@ X2GOBROKER_CONFIG_DEFAULTS = { u'user-db': u'libnss', u'group-db': u'libnss', u'desktop-shell': u'KDE', + u'load-checker': False, }, 'broker_inifile': { u'enable': True, @@ -227,6 +243,7 @@ X2GOBROKER_CONFIG_DEFAULTS = { u'auth-mech': u'', u'user-db': u'', u'group-db': u'', + u'use-load-checker': True, }, 'broker_ldap': { u'enable': False, @@ -240,6 +257,7 @@ X2GOBROKER_CONFIG_DEFAULTS = { u'group-search-filter': u'(&(objectClass=posifxGroup)(cn=*))', u'starttls': False, u'agent-query-mode': u'SSH', + u'load-checker': True, }, } diff --git a/x2gobroker/loadchecker.py b/x2gobroker/loadchecker.py new file mode 100644 index 0000000..f38717e --- /dev/null +++ b/x2gobroker/loadchecker.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +# This file is part of the X2Go Project - http://www.x2go.org +# Copyright (C) 2012-2015 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# +# X2Go Session Broker is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# X2Go Session Broker 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +import threading +import time +import copy +import socket + +# X2Go Session Broker modules +import x2gobroker.defaults +import x2gobroker.config +from x2gobroker.loggers import logger_broker + + +def checkload(backend, profile_id, hostname=None): + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + logger_broker.debug('loadchecker.checkload(): connecting to load checker service socket {socket}'.format(socket=x2gobroker.defaults.X2GOBROKER_LOADCHECKER_SOCKET)) + try: + s.connect(x2gobroker.defaults.X2GOBROKER_LOADCHECKER_SOCKET) + except socket.error, e: + logger_broker.error('loadchecker.checkload(): failure when connecting to the load checker service socket {socket}: {errmsg}'.format(socket=x2gobroker.defaults.X2GOBROKER_LOADCHECKER_SOCKET, errmsg=str(e))) + + if hostname is not None: + load_factor = 'LOAD-UNAVAILABLE' + logger_broker.debug('loadchecker.checkload(): sending backend={backend}, profile_id={profile_id}, hostname={hostname} to load checker service'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + try: + s.send('{backend}\r{profile_id}\r{hostname}\n'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + load_factor = s.recv(1024) + s.close() + except socket.error, e: + logger_broker.error('loadchecker.checkload(): failure when sending data to the load checker service socket {socket}: {errmsg}'.format(socket=x2gobroker.defaults.X2GOBROKER_LOADCHECKER_SOCKET, errmsg=str(e))) + + if load_factor.startswith('LOAD-UNAVAILABLE'): + logger_broker.warning('loadchecker.checkload(): load unavailable for backend={backend}, profile_id={profile_id}, hostname={hostname}'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + return None + + logger_broker.info('loadchecker.checkload(): load factor for backend={backend}, profile_id={profile_id}, hostname={hostname} is: {lf}'.format(backend=backend, profile_id=profile_id, hostname=hostname, lf=load_factor)) + return load_factor + + else: + raw_output = "" + logger_broker.debug('loadchecker.checkload(): sending backend={backend}, profile_id={profile_id} to load checker service'.format(backend=backend, profile_id=profile_id, hostname=hostname)) + try: + s.send('{backend}\r{profile_id}\r\n'.format(backend=backend, profile_id=profile_id)) + raw_output = s.recv(1024) + s.close() + except socket.error, e: + logger_broker.error('loadchecker.checkload(): failure when sending data to the load checker service socket {socket}: {errmsg}'.format(socket=x2gobroker.defaults.X2GOBROKER_LOADCHECKER_SOCKET, errmsg=str(e))) + + load_factors = {} + items = raw_output.split('\n') + for item in items: + if ":" in item: + key, val = item.split(':', 1) + load_factors[key] = val + + logger_broker.info('loadchecker.checkload(): load metrics for backend={backend}, profile_id={profile_id} are: {lf}'.format(backend=backend, profile_id=profile_id, hostname=hostname, lf=load_factors)) + return load_factors + + +class LoadChecker(threading.Thread): + + def __init__(self, config_file=None, config_defaults=None, logger=None, **kwargs): + """\ + Initialize a new LoadChecker instance for querying remote X2Go Broker Agent instances + about server/system load, CPU usage, etc. + + """ + self.logger = logger + + self.config_file = config_file + if self.config_file is None: self.config_file = x2gobroker.defaults.X2GOBROKER_CONFIG + self.config_defaults = config_defaults + if self.config_defaults is None: self.config_defaults = x2gobroker.defaults.X2GOBROKER_CONFIG_DEFAULTS + self.kwargs = kwargs + + threading.Thread.__init__(self, target=self.loadchecker) + self.server_load = {} + self.keep_alive = True + self.daemon = True + + def get_server_load(self, backend, profile_id, hostname): + """\ + Retrieve system load for a given server (via broker backend, + profile ID and hostname). + + @param backend: broker backend to query. + @type backend: C{unicode} + @param profile_id: profile ID of the session profile to query + @type profile_id: C{unicode} + @param hostname: hostname of the X2Go Server + @type hostname: C{unicode} + + @return: load factor of the given server (or None if an error occurs) + @rtype: C{int} + + """ + try: + return self.server_load[backend][profile_id][hostname] + except KeyError: + return None + + def get_profile_load(self, backend, profile_id): + """\ + Retrieve system load for all servers for a given profile ID (and a given + broker backend). + + @param backend: broker backend to query. + @type backend: C{unicode} + @param profile_id: profile ID of the session profile to query + @type profile_id: C{unicode} + + @return: load factor of the given server (or None if an error occurs) + @rtype: C{dict} + + """ + try: + return self.server_load[backend][profile_id] + except KeyError: + return None + + def loadchecker(self): + """\ + This is the actual thread runner that queries configured / available X2Go Broker Agents in regular + intervals about system load, CPU types and usage. + + """ + time_to_sleep = 0 + while self.keep_alive: + + # in every loop we re-read the main configuration file(s) + # this allows changes to the config files to take effect immediately... + + self.config = x2gobroker.config.X2GoBrokerConfigFile(config_files=self.config_file, defaults=self.config_defaults) + self.load_checker_intervals = self.config.get_value('global', 'load-checker-intervals') + + self.broker_backends = [ "_".join(bs.split('_')[1:]) for bs in self.config.list_sections() if bs.startswith('broker_') and self.config.get_value(bs, 'enable') ] + + # potentially, the X2Go Session Broker can manage different broker backends at the same time, so we initialize + # all configured/enabled broker backends + self.brokers = {} + num_queries = 0 + for backend in self.broker_backends: + + if self.logger: self.logger.debug('LoadChecker.loadchecker(): load checker thread waking up...') + + if not self.server_load.has_key(backend): + self.server_load[backend] = {} + + _broker_backend_module = None + exec("import x2gobroker.brokers.{backend}_broker as _broker_backend_module".format(backend=backend)) + self.brokers[backend] = _broker_backend_module.X2GoBroker(config_file=self.config_file, config_defaults=self.config_defaults, **self.kwargs) + profile_ids_to_check = [ id for id in self.brokers[backend].get_profile_ids() if self.brokers[backend].use_load_checker(id) ] + + for profile_id in profile_ids_to_check: + + if not self.server_load[backend].has_key(profile_id): + self.server_load[backend][profile_id] = {} + remote_agents = self.brokers[backend].get_all_remote_agents(profile_id) + for remote_agent in remote_agents: + self.server_load[backend][profile_id][remote_agent[u'hostname']] = x2gobroker.agent.checkload(remote_agent) + if self.logger: self.logger.info('LoadChecker.loadchecker(): contacted remote broker agent for backend={backend}, profile_id={profile_id}, hostname={hostname}, new load factor is: {lf}'.format(backend=backend, profile_id=profile_id, hostname=remote_agent[u'hostname'], lf=self.server_load[backend][profile_id][remote_agent[u'hostname']])) + num_queries += 1 + if time_to_sleep > 0: + if self.logger: self.logger.debug('LoadChecker.loadchecker(): sleeping for {secs}secs before querying next server'.format(secs=time_to_sleep)) + time.sleep(time_to_sleep) + + # clean up vanished hostnames + _hostnames = self.server_load[backend][profile_id].keys() + for hostname in _hostnames: + if hostname not in [ ra[u'hostname'] for ra in remote_agents ]: + del self.server_load[backend][profile_id][hostname] + + # clean up vanished profile IDs + _profile_ids = copy.deepcopy(self.server_load[backend].keys()) + for profile_id in _profile_ids: + if profile_id not in profile_ids_to_check: + del self.server_load[backend][profile_id] + + # clean up vanished backends + _backends = copy.deepcopy(self.server_load.keys()) + for backend in _backends: + if backend not in self.broker_backends: + del self.server_load[backend] + + # don't do all queries every 300-or-so seconds, but distribute next round of queries over the + # complete load_checker_intervals range + if time_to_sleep == 0: + if self.logger: self.logger.debug('LoadChecker.loadchecker(): sleeping for {secs}secs before starting next query cycle'.format(secs=self.load_checker_intervals)) + time.sleep(self.load_checker_intervals) + if num_queries > 0: + if time_to_sleep > 0: + if self.logger: self.logger.debug('LoadChecker.loadchecker(): performed {num} queries, sleeping for {secs}secs before starting next query cycle'.format(num=num_queries, secs=self.load_checker_intervals - time_to_sleep * num_queries)) + time.sleep(self.load_checker_intervals - time_to_sleep * num_queries) + time_to_sleep = self.load_checker_intervals / (num_queries +1) + else: + if num_queries == 0: + if self.logger: self.logger.warning('LoadChecker.loadchecker(): performed {num} queries in this cycle, if this message keeps repeating itself, consider disabling the X2Go Broker Load Checker daemon'.format(num=num_queries, secs=self.load_checker_intervals - time_to_sleep * num_queries)) + time_to_sleep = 0 + + def stop_thread(self): + self.keep_alive = False -- Alioth's /srv/git/code.x2go.org/x2gobroker.git//..//_hooks_/post-receive-email on /srv/git/code.x2go.org/x2gobroker.git