diff options
Diffstat (limited to 'glances/autodiscover.py')
-rw-r--r-- | glances/autodiscover.py | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/glances/autodiscover.py b/glances/autodiscover.py new file mode 100644 index 00000000..fa1414d4 --- /dev/null +++ b/glances/autodiscover.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com> +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Manage autodiscover Glances server (thk to the ZeroConf protocol).""" + +import socket +import sys + +from glances.globals import appname, BSD +from glances.logger import logger + +try: + from zeroconf import ( + __version__ as __zeroconf_version, + ServiceBrowser, + ServiceInfo, + Zeroconf + ) + zeroconf_tag = True +except ImportError: + zeroconf_tag = False + +# Zeroconf 0.17 or higher is needed +if zeroconf_tag: + zeroconf_min_version = (0, 17, 0) + zeroconf_version = tuple([int(num) for num in __zeroconf_version.split('.')]) + logger.debug("Zeroconf version {0} detected.".format(__zeroconf_version)) + if zeroconf_version < zeroconf_min_version: + logger.critical("Please install zeroconf 0.17 or higher.") + sys.exit(1) + +# Global var +zeroconf_type = "_%s._tcp." % appname + + +class AutoDiscovered(object): + + """Class to manage the auto discovered servers dict.""" + + def __init__(self): + # server_dict is a list of dict (JSON compliant) + # [ {'key': 'zeroconf name', ip': '172.1.2.3', 'port': 61209, 'cpu': 3, 'mem': 34 ...} ... ] + self._server_list = [] + + def get_servers_list(self): + """Return the current server list (list of dict).""" + return self._server_list + + def set_server(self, server_pos, key, value): + """Set the key to the value for the server_pos (position in the list).""" + self._server_list[server_pos][key] = value + + def add_server(self, name, ip, port): + """Add a new server to the list.""" + new_server = { + 'key': name, # Zeroconf name with both hostname and port + 'name': name.split(':')[0], # Short name + 'ip': ip, # IP address seen by the client + 'port': port, # TCP port + 'username': 'glances', # Default username + 'password': '', # Default password + 'status': 'UNKNOWN', # Server status: 'UNKNOWN', 'OFFLINE', 'ONLINE', 'PROTECTED' + 'type': 'DYNAMIC'} # Server type: 'STATIC' or 'DYNAMIC' + self._server_list.append(new_server) + logger.debug("Updated servers list (%s servers): %s" % + (len(self._server_list), self._server_list)) + + def remove_server(self, name): + """Remove a server from the dict.""" + for i in self._server_list: + if i['key'] == name: + try: + self._server_list.remove(i) + logger.debug("Remove server %s from the list" % name) + logger.debug("Updated servers list (%s servers): %s" % ( + len(self._server_list), self._server_list)) + except ValueError: + logger.error( + "Cannot remove server %s from the list" % name) + + +class GlancesAutoDiscoverListener(object): + + """Zeroconf listener for Glances server.""" + + def __init__(self): + # Create an instance of the servers list + self.servers = AutoDiscovered() + + def get_servers_list(self): + """Return the current server list (list of dict).""" + return self.servers.get_servers_list() + + def set_server(self, server_pos, key, value): + """Set the key to the value for the server_pos (position in the list).""" + self.servers.set_server(server_pos, key, value) + + def add_service(self, zeroconf, srv_type, srv_name): + """Method called when a new Zeroconf client is detected. + + Return True if the zeroconf client is a Glances server + Note: the return code will never be used + """ + if srv_type != zeroconf_type: + return False + logger.debug("Check new Zeroconf server: %s / %s" % + (srv_type, srv_name)) + info = zeroconf.get_service_info(srv_type, srv_name) + if info: + new_server_ip = socket.inet_ntoa(info.address) + new_server_port = info.port + + # Add server to the global dict + self.servers.add_server(srv_name, new_server_ip, new_server_port) + logger.info("New Glances server detected (%s from %s:%s)" % + (srv_name, new_server_ip, new_server_port)) + else: + logger.warning( + "New Glances server detected, but Zeroconf info failed to be grabbed") + return True + + def remove_service(self, zeroconf, srv_type, srv_name): + """Remove the server from the list.""" + self.servers.remove_server(srv_name) + logger.info( + "Glances server %s removed from the autodetect list" % srv_name) + + +class GlancesAutoDiscoverServer(object): + + """Implementation of the Zeroconf protocol (server side for the Glances client).""" + + def __init__(self, args=None): + if zeroconf_tag: + logger.info("Init autodiscover mode (Zeroconf protocol)") + try: + self.zeroconf = Zeroconf() + except socket.error as e: + logger.error("Cannot start Zeroconf (%s)" % e) + self.zeroconf_enable_tag = False + else: + self.listener = GlancesAutoDiscoverListener() + self.browser = ServiceBrowser( + self.zeroconf, zeroconf_type, self.listener) + self.zeroconf_enable_tag = True + else: + logger.error("Cannot start autodiscover mode (Zeroconf lib is not installed)") + self.zeroconf_enable_tag = False + + def get_servers_list(self): + """Return the current server list (dict of dict).""" + if zeroconf_tag and self.zeroconf_enable_tag: + return self.listener.get_servers_list() + else: + return [] + + def set_server(self, server_pos, key, value): + """Set the key to the value for the server_pos (position in the list).""" + if zeroconf_tag and self.zeroconf_enable_tag: + self.listener.set_server(server_pos, key, value) + + def close(self): + if zeroconf_tag and self.zeroconf_enable_tag: + self.zeroconf.close() + + +class GlancesAutoDiscoverClient(object): + + """Implementation of the zeroconf protocol (client side for the Glances server).""" + + def __init__(self, hostname, args=None): + if zeroconf_tag: + zeroconf_bind_address = args.bind_address + try: + self.zeroconf = Zeroconf() + except socket.error as e: + logger.error("Cannot start zeroconf: {0}".format(e)) + + # XXX *BSDs: Segmentation fault (core dumped) + # -- https://bitbucket.org/al45tair/netifaces/issues/15 + if not BSD: + try: + # -B @ overwrite the dynamic IPv4 choice + if zeroconf_bind_address == '0.0.0.0': + zeroconf_bind_address = self.find_active_ip_address() + except KeyError: + # Issue #528 (no network interface available) + pass + + print("Announce the Glances server on the LAN (using {0} IP address)".format(zeroconf_bind_address)) + self.info = ServiceInfo( + zeroconf_type, '{0}:{1}.{2}'.format(hostname, args.port, zeroconf_type), + address=socket.inet_aton(zeroconf_bind_address), port=args.port, + weight=0, priority=0, properties={}, server=hostname) + self.zeroconf.register_service(self.info) + else: + logger.error("Cannot announce Glances server on the network: zeroconf library not found.") + + @staticmethod + def find_active_ip_address(): + """Try to find the active IP addresses.""" + import netifaces + # Interface of the default gateway + gateway_itf = netifaces.gateways()['default'][netifaces.AF_INET][1] + # IP address for the interface + return netifaces.ifaddresses(gateway_itf)[netifaces.AF_INET][0]['addr'] + + def close(self): + if zeroconf_tag: + self.zeroconf.unregister_service(self.info) + self.zeroconf.close() |