Test Failed
Push — develop ( d0629e...eebab0 )
by Nicolas
03:11
created

glances.servers_list_dynamic   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 152
dl 0
loc 247
rs 8.72
c 0
b 0
f 0
wmc 46

17 Methods

Rating   Name   Duplication   Size   Complexity  
A GlancesAutoDiscoverListener.get_servers_list() 0 3 1
A GlancesAutoDiscoverClient.find_active_ip_address() 0 9 1
A GlancesAutoDiscoverServer.__init__() 0 15 4
A GlancesAutoDiscoverListener.remove_service() 0 4 1
C GlancesAutoDiscoverClient.__init__() 0 58 11
A AutoDiscovered.__init__() 0 4 1
A AutoDiscovered.add_server() 0 15 1
A GlancesAutoDiscoverListener.__init__() 0 3 1
A GlancesAutoDiscoverClient.close() 0 4 2
A AutoDiscovered.remove_server() 0 10 4
A GlancesAutoDiscoverServer.close() 0 3 3
A AutoDiscovered.set_server() 0 3 1
B GlancesAutoDiscoverListener.add_service() 0 30 7
A GlancesAutoDiscoverServer.get_servers_list() 0 5 3
A GlancesAutoDiscoverServer.set_server() 0 4 3
A AutoDiscovered.get_servers_list() 0 3 1
A GlancesAutoDiscoverListener.set_server() 0 3 1

How to fix   Complexity   

Complexity

Complex classes like glances.servers_list_dynamic often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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