~openstack-charmers/charms/precise/quantum-gateway/trunk

« back to all changes in this revision

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

  • Committer: james.page at ubuntu
  • Date: 2015-08-10 16:38:06 UTC
  • Revision ID: james.page@ubuntu.com-20150810163806-dccx0q0xxi6ddy2l
Tags: 15.07
[gnuoy] 15.07 Charm release

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