glances_autodiscover.py 8.5 KB
Newer Older
N
Nicolargo 已提交
1 2 3 4
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
5
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
N
Nicolargo 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#
# 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 system libs
import socket
24
import sys
25

N
Nicolargo 已提交
26
try:
A
Alessio Sergi 已提交
27 28 29 30 31 32
    from zeroconf import (
        __version__ as __zeroconf_version,
        ServiceBrowser,
        ServiceInfo,
        Zeroconf
    )
N
Nicolargo 已提交
33 34 35 36 37
    zeroconf_tag = True
except ImportError:
    zeroconf_tag = False

# Import Glances libs
A
Alessio Sergi 已提交
38 39
from glances.core.glances_globals import appname
from glances.core.glances_logging import logger
N
Nicolargo 已提交
40

41
# Zeroconf 0.17 or higher is needed
42
if zeroconf_tag:
43
    zeroconf_min_version = (0, 17, 0)
44 45 46
    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:
47
        logger.critical("Please install zeroconf 0.17 or higher.")
48
        sys.exit(1)
49

N
Nicolargo 已提交
50 51 52 53 54
# Global var
zeroconf_type = "_%s._tcp." % appname


class AutoDiscovered(object):
N
Nicolargo 已提交
55

A
PEP 257  
Alessio Sergi 已提交
56
    """Class to manage the auto discovered servers dict."""
N
Nicolargo 已提交
57 58

    def __init__(self):
59 60 61
        # 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 = []
N
Nicolargo 已提交
62 63

    def get_servers_list(self):
A
PEP 257  
Alessio Sergi 已提交
64
        """Return the current server list (list of dict)."""
65
        return self._server_list
N
Nicolargo 已提交
66

67
    def set_server(self, server_pos, key, value):
A
PEP 257  
Alessio Sergi 已提交
68
        """Set the key to the value for the server_pos (position in the list)."""
69 70
        self._server_list[server_pos][key] = value

N
Nicolargo 已提交
71
    def add_server(self, name, ip, port):
A
PEP 257  
Alessio Sergi 已提交
72
        """Add a new server to the list."""
N
Nicolargo 已提交
73 74 75 76
        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
N
Nicolargo 已提交
77 78 79 80
                      'username': 'glances',  # Default username
                      'password': '',  # Default password
                      'status': 'UNKNOWN',  # Server status: 'UNKNOWN', 'OFFLINE', 'ONLINE', 'PROTECTED'
                      'type': 'DYNAMIC',  # Server type: 'STATIC' or 'DYNAMIC'
N
Nicolargo 已提交
81
                      }
82
        self._server_list.append(new_server)
N
Nicolargo 已提交
83 84
        logger.debug("Updated servers list (%s servers): %s" %
                     (len(self._server_list), self._server_list))
N
Nicolargo 已提交
85 86

    def remove_server(self, name):
A
PEP 257  
Alessio Sergi 已提交
87
        """Remove a server from the dict."""
88 89 90 91 92
        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)
N
Nicolargo 已提交
93 94
                    logger.debug("Updated servers list (%s servers): %s" % (
                        len(self._server_list), self._server_list))
95
                except ValueError:
N
Nicolargo 已提交
96
                    logger.error(
A
Alessio Sergi 已提交
97
                        "Cannot remove server %s from the list" % name)
N
Nicolargo 已提交
98 99 100


class GlancesAutoDiscoverListener(object):
N
Nicolargo 已提交
101

A
PEP 257  
Alessio Sergi 已提交
102
    """Zeroconf listener for Glances server."""
N
Nicolargo 已提交
103 104 105 106 107 108

    def __init__(self):
        # Create an instance of the servers list
        self.servers = AutoDiscovered()

    def get_servers_list(self):
A
PEP 257  
Alessio Sergi 已提交
109
        """Return the current server list (list of dict)."""
N
Nicolargo 已提交
110 111
        return self.servers.get_servers_list()

112
    def set_server(self, server_pos, key, value):
A
PEP 257  
Alessio Sergi 已提交
113
        """Set the key to the value for the server_pos (position in the list)."""
114 115
        self.servers.set_server(server_pos, key, value)

116
    def add_service(self, zeroconf, srv_type, srv_name):
