This is an automated email from the git hooks/post-receive script. x2go pushed a commit to branch master in repository x2gobroker. commit 7e96a3af36a232fb7e4fbe6222e336b69ffe5794 Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de> Date: Thu Sep 11 22:38:17 2014 +0200 Provide support for load-balancing to hosts that are all reachable over the same IP address, but different TCP/IP ports (e.g. docker instances or hosts behind a reverse NATed IPv4 gateway). This ended up in a rewrite of the complete selection_session() method of the base broker code. --- debian/changelog | 5 ++ x2gobroker/brokers/base_broker.py | 162 ++++++++++++++++++++-------------- x2gobroker/brokers/inifile_broker.py | 5 ++ 3 files changed, 108 insertions(+), 64 deletions(-) diff --git a/debian/changelog b/debian/changelog index 4e79add..4f207a1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -152,6 +152,11 @@ x2gobroker (0.0.3.0-0x2go1) UNRELEASED; urgency=low /etc/default/x2gobroker-daemon and /etc/x2go/broker/defaults.cfg. - Move split_host_address() code into x2gobroker.utils. - Report to log what the broker agent replied to us. + - Provide support for load-balancing to hosts that are all reachable + over the same IP address, but different TCP/IP ports (e.g. docker + instances or hosts behind a reverse NATed IPv4 gateway). This ended + up in a rewrite of the complete selection_session() method of the + base broker code. * debian/control: + Provide separate bin:package for SSH brokerage: x2gobroker-ssh. + Replace LDAP support with session brokerage support in LONG_DESCRIPTION. diff --git a/x2gobroker/brokers/base_broker.py b/x2gobroker/brokers/base_broker.py index e92c7d8..b72576d 100644 --- a/x2gobroker/brokers/base_broker.py +++ b/x2gobroker/brokers/base_broker.py @@ -1053,19 +1053,81 @@ class X2GoBroker(object): try: profile = self.get_profile(profile_id) except x2gobroker.x2gobroker_exceptions.X2GoBrokerProfileException: - return {} + return { 'server': 'no-server-available', 'port': 22, } # if we have more than one server, pick one server randomly for X2Go Broker Agent queries server_list = profile[u'host'] + if len(server_list) == 0: + return { 'server': 'no-server-available', 'port': profile['sshport'], } + + # if everything below fails, this will be the X2Go Server's hostname that + # we will connect to... + server_name = server_list[0] + server_port = profile['sshport'] + + # try to retrieve contact to a remote broker agent remote_agent = self.get_remote_agent(profile_id) if remote_agent: agent_query_mode = ( remote_agent == u'LOCAL') and u'LOCAL' or u'SSH' + # check for already running sessions for the given user (if any is given) + session_list = [] + if remote_agent and username: + try: + success, session_list = x2gobroker.agent.list_sessions(username=username, remote_agent=remote_agent) + except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException: + session_list = [] + + session_info = None + if session_list: + + # Obviously a remote broker agent reported an already running session + # on the / on one the available X2Go Server host(s) + + # When resuming, always select the first session in the list, + # there should only be one running/suspended session by design + # of X2Go brokerage (this may change in the future) + try: + running_sessions = [] + suspended_sessions = [] + for session_info in session_list: + if session_info.split('|')[4] == 'R': + running_sessions.append(session_info) + if session_info.split('|')[4] == 'S': + suspended_sessions.append(session_info) + + # we prefer suspended sessions over resuming sessions if we find sessions with both + # states of activity + if suspended_sessions: + session_info = suspended_sessions[0] + elif running_sessions: + session_info = running_sessions[0] + x2gobroker.agent.suspend_session(username=username, session_name=session_info.split('|')[1], remote_agent=remote_agent) + # this is the turn-around in x2gocleansessions, so waiting as along as the daemon + # that will suspend the session + time.sleep(2) + session_info = session_info.replace('|R|', '|S|') + + # only use the server's official hostname (as set on the server) + # if we know about the real/physical address of the server + _session_server_name = session_info.split('|')[3] + if profile.has_key('host={server_name}'.format(server_name=_session_server_name)): + server_name = _session_server_name + + except IndexError: + # FIXME: if we get here, we have to deal with a broken session info + # entry in the X2Go session database. -> AWFUL!!! + pass + # detect best X2Go server for this user if load balancing is configured - if remote_agent and len(server_list) >= 2 and username: + elif remote_agent and len(server_list) >= 2 and username: - # query remote agent for session info + # No running / suspended session was found on any of the available + # X2Go Servers. Thus, we will try to detect the best server for this + # load balanced X2Go Server farm. + + # query remote agent on how busy our servers are... busy_servers = None try: success, busy_servers = x2gobroker.agent.find_busy_servers(username=username, remote_agent=remote_agent) @@ -1078,20 +1140,23 @@ class X2GoBroker(object): # when detecting the server load we have to support handling of differing subdomains (config # file vs. server load returned by x2gobroker agent). Best approach: all members of a multi-node - # server farm either (a) do not have a subdomain in their hostname or (b) the subdomain is - # is identical. + # server farm either + # + # (a) do not have a subdomain in their hostname or + # (b) have an identical subdomain in their hostnames # Example: # # ts01, ts02 - hostnames as returned by agent # ts01.intern, ts02.intern - hostnames configured in session profile option ,,host'' - # -> this will result in the subdomain .intern being stripped off from the hostnames before detecting - # the best server for this user + # -> this will result in the subdomain .intern being stripped off from the hostnames before + # detecting the best server for this user ### NORMALIZE (=reduce to hostname only) X2Go server names (as found in config) if possible server_list_normalized, subdomains_config = x2gobroker.utils.normalize_hostnames(server_list) - ### NORMALIZE X2Go server names (as returned by x2gobroker agent)--only if the hostnames in the config share the same subdomain + ### NORMALIZE X2Go server names (as returned by broker agent)--only if the hostnames in + # the config share the same subdomain if len(subdomains_config) == 1: busy_servers_normalized, subdomains_agent = x2gobroker.utils.normalize_hostnames(busy_servers) @@ -1110,74 +1175,43 @@ class X2GoBroker(object): logger_broker.debug('base_broker.X2GoBroker.select_session(): load balancer analysis: {server_load}'.format(server_load=unicode(busy_server_list))) - best_server = busy_server_list[0][1] + server_name = busy_server_list[0][1] else: logger_broker.warning('base_broker.X2GoBroker.select_session(): no broker agent could be contacted, this does not look good. We tried these agent hosts: {agent_hosts}'.format(agent_hosts=unicode(server_list))) - if server_list: best_server = server_list[0] - else: return { 'server': 'no-server-available', 'port': profile[u'sshport'], } - else: - if server_list: best_server = server_list[0] - else: return { 'server': 'no-server-available', 'port': profile[u'sshport'], } + # detect best X2Go server for this user if load balancing is configured + elif len(server_list) >= 2: + + # no remote broker agent or no username? Let's play roulette then... + server_name = random.choice(server_list) + + ### + ### by now we should know the proper host to connect to... + ### - # if we have an explicit IP address for best_server, let's use that instead... + # if we have an explicit TCP/IP port server_name, let's use that instead... try: - best_server = profile['host={hostname}'.format(hostname=best_server)] + server_port = profile['sshport={hostname}'.format(hostname=server_name)] + logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server port: {port}'.format(port=server_port)) + except KeyError: + pass + + # if we have an explicit TCP/IP address for server_name, let's use that instead... + try: + server_name = profile['host={hostname}'.format(hostname=server_name)] + logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server address: {address}'.format(address=server_name)) except KeyError: pass selected_session = { - 'server': best_server, - 'port': profile[u'sshport'], + 'server': server_name, + 'port': server_port, } - # find already running/suspended sessions and resume the first one found - if remote_agent and server_list and username: - - try: - success, session_list = x2gobroker.agent.list_sessions(username=username, remote_agent=remote_agent) - except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException: - session_list = [] - - if session_list: - - # if resuming, always select the first session in the list, there should only be one suspended session - try: - running_sessions = [] - suspended_sessions = [] - for session_info in session_list: - if session_info.split('|')[4] == 'R': - running_sessions.append(session_info) - if session_info.split('|')[4] == 'S': - suspended_sessions.append(session_info) - - # we prefer suspended sessions for resuming if we find sessions with both states of activity - if suspended_sessions: - session_info = suspended_sessions[0] - else: - session_info = running_sessions[0] - x2gobroker.agent.suspend_session(username=username, session_name=session_info.split('|')[1], remote_agent=remote_agent) - # this is the turn-around in x2gocleansessions, so waiting as along as the daemon that will suspend the session - time.sleep(2) - session_info = session_info.replace('|R|', '|S|') - server_name = session_info.split('|')[3] - - # if we have an explicit IP address for server_name, let's use that instead... - try: - server_name = profile['host={hostname}'.format(hostname=server_name)] - except KeyError: - pass - - selected_session.update({ - 'server': server_name, - 'session_info': session_info, - }) - - except IndexError: - - # FIXME: if we get here, we have to deal with a broker session info entry in the X2Go session database - pass + # are we resuming a running/suspended session? + if session_info is not None: + selected_session['session_info'] = session_info # define a remote SSH proxy agent if an SSH proxy host is used with this session profile if profile.has_key(u'sshproxyhost') and profile[u'sshproxyhost']: diff --git a/x2gobroker/brokers/inifile_broker.py b/x2gobroker/brokers/inifile_broker.py index 81a69cb..18341bf 100644 --- a/x2gobroker/brokers/inifile_broker.py +++ b/x2gobroker/brokers/inifile_broker.py @@ -87,11 +87,14 @@ class X2GoBroker(base.X2GoBroker): del profile[key] if key == 'host': _hosts = copy.deepcopy(profile[key]) + try: _default_sshport = int(profile['sshport']) + except TypeError: _default_sshport = 22 profile[key] = [] for host in _hosts: if re.match('^.*\ \(.*\)$', host): _hostname = host.split(' ')[0] _address = host.split(' ')[1][1:-1] + _address, _port = x2gobroker.utils.split_host_address(_address, default_port=_default_sshport) # test if _address is a valid hostname, a valid DNS name or an IPv4/IPv6 address if (re.match('(?!-)[A-Z\d-]{1,63}(?<!-)$', _hostname, flags=re.IGNORECASE) or \ @@ -100,6 +103,8 @@ class X2GoBroker(base.X2GoBroker): netaddr.valid_ipv4(_address) or netaddr.valid_ipv6(_address)): profile["host={hostname}".format(hostname=_hostname)] = _address + if _port != _default_sshport: + profile["sshport={hostname}".format(hostname=_hostname)] = _port profile[key].append(_hostname) else: -- Alioth's /srv/git/_hooks_/post-receive-email on /srv/git/code.x2go.org/x2gobroker.git