You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
9.1 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Glances.
  4. #
  5. # Copyright (C) 2018 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. """Manage autodiscover Glances server (thk to the ZeroConf protocol)."""
  20. import socket
  21. import sys
  22. from glances.globals import BSD
  23. from glances.logger import logger
  24. try:
  25. from zeroconf import (
  26. __version__ as __zeroconf_version,
  27. ServiceBrowser,
  28. ServiceInfo,
  29. Zeroconf
  30. )
  31. zeroconf_tag = True
  32. except ImportError:
  33. zeroconf_tag = False
  34. # Zeroconf 0.17 or higher is needed
  35. if zeroconf_tag:
  36. zeroconf_min_version = (0, 17, 0)
  37. zeroconf_version = tuple([int(num) for num in __zeroconf_version.split('.')])
  38. logger.debug("Zeroconf version {} detected.".format(__zeroconf_version))
  39. if zeroconf_version < zeroconf_min_version:
  40. logger.critical("Please install zeroconf 0.17 or higher.")
  41. sys.exit(1)
  42. # Global var
  43. # Recent versions of the zeroconf python package doesnt like a zeroconf type that ends with '._tcp.'.
  44. # Correct issue: zeroconf problem with zeroconf_type = "_%s._tcp." % 'glances' #888
  45. zeroconf_type = "_%s._tcp.local." % 'glances'
  46. class AutoDiscovered(object):
  47. """Class to manage the auto discovered servers dict."""
  48. def __init__(self):
  49. # server_dict is a list of dict (JSON compliant)
  50. # [ {'key': 'zeroconf name', ip': '172.1.2.3', 'port': 61209, 'cpu': 3, 'mem': 34 ...} ... ]
  51. self._server_list = []
  52. def get_servers_list(self):
  53. """Return the current server list (list of dict)."""
  54. return self._server_list
  55. def set_server(self, server_pos, key, value):
  56. """Set the key to the value for the server_pos (position in the list)."""
  57. self._server_list[server_pos][key] = value
  58. def add_server(self, name, ip, port):
  59. """Add a new server to the list."""
  60. new_server = {
  61. 'key': name, # Zeroconf name with both hostname and port
  62. 'name': name.split(':')[0], # Short name
  63. 'ip': ip, # IP address seen by the client
  64. 'port': port, # TCP port
  65. 'username': 'glances', # Default username
  66. 'password': '', # Default password
  67. 'status': 'UNKNOWN', # Server status: 'UNKNOWN', 'OFFLINE', 'ONLINE', 'PROTECTED'
  68. 'type': 'DYNAMIC'} # Server type: 'STATIC' or 'DYNAMIC'
  69. self._server_list.append(new_server)
  70. logger.debug("Updated servers list (%s servers): %s" %
  71. (len(self._server_list), self._server_list))
  72. def remove_server(self, name):
  73. """Remove a server from the dict."""
  74. for i in self._server_list:
  75. if i['key'] == name:
  76. try:
  77. self._server_list.remove(i)
  78. logger.debug("Remove server %s from the list" % name)
  79. logger.debug("Updated servers list (%s servers): %s" % (
  80. len(self._server_list), self._server_list))
  81. except ValueError:
  82. logger.error(
  83. "Cannot remove server %s from the list" % name)
  84. class GlancesAutoDiscoverListener(object):
  85. """Zeroconf listener for Glances server."""
  86. def __init__(self):
  87. # Create an instance of the servers list
  88. self.servers = AutoDiscovered()
  89. def get_servers_list(self):
  90. """Return the current server list (list of dict)."""
  91. return self.servers.get_servers_list()
  92. def set_server(self, server_pos, key, value):
  93. """Set the key to the value for the server_pos (position in the list)."""
  94. self.servers.set_server(server_pos, key, value)
  95. def add_service(self, zeroconf, srv_type, srv_name):
  96. """Method called when a new Zeroconf client is detected.
  97. Return True if the zeroconf client is a Glances server
  98. Note: the return code will never be used
  99. """
  100. if srv_type != zeroconf_type:
  101. return False
  102. logger.debug("Check new Zeroconf server: %s / %s" %
  103. (srv_type, srv_name))
  104. info = zeroconf.get_service_info(srv_type, srv_name)
  105. if info:
  106. new_server_ip = socket.inet_ntoa(info.address)
  107. new_server_port = info.port
  108. # Add server to the global dict
  109. self.servers.add_server(srv_name, new_server_ip, new_server_port)
  110. logger.info("New Glances server detected (%s from %s:%s)" %
  111. (srv_name, new_server_ip, new_server_port))
  112. else:
  113. logger.warning(
  114. "New Glances server detected, but Zeroconf info failed to be grabbed")
  115. return True
  116. def remove_service(self, zeroconf, srv_type, srv_name):
  117. """Remove the server from the list."""
  118. self.servers.remove_server(srv_name)
  119. logger.info(
  120. "Glances server %s removed from the autodetect list" % srv_name)
  121. class GlancesAutoDiscoverServer(object):
  122. """Implementation of the Zeroconf protocol (server side for the Glances client)."""
  123. def __init__(self, args=None):
  124. if zeroconf_tag:
  125. logger.info("Init autodiscover mode (Zeroconf protocol)")
  126. try:
  127. self.zeroconf = Zeroconf()
  128. except socket.error as e:
  129. logger.error("Cannot start Zeroconf (%s)" % e)
  130. self.zeroconf_enable_tag = False
  131. else:
  132. self.listener = GlancesAutoDiscoverListener()
  133. self.browser = ServiceBrowser(
  134. self.zeroconf, zeroconf_type, self.listener)
  135. self.zeroconf_enable_tag = True
  136. else:
  137. logger.error("Cannot start autodiscover mode (Zeroconf lib is not installed)")
  138. self.zeroconf_enable_tag = False
  139. def get_servers_list(self):
  140. """Return the current server list (dict of dict)."""
  141. if zeroconf_tag and self.zeroconf_enable_tag:
  142. return self.listener.get_servers_list()
  143. else:
  144. return []
  145. def set_server(self, server_pos, key, value):
  146. """Set the key to the value for the server_pos (position in the list)."""
  147. if zeroconf_tag and self.zeroconf_enable_tag:
  148. self.listener.set_server(server_pos, key, value)
  149. def close(self):
  150. if zeroconf_tag and self.zeroconf_enable_tag:
  151. self.zeroconf.close()
  152. class GlancesAutoDiscoverClient(object):
  153. """Implementation of the zeroconf protocol (client side for the Glances server)."""
  154. def __init__(self, hostname, args=None):
  155. if zeroconf_tag:
  156. zeroconf_bind_address = args.bind_address
  157. try:
  158. self.zeroconf = Zeroconf()
  159. except socket.error as e:
  160. logger.error("Cannot start zeroconf: {}".format(e))
  161. # XXX *BSDs: Segmentation fault (core dumped)
  162. # -- https://bitbucket.org/al45tair/netifaces/issues/15
  163. if not BSD:
  164. try:
  165. # -B @ overwrite the dynamic IPv4 choice
  166. if zeroconf_bind_address == '0.0.0.0':
  167. zeroconf_bind_address = self.find_active_ip_address()
  168. except KeyError:
  169. # Issue #528 (no network interface available)
  170. pass
  171. # Check IP v4/v6
  172. address_family = socket.getaddrinfo(zeroconf_bind_address, args.port)[0][0]
  173. # Start the zeroconf service
  174. self.info = ServiceInfo(
  175. zeroconf_type, '{}:{}.{}'.format(hostname, args.port, zeroconf_type),
  176. address=socket.inet_pton(address_family, zeroconf_bind_address),
  177. port=args.port, weight=0, priority=0, properties={}, server=hostname)
  178. try:
  179. self.zeroconf.register_service(self.info)
  180. except socket.error as e:
  181. logger.error("Error while announcing Glances server: {}".format(e))
  182. else:
  183. print("Announce the Glances server on the LAN (using {} IP address)".format(zeroconf_bind_address))
  184. else:
  185. logger.error("Cannot announce Glances server on the network: zeroconf library not found.")
  186. @staticmethod
  187. def find_active_ip_address():
  188. """Try to find the active IP addresses."""
  189. import netifaces
  190. # Interface of the default gateway
  191. gateway_itf = netifaces.gateways()['default'][netifaces.AF_INET][1]
  192. # IP address for the interface
  193. return netifaces.ifaddresses(gateway_itf)[netifaces.AF_INET][0]['addr']
  194. def close(self):
  195. if zeroconf_tag:
  196. self.zeroconf.unregister_service(self.info)
  197. self.zeroconf.close()