~quobyte/charms/trusty/quobyte-webconsole/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/network/ip.py

  • Committer: Bruno Ranieri
  • Date: 2016-07-13 14:50:01 UTC
  • Revision ID: bruno@quobyte.com-20160713145001-1h6cddu9sltlvx7w
Initial charm

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
5
# charm-helpers is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3 as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# charm-helpers is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
import glob
 
18
import re
 
19
import subprocess
 
20
import six
 
21
import socket
 
22
 
 
23
from functools import partial
 
24
 
 
25
from charmhelpers.core.hookenv import unit_get
 
26
from charmhelpers.fetch import apt_install, apt_update
 
27
from charmhelpers.core.hookenv import (
 
28
    log,
 
29
    WARNING,
 
30
)
 
31
 
 
32
try:
 
33
    import netifaces
 
34
except ImportError:
 
35
    apt_update(fatal=True)
 
36
    apt_install('python-netifaces', fatal=True)
 
37
    import netifaces
 
38
 
 
39
try:
 
40
    import netaddr
 
41
except ImportError:
 
42
    apt_update(fatal=True)
 
43
    apt_install('python-netaddr', fatal=True)
 
44
    import netaddr
 
45
 
 
46
 
 
47
def _validate_cidr(network):
 
48
    try:
 
49
        netaddr.IPNetwork(network)
 
50
    except (netaddr.core.AddrFormatError, ValueError):
 
51
        raise ValueError("Network (%s) is not in CIDR presentation format" %
 
52
                         network)
 
53
 
 
54
 
 
55
def no_ip_found_error_out(network):
 
56
    errmsg = ("No IP address found in network(s): %s" % network)
 
57
    raise ValueError(errmsg)
 
58
 
 
59
 
 
60
def get_address_in_network(network, fallback=None, fatal=False):
 
61
    """Get an IPv4 or IPv6 address within the network from the host.
 
62
 
 
63
    :param network (str): CIDR presentation format. For example,
 
64
        '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
 
65
    :param fallback (str): If no address is found, return fallback.
 
66
    :param fatal (boolean): If no address is found, fallback is not
 
67
        set and fatal is True then exit(1).
 
68
    """
 
69
    if network is None:
 
70
        if fallback is not None:
 
71
            return fallback
 
72
 
 
73
        if fatal:
 
74
            no_ip_found_error_out(network)
 
75
        else:
 
76
            return None
 
77
 
 
78
    networks = network.split() or [network]
 
79
    for network in networks:
 
80
        _validate_cidr(network)
 
81
        network = netaddr.IPNetwork(network)
 
82
        for iface in netifaces.interfaces():
 
83
            addresses = netifaces.ifaddresses(iface)
 
84
            if network.version == 4 and netifaces.AF_INET in addresses:
 
85
                addr = addresses[netifaces.AF_INET][0]['addr']
 
86
                netmask = addresses[netifaces.AF_INET][0]['netmask']
 
87
                cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
 
88
                if cidr in network:
 
89
                    return str(cidr.ip)
 
90
 
 
91
            if network.version == 6 and netifaces.AF_INET6 in addresses:
 
92
                for addr in addresses[netifaces.AF_INET6]:
 
93
                    if not addr['addr'].startswith('fe80'):
 
94
                        cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
 
95
                                                            addr['netmask']))
 
96
                        if cidr in network:
 
97
                            return str(cidr.ip)
 
98
 
 
99
    if fallback is not None:
 
100
        return fallback
 
101
 
 
102
    if fatal:
 
103
        no_ip_found_error_out(network)
 
104
 
 
105
    return None
 
106
 
 
107
 
 
108
def is_ipv6(address):
 
109
    """Determine whether provided address is IPv6 or not."""
 
110
    try:
 
111
        address = netaddr.IPAddress(address)
 
112
    except netaddr.AddrFormatError:
 
113
        # probably a hostname - so not an address at all!
 
114
        return False
 
115
 
 
116
    return address.version == 6
 
117
 
 
118
 
 
119
def is_address_in_network(network, address):
 