A
PEP 257  
Alessio Sergi 已提交
117 118
        """Method called when a new Zeroconf client is detected.

N
Nicolargo 已提交
119 120 121 122 123
        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
N
Nicolargo 已提交
124 125
        logger.debug("Check new Zeroconf server: %s / %s" %
                     (srv_type, srv_name))
126
        info = zeroconf.get_service_info(srv_type, srv_name)
N
Nicolargo 已提交
127
        if info:
128 129
            new_server_ip = socket.inet_ntoa(info.address)
            new_server_port = info.port
N
Nicolargo 已提交
130 131 132

            # Add server to the global dict
            self.servers.add_server(srv_name, new_server_ip, new_server_port)
N
Nicolargo 已提交
133 134
            logger.info("New Glances server detected (%s from %s:%s)" %
                        (srv_name, new_server_ip, new_server_port))
N
Nicolargo 已提交
135
        else:
N
Nicolargo 已提交
136 137
            logger.warning(
                "New Glances server detected, but Zeroconf info failed to be grabbed")
N
Nicolargo 已提交
138 139
        return True

140
    def remove_service(self, zeroconf, srv_type, srv_name):
A
PEP 257  
Alessio Sergi 已提交
141
        """Remove the server from the list."""
N
Nicolargo 已提交
142
        self.servers.remove_server(srv_name)
N
Nicolargo 已提交
143 144
        logger.info(
            "Glances server %s removed from the autodetect list" % srv_name)
N
Nicolargo 已提交
145 146 147


class GlancesAutoDiscoverServer(object):
N
Nicolargo 已提交
148

A
PEP 257  
Alessio Sergi 已提交
149
    """Implementation of the Zeroconf protocol (server side for the Glances client)."""
N
Nicolargo 已提交
150 151 152 153

    def __init__(self, args=None):
        if zeroconf_tag:
            logger.info("Init autodiscover mode (Zeroconf protocol)")
154 155
            try:
                self.zeroconf = Zeroconf()
156
            except socket.error as e:
A
Alessio Sergi 已提交
157
                logger.error("Cannot start Zeroconf (%s)" % e)
158
                self.zeroconf_enable_tag = False
159
            else:
160 161 162 163
                self.listener = GlancesAutoDiscoverListener()
                self.browser = ServiceBrowser(
                    self.zeroconf, zeroconf_type, self.listener)
                self.zeroconf_enable_tag = True
N
Nicolargo 已提交
164
        else:
A
Alessio Sergi 已提交
165
            logger.error("Cannot start autodiscover mode (Zeroconf lib is not installed)")
166
            self.zeroconf_enable_tag = False
N
Nicolargo 已提交
167 168

    def get_servers_list(self):
A
PEP 257  
Alessio Sergi 已提交
169
        """Return the current server list (dict of dict)."""
170
        if zeroconf_tag and self.zeroconf_enable_tag:
N
Nicolargo 已提交
171 172
            return self.listener.get_servers_list()
        else:
N
Nicolargo 已提交
173
            return []
N
Nicolargo 已提交
174

175
    def set_server(self, server_pos, key, value):
A
PEP 257  
Alessio Sergi 已提交
176
        """Set the key to the value for the server_pos (position in the list)."""
177 178
        if zeroconf_tag and self.zeroconf_enable_tag:
            self.listener.set_server(server_pos, key, value)
179

N
Nicolargo 已提交
180
    def close(self):
181
        if zeroconf_tag and self.zeroconf_enable_tag:
N
Nicolargo 已提交
182 183 184 185
            self.zeroconf.close()


class GlancesAutoDiscoverClient(object):
N
Nicolargo 已提交
186

187
    """Implementation of the zeroconf protocol (client side for the Glances server)."""
N
Nicolargo 已提交
188 189

    def __init__(self, hostname, args=None):
190 191 192 193 194 195
        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))
N
Nicolargo 已提交
196

197
            try:
198 199 200
                # -B @ overwrite the dynamic IPv4 choice
                if zeroconf_bind_address == '0.0.0.0':
                    zeroconf_bind_address = self.find_active_ip_address()
201 202 203
            except KeyError:
                # Issue #528 (no network interface available)
                pass
204

205 206 207 208 209 210
            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)
N
Nicolargo 已提交
211
        else:
212
            logger.error("Cannot announce Glances server on the network: zeroconf library not found.")
N
Nicolargo 已提交
213

214 215
    @staticmethod
    def find_active_ip_address():
216
        """Try to find the active IP addresses."""
217 218 219 220 221
        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']
222

N
Nicolargo 已提交
223 224
    def close(self):
        if zeroconf_tag:
225
            self.zeroconf.unregister_service(self.info)
N
Nicolargo 已提交
226
            self.zeroconf.close()