oslo.utils/oslo_utils/netutils.py
Derek Higgins 785b33c295 Add method to escape ipv6 ip addresses
IPv6 addresses are commonly enclosed in square brackets
in resource identifiers to allow them to be distinguished
from port numbers.

I've come accross the need of this in multiple places
zaqar https://review.openstack.org/#/c/495279
tripleo https://review.openstack.org/#/c/494440
ironic https://review.openstack.org/#/c/411809
ironic-python-agent https://review.openstack.org/#/c/411817

Change-Id: Icdcf681c1d71a09b88b029f80d13cd5015dacd56
2017-08-18 12:50:08 +01:00

468 lines
14 KiB
Python

# Copyright 2012 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Network-related utilities and helper functions.
"""
import logging
import os
import re
import socket
import netaddr
import netifaces
import six
from six.moves.urllib import parse
from oslo_utils._i18n import _
LOG = logging.getLogger(__name__)
_IS_IPV6_ENABLED = None
def parse_host_port(address, default_port=None):
"""Interpret a string as a host:port pair.
An IPv6 address MUST be escaped if accompanied by a port,
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
means both [2001:db8:85a3::8a2e:370:7334] and
[2001:db8:85a3::8a2e:370]:7334.
>>> parse_host_port('server01:80')
('server01', 80)
>>> parse_host_port('server01')
('server01', None)
>>> parse_host_port('server01', default_port=1234)
('server01', 1234)
>>> parse_host_port('[::1]:80')
('::1', 80)
>>> parse_host_port('[::1]')
('::1', None)
>>> parse_host_port('[::1]', default_port=1234)
('::1', 1234)
>>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234)
('2001:db8:85a3::8a2e:370:7334', 1234)
>>> parse_host_port(None)
(None, None)
"""
if not address:
return (None, None)
if address[0] == '[':
# Escaped ipv6
_host, _port = address[1:].split(']')
host = _host
if ':' in _port:
port = _port.split(':')[1]
else:
port = default_port
else:
if address.count(':') == 1:
host, port = address.split(':')
else:
# 0 means ipv4, >1 means ipv6.
# We prohibit unescaped ipv6 addresses with port.
host = address
port = default_port
return (host, None if port is None else int(port))
def is_valid_ipv4(address):
"""Verify that address represents a valid IPv4 address.
:param address: Value to verify
:type address: string
:returns: bool
.. versionadded:: 1.1
"""
try:
return netaddr.valid_ipv4(address)
except netaddr.AddrFormatError:
return False
def is_valid_ipv6(address):
"""Verify that address represents a valid IPv6 address.
:param address: Value to verify
:type address: string
:returns: bool
.. versionadded:: 1.1
"""
if not address:
return False
parts = address.rsplit("%", 1)
address = parts[0]
scope = parts[1] if len(parts) > 1 else None
if scope is not None and (len(scope) < 1 or len(scope) > 15):
return False
try:
return netaddr.valid_ipv6(address, netaddr.core.INET_PTON)
except netaddr.AddrFormatError:
return False
def is_valid_cidr(address):
"""Verify that address represents a valid CIDR address.
:param address: Value to verify
:type address: string
:returns: bool
.. versionadded:: 3.8
"""
try:
# Validate the correct CIDR Address
netaddr.IPNetwork(address)
except (TypeError, netaddr.AddrFormatError):
return False
# Prior validation partially verify /xx part
# Verify it here
ip_segment = address.split('/')
if (len(ip_segment) <= 1 or
ip_segment[1] == ''):
return False
return True
def is_valid_ipv6_cidr(address):
"""Verify that address represents a valid IPv6 CIDR address.
:param address: address to verify
:type address: string
:returns: true if address is valid, false otherwise
.. versionadded:: 3.17
"""
try:
netaddr.IPNetwork(address, version=6).cidr
return True
except (TypeError, netaddr.AddrFormatError):
return False
def get_ipv6_addr_by_EUI64(prefix, mac):
"""Calculate IPv6 address using EUI-64 specification.
This method calculates the IPv6 address using the EUI-64
addressing scheme as explained in rfc2373.
:param prefix: IPv6 prefix.
:param mac: IEEE 802 48-bit MAC address.
:returns: IPv6 address on success.
:raises ValueError, TypeError: For any invalid input.
.. versionadded:: 1.4
"""
# Check if the prefix is an IPv4 address
if is_valid_ipv4(prefix):
msg = _("Unable to generate IP address by EUI64 for IPv4 prefix")
raise ValueError(msg)
try:
eui64 = int(netaddr.EUI(mac).eui64())
prefix = netaddr.IPNetwork(prefix)
return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57))
except (ValueError, netaddr.AddrFormatError):
raise ValueError(_('Bad prefix or mac format for generating IPv6 '
'address by EUI-64: %(prefix)s, %(mac)s:')
% {'prefix': prefix, 'mac': mac})
except TypeError:
raise TypeError(_('Bad prefix type for generating IPv6 address by '
'EUI-64: %s') % prefix)
def is_ipv6_enabled():
"""Check if IPv6 support is enabled on the platform.
This api will look into the proc entries of the platform to figure
out the status of IPv6 support on the platform.
:returns: True if the platform has IPv6 support, False otherwise.
.. versionadded:: 1.4
"""
global _IS_IPV6_ENABLED
if _IS_IPV6_ENABLED is None:
disabled_ipv6_path = "/proc/sys/net/ipv6/conf/default/disable_ipv6"
if os.path.exists(disabled_ipv6_path):
with open(disabled_ipv6_path, 'r') as f:
disabled = f.read().strip()
_IS_IPV6_ENABLED = disabled == "0"
else:
_IS_IPV6_ENABLED = False
return _IS_IPV6_ENABLED
def escape_ipv6(address):
"""Escape an IP address in square brackets if IPv6
:param address: address to optionaly escape
:type address: string
:returns: string
.. versionadded:: 3.29.0
"""
if is_valid_ipv6(address):
return "[%s]" % address
return address
def is_valid_ip(address):
"""Verify that address represents a valid IP address.
:param address: Value to verify
:type address: string
:returns: bool
.. versionadded:: 1.1
"""
return is_valid_ipv4(address) or is_valid_ipv6(address)
def is_valid_mac(address):
"""Verify the format of a MAC address.
Check if a MAC address is valid and contains six octets. Accepts
colon-separated format only.
:param address: MAC address to be validated.
:returns: True if valid. False if not.
.. versionadded:: 3.17
"""
m = "[0-9a-f]{2}(:[0-9a-f]{2}){5}$"
return (isinstance(address, six.string_types) and
re.match(m, address.lower()))
def _is_int_in_range(value, start, end):
"""Try to convert value to int and check if it lies within
range 'start' to 'end'.
:param value: value to verify
:param start: start number of range
:param end: end number of range
:returns: bool
"""
try:
val = int(value)
except (ValueError, TypeError):
return False
return (start <= val <= end)
def is_valid_port(port):
"""Verify that port represents a valid port number.
Port can be valid integer having a value of 0 up to and
including 65535.
.. versionadded:: 1.1.1
"""
return _is_int_in_range(port, 0, 65535)
def is_valid_icmp_type(type):
"""Verify if ICMP type is valid.
:param type: ICMP *type* field can only be a valid integer
:returns: bool
ICMP *type* field can be valid integer having a value of 0
up to and including 255.
"""
return _is_int_in_range(type, 0, 255)
def is_valid_icmp_code(code):
"""Verify if ICMP code is valid.
:param code: ICMP *code* field can be valid integer or None
:returns: bool
ICMP *code* field can be either None or valid integer having
a value of 0 up to and including 255.
"""
if code is None:
return True
return _is_int_in_range(code, 0, 255)
def get_my_ipv4():
"""Returns the actual ipv4 of the local machine.
This code figures out what source address would be used if some traffic
were to be sent out to some well known address on the Internet. In this
case, IP from RFC5737 is used, but the specific address does not
matter much. No traffic is actually sent.
.. versionadded:: 1.1
.. versionchanged:: 1.2.1
Return ``'127.0.0.1'`` if there is no default interface.
"""
try:
csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
csock.connect(('192.0.2.0', 80))
(addr, port) = csock.getsockname()
csock.close()
return addr
except socket.error:
return _get_my_ipv4_address()
def _get_my_ipv4_address():
"""Figure out the best ipv4
"""
LOCALHOST = '127.0.0.1'
gtw = netifaces.gateways()
try:
interface = gtw['default'][netifaces.AF_INET][1]
except (KeyError, IndexError):
LOG.info('Could not determine default network interface, '
'using 127.0.0.1 for IPv4 address')
return LOCALHOST
try:
return netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr']
except (KeyError, IndexError):
LOG.info('Could not determine IPv4 address for interface %s, '
'using 127.0.0.1',
interface)
except Exception as e:
LOG.info('Could not determine IPv4 address for '
'interface %(interface)s: %(error)s',
{'interface': interface, 'error': e})
return LOCALHOST
class _ModifiedSplitResult(parse.SplitResult):
"""Split results class for urlsplit."""
def params(self, collapse=True):
"""Extracts the query parameters from the split urls components.
This method will provide back as a dictionary the query parameter
names and values that were provided in the url.
:param collapse: Boolean, turn on or off collapsing of query values
with the same name. Since a url can contain the same query parameter
name with different values it may or may not be useful for users to
care that this has happened. This parameter when True uses the
last value that was given for a given name, while if False it will
retain all values provided by associating the query parameter name with
a list of values instead of a single (non-list) value.
"""
if self.query:
if collapse:
return dict(parse.parse_qsl(self.query))
else:
params = {}
for (key, value) in parse.parse_qsl(self.query):
if key in params:
if isinstance(params[key], list):
params[key].append(value)
else:
params[key] = [params[key], value]
else:
params[key] = value
return params
else:
return {}
def urlsplit(url, scheme='', allow_fragments=True):
"""Parse a URL using urlparse.urlsplit(), splitting query and fragments.
This function papers over Python issue9374_ when needed.
.. _issue9374: http://bugs.python.org/issue9374
The parameters are the same as urlparse.urlsplit.
"""
scheme, netloc, path, query, fragment = parse.urlsplit(
url, scheme, allow_fragments)
if allow_fragments and '#' in path:
path, fragment = path.split('#', 1)
if '?' in path:
path, query = path.split('?', 1)
return _ModifiedSplitResult(scheme, netloc,
path, query, fragment)
def set_tcp_keepalive(sock, tcp_keepalive=True,
tcp_keepidle=None,
tcp_keepalive_interval=None,
tcp_keepalive_count=None):
"""Set values for tcp keepalive parameters
This function configures tcp keepalive parameters if users wish to do
so.
:param tcp_keepalive: Boolean, turn on or off tcp_keepalive. If users are
not sure, this should be True, and default values will be used.
:param tcp_keepidle: time to wait before starting to send keepalive probes
:param tcp_keepalive_interval: time between successive probes, once the
initial wait time is over
:param tcp_keepalive_count: number of probes to send before the connection
is killed
"""
# NOTE(praneshp): Despite keepalive being a tcp concept, the level is
# still SOL_SOCKET. This is a quirk.
if isinstance(tcp_keepalive, bool):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, tcp_keepalive)
else:
raise TypeError("tcp_keepalive must be a boolean")
if not tcp_keepalive:
return
# These options aren't available in the OS X version of eventlet,
# Idle + Count * Interval effectively gives you the total timeout.
if tcp_keepidle is not None:
if hasattr(socket, 'TCP_KEEPIDLE'):
sock.setsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE,
tcp_keepidle)
else:
LOG.warning('tcp_keepidle not available on your system')
if tcp_keepalive_interval is not None:
if hasattr(socket, 'TCP_KEEPINTVL'):
sock.setsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPINTVL,
tcp_keepalive_interval)
else:
LOG.warning('tcp_keepintvl not available on your system')
if tcp_keepalive_count is not None:
if hasattr(socket, 'TCP_KEEPCNT'):
sock.setsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPCNT,
tcp_keepalive_count)
else:
LOG.warning('tcp_keepcnt not available on your system')