120
    """
 
121
    Determine whether the provided address is within a network range.
 
122
 
 
123
    :param network (str): CIDR presentation format. For example,
 
124
        '192.168.1.0/24'.
 
125
    :param address: An individual IPv4 or IPv6 address without a net
 
126
        mask or subnet prefix. For example, '192.168.1.1'.
 
127
    :returns boolean: Flag indicating whether address is in network.
 
128
    """
 
129
    try:
 
130
        network = netaddr.IPNetwork(network)
 
131
    except (netaddr.core.AddrFormatError, ValueError):
 
132
        raise ValueError("Network (%s) is not in CIDR presentation format" %
 
133
                         network)
 
134
 
 
135
    try:
 
136
        address = netaddr.IPAddress(address)
 
137
    except (netaddr.core.AddrFormatError, ValueError):
 
138
        raise ValueError("Address (%s) is not in correct presentation format" %
 
139
                         address)
 
140
 
 
141
    if address in network:
 
142
        return True
 
143
    else:
 
144
        return False
 
145
 
 
146
 
 
147
def _get_for_address(address, key):
 
148
    """Retrieve an attribute of or the physical interface that
 
149
    the IP address provided could be bound to.
 
150
 
 
151
    :param address (str): An individual IPv4 or IPv6 address without a net
 
152
        mask or subnet prefix. For example, '192.168.1.1'.
 
153
    :param key: 'iface' for the physical interface name or an attribute
 
154
        of the configured interface, for example 'netmask'.
 
155
    :returns str: Requested attribute or None if address is not bindable.
 
156
    """
 
157
    address = netaddr.IPAddress(address)
 
158
    for iface in netifaces.interfaces():
 
159
        addresses = netifaces.ifaddresses(iface)
 
160
        if address.version == 4 and netifaces.AF_INET in addresses:
 
161
            addr = addresses[netifaces.AF_INET][0]['addr']
 
162
            netmask = addresses[netifaces.AF_INET][0]['netmask']
 
163
            network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
 
164
            cidr = network.cidr
 
165
            if address in cidr:
 
166
                if key == 'iface':
 
167
                    return iface
 
168
                else:
 
169
                    return addresses[netifaces.AF_INET][0][key]
 
170
 
 
171
        if address.version == 6 and netifaces.AF_INET6 in addresses:
 
172
            for addr in addresses[netifaces.AF_INET6]:
 
173
                if not addr['addr'].startswith('fe80'):
 
174
                    network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
 
175
                                                           addr['netmask']))
 
176
                    cidr = network.cidr
 
177
                    if address in cidr:
 
178
                        if key == 'iface':
 
179
                            return iface
 
180
                        elif key == 'netmask' and cidr:
 
181
                            return str(cidr).split('/')[1]
 
182
                        else:
 
183
                            return addr[key]
 
184
 
 
185
    return None
 
186
 
 
187
 
 
188
get_iface_for_address = partial(_get_for_address, key='iface')
 
189
 
 
190
 
 
191
get_netmask_for_address = partial(_get_for_address, key='netmask')
 
192
 
 
193
 
 
194
def format_ipv6_addr(address):
 
195
    """If address is IPv6, wrap it in '[]' otherwise return None.
 
196
 
 
197
    This is required by most configuration files when specifying IPv6
 
198
    addresses.
 
199
    """
 
200
    if is_ipv6(address):
 
201
        return "[%s]" % address
 
202
 
 
203
    return None
 
204
 
 
205
 
 
206
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
 
207
                   fatal=True, exc_list=None):
 
208
    """Return the assigned IP address for a given interface, if any."""
 
209
    # Extract nic if passed /dev/ethX
 
210
    if '/' in iface:
 
211
        iface = iface.split('/')[-1]
 
212
 
 
213
    if not exc_list:
 
214
        exc_list = []
 
215
 
 
216
    try:
 
217
        inet_num = getattr(netifaces, inet_type)
 
218
    except AttributeError:
 
219
        raise Exception("Unknown inet type '%s'" % str(inet_type))
 
220
 
 
221
    interfaces = netifaces.interfaces()
 
222
    if inc_aliases:
 
223
        ifaces = []
 
