1# -*- coding: utf-8 -*- 2# 3# This file is part of Glances. 4# 5# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com> 6# 7# Glances is free software; you can redistribute it and/or modify 8# it under the terms of the GNU Lesser General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# Glances is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU Lesser General Public License for more details. 16# 17# You should have received a copy of the GNU Lesser General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19 20"""Manage autodiscover Glances server (thk to the ZeroConf protocol).""" 21 22import socket 23import sys 24 25from glances.globals import BSD 26from glances.logger import logger 27 28try: 29 from zeroconf import ( 30 __version__ as __zeroconf_version, 31 ServiceBrowser, 32 ServiceInfo, 33 Zeroconf 34 ) 35 zeroconf_tag = True 36except ImportError: 37 zeroconf_tag = False 38 39# Zeroconf 0.17 or higher is needed 40if zeroconf_tag: 41 zeroconf_min_version = (0, 17, 0) 42 zeroconf_version = tuple([int(num) for num in __zeroconf_version.split('.')]) 43 logger.debug("Zeroconf version {} detected.".format(__zeroconf_version)) 44 if zeroconf_version < zeroconf_min_version: 45 logger.critical("Please install zeroconf 0.17 or higher.") 46 sys.exit(1) 47 48# Global var 49# Recent versions of the zeroconf python package doesnt like a zeroconf type that ends with '._tcp.'. 50# Correct issue: zeroconf problem with zeroconf_type = "_%s._tcp." % 'glances' #888 51zeroconf_type = "_%s._tcp.local." % 'glances' 52 53 54class AutoDiscovered(object): 55 56 """Class to manage the auto discovered servers dict.""" 57 58 def __init__(self): 59 # server_dict is a list of dict (JSON compliant) 60 # [ {'key': 'zeroconf name', ip': '172.1.2.3', 'port': 61209, 'cpu': 3, 'mem': 34 ...} ... ] 61 self._server_list = [] 62 63 def get_servers_list(self): 64 """Return the current server list (list of dict).""" 65 return self._server_list 66 67 def set_server(self, server_pos, key, value): 68 """Set the key to the value for the server_pos (position in the list).""" 69 self._server_list[server_pos][key] = value 70 71 def add_server(self, name, ip, port): 72 """Add a new server to the list.""" 73 new_server = { 74 'key': name, # Zeroconf name with both hostname and port 75 'name': name.split(':')[0], # Short name 76 'ip': ip, # IP address seen by the client 77 'port': port, # TCP port 78 'username': 'glances', # Default username 79 'password': '', # Default password 80 'status': 'UNKNOWN', # Server status: 'UNKNOWN', 'OFFLINE', 'ONLINE', 'PROTECTED' 81 'type': 'DYNAMIC'} # Server type: 'STATIC' or 'DYNAMIC' 82 self._server_list.append(new_server) 83 logger.debug("Updated servers list (%s servers): %s" % 84 (len(self._server_list), self._server_list)) 85 86 def remove_server(self, name): 87 """Remove a server from the dict.""" 88 for i in self._server_list: 89 if i['key'] == name: 90 try: 91 self._server_list.remove(i) 92 logger.debug("Remove server %s from the list" % name) 93 logger.debug("Updated servers list (%s servers): %s" % ( 94 len(self._server_list), self._server_list)) 95 except ValueError: 96 logger.error( 97 "Cannot remove server %s from the list" % name) 98 99 100class GlancesAutoDiscoverListener(object): 101 102 """Zeroconf listener for Glances server.""" 103 104 def __init__(self): 105 # Create an instance of the servers list 106 self.servers = AutoDiscovered() 107 108 def get_servers_list(self): 109 """Return the current server list (list of dict).""" 110 return self.servers.get_servers_list() 111 112 def set_server(self, server_pos, key, value): 113 """Set the key to the value for the server_pos (position in the list).""" 114 self.servers.set_server(server_pos, key, value) 115 116 def add_service(self, zeroconf, srv_type, srv_name): 117 """Method called when a new Zeroconf client is detected. 118 119 Return True if the zeroconf client is a Glances server 120 Note: the return code will never be used 121 """ 122 if srv_type != zeroconf_type: 123 return False 124 logger.debug("Check new Zeroconf server: %s / %s" % 125 (srv_type, srv_name)) 126 info = zeroconf.get_service_info(srv_type, srv_name) 127 if info: 128 new_server_ip = socket.inet_ntoa(info.address) 129 new_server_port = info.port 130 131 # Add server to the global dict 132 self.servers.add_server(srv_name, new_server_ip, new_server_port) 133 logger.info("New Glances server detected (%s from %s:%s)" % 134 (srv_name, new_server_ip, new_server_port)) 135 else: 136 logger.warning( 137 "New Glances server detected, but Zeroconf info failed to be grabbed") 138 return True 139 140 def remove_service(self, zeroconf, srv_type, srv_name): 141 """Remove the server from the list.""" 142 self.servers.remove_server(srv_name) 143 logger.info( 144 "Glances server %s removed from the autodetect list" % srv_name) 145 146 147class GlancesAutoDiscoverServer(object): 148 149 """Implementation of the Zeroconf protocol (server side for the Glances client).""" 150 151 def __init__(self, args=None): 152 if zeroconf_tag: 153 logger.info("Init autodiscover mode (Zeroconf protocol)") 154 try: 155 self.zeroconf = Zeroconf() 156 except socket.error as e: 157 logger.error("Cannot start Zeroconf (%s)" % e) 158 self.zeroconf_enable_tag = False 159 else: 160 self.listener = GlancesAutoDiscoverListener() 161 self.browser = ServiceBrowser( 162 self.zeroconf, zeroconf_type, self.listener) 163 self.zeroconf_enable_tag = True 164 else: 165 logger.error("Cannot start autodiscover mode (Zeroconf lib is not installed)") 166 self.zeroconf_enable_tag = False 167 168 def get_servers_list(self): 169 """Return the current server list (dict of dict).""" 170 if zeroconf_tag and self.zeroconf_enable_tag: 171 return self.listener.get_servers_list() 172 else: 173 return [] 174 175 def set_server(self, server_pos, key, value): 176 """Set the key to the value for the server_pos (position in the list).""" 177 if zeroconf_tag and self.zeroconf_enable_tag: 178 self.listener.set_server(server_pos, key, value) 179 180 def close(self): 181 if zeroconf_tag and self.zeroconf_enable_tag: 182 self.zeroconf.close() 183 184 185class GlancesAutoDiscoverClient(object): 186 187 """Implementation of the zeroconf protocol (client side for the Glances server).""" 188 189 def __init__(self, hostname, args=None): 190 if zeroconf_tag: 191 zeroconf_bind_address = args.bind_address 192 try: 193 self.zeroconf = Zeroconf() 194 except socket.error as e: 195 logger.error("Cannot start zeroconf: {}".format(e)) 196 197 # XXX *BSDs: Segmentation fault (core dumped) 198 # -- https://bitbucket.org/al45tair/netifaces/issues/15 199 if not BSD: 200 try: 201 # -B @ overwrite the dynamic IPv4 choice 202 if zeroconf_bind_address == '0.0.0.0': 203 zeroconf_bind_address = self.find_active_ip_address() 204 except KeyError: 205 # Issue #528 (no network interface available) 206 pass 207 208 # Check IP v4/v6 209 address_family = socket.getaddrinfo(zeroconf_bind_address, args.port)[0][0] 210 211 # Start the zeroconf service 212 self.info = ServiceInfo( 213 zeroconf_type, '{}:{}.{}'.format(hostname, args.port, zeroconf_type), 214 address=socket.inet_pton(address_family, zeroconf_bind_address), 215 port=args.port, weight=0, priority=0, properties={}, server=hostname) 216 try: 217 self.zeroconf.register_service(self.info) 218 except socket.error as e: 219 logger.error("Error while announcing Glances server: {}".format(e)) 220 else: 221 print("Announce the Glances server on the LAN (using {} IP address)".format(zeroconf_bind_address)) 222 else: 223 logger.error("Cannot announce Glances server on the network: zeroconf library not found.") 224 225 @staticmethod 226 def find_active_ip_address(): 227 """Try to find the active IP addresses.""" 228 import netifaces 229 # Interface of the default gateway 230 gateway_itf = netifaces.gateways()['default'][netifaces.AF_INET][1] 231 # IP address for the interface 232 return netifaces.ifaddresses(gateway_itf)[netifaces.AF_INET][0]['addr'] 233 234 def close(self): 235 if zeroconf_tag: 236 self.zeroconf.unregister_service(self.info) 237 self.zeroconf.close() 238