Test Failed
Push — develop ( abf64f...1d1151 )
by Nicolas
02:59
created

glances/autodiscover.py (2 issues)

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2019 Nicolargo <[email protected]>
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
22
import socket
23
import sys
24
25
from glances.globals import BSD
26
from glances.logger import logger
27
28
try:
29
    from zeroconf import (
30
        __version__ as __zeroconf_version,
31
        ServiceBrowser,
32
        ServiceInfo,
33
        Zeroconf
34
    )
35
    zeroconf_tag = True
36
except ImportError:
37
    zeroconf_tag = False
38
39
# Zeroconf 0.17 or higher is needed
40
if 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
51
zeroconf_type = "_%s._tcp.local." % 'glances'
52
53
54
class 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
100
class 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
147
class 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
185
class 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))
0 ignored issues
show
print statement used
Loading history...
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
0 ignored issues
show
Unable to import 'netifaces'
Loading history...
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