224
        for _iface in interfaces:
 
225
            if iface == _iface or _iface.split(':')[0] == iface:
 
226
                ifaces.append(_iface)
 
227
 
 
228
        if fatal and not ifaces:
 
229
            raise Exception("Invalid interface '%s'" % iface)
 
230
 
 
231
        ifaces.sort()
 
232
    else:
 
233
        if iface not in interfaces:
 
234
            if fatal:
 
235
                raise Exception("Interface '%s' not found " % (iface))
 
236
            else:
 
237
                return []
 
238
 
 
239
        else:
 
240
            ifaces = [iface]
 
241
 
 
242
    addresses = []
 
243
    for netiface in ifaces:
 
244
        net_info = netifaces.ifaddresses(netiface)
 
245
        if inet_num in net_info:
 
246
            for entry in net_info[inet_num]:
 
247
                if 'addr' in entry and entry['addr'] not in exc_list:
 
248
                    addresses.append(entry['addr'])
 
249
 
 
250
    if fatal and not addresses:
 
251
        raise Exception("Interface '%s' doesn't have any %s addresses." %
 
252
                        (iface, inet_type))
 
253
 
 
254
    return sorted(addresses)
 
255
 
 
256
 
 
257
get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
 
258
 
 
259
 
 
260
def get_iface_from_addr(addr):
 
261
    """Work out on which interface the provided address is configured."""
 
262
    for iface in netifaces.interfaces():
 
263
        addresses = netifaces.ifaddresses(iface)
 
264
        for inet_type in addresses:
 
265
            for _addr in addresses[inet_type]:
 
266
                _addr = _addr['addr']
 
267
                # link local
 
268
                ll_key = re.compile("(.+)%.*")
 
269
                raw = re.match(ll_key, _addr)
 
270
                if raw:
 
271
                    _addr = raw.group(1)
 
272
 
 
273
                if _addr == addr:
 
274
                    log("Address '%s' is configured on iface '%s'" %
 
275
                        (addr, iface))
 
276
                    return iface
 
277
 
 
278
    msg = "Unable to infer net iface on which '%s' is configured" % (addr)
 
279
    raise Exception(msg)
 
280
 
 
281
 
 
282
def sniff_iface(f):
 
283
    """Ensure decorated function is called with a value for iface.
 
284
 
 
285
    If no iface provided, inject net iface inferred from unit private address.
 
286
    """
 
287
    def iface_sniffer(*args, **kwargs):
 
288
        if not kwargs.get('iface', None):
 
289
            kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
 
290
 
 
291
        return f(*args, **kwargs)
 
292
 
 
293
    return iface_sniffer
 
294
 
 
295
 
 
296
@sniff_iface
 
297
def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
 
298
                  dynamic_only=True):
 
299
    """Get assigned IPv6 address for a given interface.
 
300
 
 
301
    Returns list of addresses found. If no address found, returns empty list.
 
302
 
 
303
    If iface is None, we infer the current primary interface by doing a reverse
 
304
    lookup on the unit private-address.
 
305
 
 
306
    We currently only support scope global IPv6 addresses i.e. non-temporary
 
307
    addresses. If no global IPv6 address is found, return the first one found
 
308
    in the ipv6 address list.
 
309
    """
 
310
    addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
 
311
                               inc_aliases=inc_aliases, fatal=fatal,
 
312
                               exc_list=exc_list)
 
313
 
 
314
    if addresses:
 
315
        global_addrs = []
 
316
        for addr in addresses:
 
317
            key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
 
318
            m = re.match(key_scope_link_local, addr)
 
319
            if m:
 
320
                eui_64_mac = m.group(1)
 
321
                iface = m.group(2)
 
322
            else:
 
323
                global_addrs.append(addr)
 
324
 
 
325
        if global_addrs:
 
326
            # Make sure any found global addresses are not temporary
 
327
            cmd = ['ip', 'addr', 'show', iface]
 
328
            out = subprocess.check_output(cmd).decode('UTF-8')
 
329
            if dynamic_only:
 
330
                key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
 
331
            else:
 
332
                key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
 
333
 
 
334
            addrs = []
 
335
            for line in out.split('\n'):
 
336
                line = line.strip()
 
337
                m = re.match(key, line)
 
338
                if m and 'temporary' not in line:
 
339
                    # Return the first valid address we find
 
340
                    for addr in global_addrs:
 
341
                        if m.group(1) == addr:
 
342
                            if not dynamic_only or \
 
343
                                    m.group(1).endswith(eui_64_mac):
 
344
                                addrs.append(addr)
 
345
 
 
346
            if addrs:
 
347
                return addrs
 
348
 
 
349
    if fatal:
 
350
        raise Exception("Interface '%s' does not have a scope global "
 
351
                        "non-temporary ipv6 address." % iface)
 
352
 
 
353
    return []
 
354
 
 
355
 
 
356
def get_bridges(vnic_dir='/sys/devices/virtual/net'):
 
357
    """Return a list of bridges on the system."""
 
358
    b_regex = "%s/*/bridge" % vnic_dir
 
359
    return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
 
360
 
 
361
 
 
362
def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
 
363
    """Return a list of nics comprising a given bridge on the system."""
 
364
    brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
 
365
    return [x.split('/')[-1] for x in glob.glob(brif_regex)]
 
366
 
 
367
 
 
368
def is_bridge_member(nic):
 
369
    """Check if a given nic is a member of a bridge."""
 
370
    for bridge in get_bridges():
 
371
        if nic in get_bridge_nics(bridge):
 
372
            return True
 
373
 
 
374
    return False
 
375
 
 
376
 
 
377
def is_ip(address):
 
378
    """
 
379
    Returns True if address is a valid IP address.
 
380
    """
 
381
    try:
 
382
        # Test to see if already an IPv4 address
 
383
        socket.inet_aton(address)
 
384
        return True
 
385
    except socket.error:
 
386
        return False
 
387
 
 
388
 
 
389
def ns_query(address):
 
390
    try:
 
391
        import dns.resolver
 
392
    except ImportError:
 
393
        apt_install('python-dnspython')
 
394
        import dns.resolver
 
395
 
 
396
    if isinstance(address, dns.name.Name):
 
397
        rtype = 'PTR'
 
398
    elif isinstance(address, six.string_types):
 
399
        rtype = 'A'
 
400
    else:
 
401
        return None
 
402
 
 
403
    answers = dns.resolver.query(address, rtype)
 
404
    if answers:
 
405
        return str(answers[0])
 
406
    return None
 
407
 
 
408
 
 
409
def get_host_ip(hostname, fallback=None):
 
410
    """
 
411
    Resolves the IP for a given hostname, or returns
 
412
    the input if it is already an IP.
 
413
    """
 
414
    if is_ip(hostname):
 
415
        return hostname
 
416
 
 
417
    ip_addr = ns_query(hostname)
 
418
    if not ip_addr:
 
419
        try:
 
420
            ip_addr = socket.gethostbyname(hostname)
 
421
        except:
 
422
            log("Failed to resolve hostname '%s'" % (hostname),
 
423
                level=WARNING)
 
424
            return fallback
 
425
    return ip_addr
 
426
 
 
427
 
 
428
def get_hostname(address, fqdn=True):
 
429
    """
 
430
    Resolves hostname for given IP, or returns the input
 
431
    if it is already a hostname.
 
432
    """
 
433
    if is_ip(address):
 
434
        try:
 
435
            import dns.reversename
 
436
        except ImportError:
 
437
            apt_install("python-dnspython")
 
438
            import dns.reversename
 
439
 
 
440
        rev = dns.reversename.from_address(address)
 
441
        result = ns_query(rev)
 
442
 
 
443
        if not result:
 
444
            try:
 
445
                result = socket.gethostbyaddr(address)[0]
 
446
            except:
 
447
                return None
 
448
    else:
 
449
        result = address
 
450
 
 
451
    if fqdn:
 
452
        # strip trailing .
 
453
        if result.endswith('.'):
 
454
            return result[:-1]
 
455
        else:
 
456
            return result
 
457
    else:
 
458
        return result.split('.')[0]