417 lines
14 KiB
Python
417 lines
14 KiB
Python
# 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.
|
|
|
|
import errno
|
|
import socket
|
|
|
|
from neutron_lib import constants
|
|
import pyroute2
|
|
from pyroute2.netlink import rtnl
|
|
from pyroute2.netlink.rtnl import ifinfmsg
|
|
from pyroute2.netlink.rtnl import ndmsg
|
|
from pyroute2 import NetlinkError
|
|
from pyroute2 import netns
|
|
|
|
from neutron._i18n import _
|
|
from neutron import privileged
|
|
|
|
|
|
_IP_VERSION_FAMILY_MAP = {4: socket.AF_INET, 6: socket.AF_INET6}
|
|
|
|
|
|
def _get_scope_name(scope):
|
|
"""Return the name of the scope (given as a number), or the scope number
|
|
if the name is unknown.
|
|
|
|
For backward compatibility (with "ip" tool) "global" scope is converted to
|
|
"universe" before converting to number
|
|
"""
|
|
scope = 'universe' if scope == 'global' else scope
|
|
return rtnl.rt_scope.get(scope, scope)
|
|
|
|
|
|
class NetworkNamespaceNotFound(RuntimeError):
|
|
message = _("Network namespace %(netns_name)s could not be found.")
|
|
|
|
def __init__(self, netns_name):
|
|
super(NetworkNamespaceNotFound, self).__init__(
|
|
self.message % {'netns_name': netns_name})
|
|
|
|
|
|
class NetworkInterfaceNotFound(RuntimeError):
|
|
message = _("Network interface %(device)s not found in namespace "
|
|
"%(namespace)s.")
|
|
|
|
def __init__(self, message=None, device=None, namespace=None):
|
|
# NOTE(slaweq): 'message' can be passed as an optional argument
|
|
# because of how privsep daemon works. If exception is raised in
|
|
# function called by privsep daemon, it will then try to reraise it
|
|
# and will call it always with passing only message from originally
|
|
# raised exception.
|
|
message = message or self.message % {
|
|
'device': device, 'namespace': namespace}
|
|
super(NetworkInterfaceNotFound, self).__init__(message)
|
|
|
|
|
|
class InterfaceOperationNotSupported(RuntimeError):
|
|
message = _("Operation not supported on interface %(device)s, namespace "
|
|
"%(namespace)s.")
|
|
|
|
def __init__(self, message=None, device=None, namespace=None):
|
|
# NOTE(slaweq): 'message' can be passed as an optional argument
|
|
# because of how privsep daemon works. If exception is raised in
|
|
# function called by privsep daemon, it will then try to reraise it
|
|
# and will call it always with passing only message from originally
|
|
# raised exception.
|
|
message = message or self.message % {
|
|
'device': device, 'namespace': namespace}
|
|
super(InterfaceOperationNotSupported, self).__init__(message)
|
|
|
|
|
|
class IpAddressAlreadyExists(RuntimeError):
|
|
message = _("IP address %(ip)s already configured on %(device)s.")
|
|
|
|
def __init__(self, message=None, ip=None, device=None):
|
|
# NOTE(slaweq): 'message' can be passed as an optional argument
|
|
# because of how privsep daemon works. If exception is raised in
|
|
# function called by privsep daemon, it will then try to reraise it
|
|
# and will call it always with passing only message from originally
|
|
# raised exception.
|
|
message = message or self.message % {'ip': ip, 'device': device}
|
|
super(IpAddressAlreadyExists, self).__init__(message)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def get_routing_table(ip_version, namespace=None):
|
|
"""Return a list of dictionaries, each representing a route.
|
|
|
|
:param ip_version: IP version of routes to return, for example 4
|
|
:param namespace: The name of the namespace from which to get the routes
|
|
:return: a list of dictionaries, each representing a route.
|
|
The dictionary format is: {'destination': cidr,
|
|
'nexthop': ip,
|
|
'device': device_name,
|
|
'scope': scope}
|
|
"""
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
try:
|
|
netns = pyroute2.NetNS(namespace, flags=0) if namespace else None
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
with pyroute2.IPDB(nl=netns) as ipdb:
|
|
ipdb_routes = ipdb.routes
|
|
ipdb_interfaces = ipdb.interfaces
|
|
routes = [{'destination': route['dst'],
|
|
'nexthop': route.get('gateway'),
|
|
'device': ipdb_interfaces[route['oif']]['ifname'],
|
|
'scope': _get_scope_name(route['scope'])}
|
|
for route in ipdb_routes if route['family'] == family]
|
|
return routes
|
|
|
|
|
|
def _get_iproute(namespace):
|
|
# From iproute.py:
|
|
# `IPRoute` -- RTNL API to the current network namespace
|
|
# `NetNS` -- RTNL API to another network namespace
|
|
if namespace:
|
|
# do not try and create the namespace
|
|
return pyroute2.NetNS(namespace, flags=0)
|
|
else:
|
|
return pyroute2.IPRoute()
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def open_namespace(namespace):
|
|
"""Open namespace to test if the namespace is ready to be manipulated"""
|
|
with pyroute2.NetNS(namespace, flags=0):
|
|
pass
|
|
|
|
|
|
def _translate_ip_device_exception(e, device=None, namespace=None):
|
|
if e.code == errno.ENODEV:
|
|
raise NetworkInterfaceNotFound(device=device, namespace=namespace)
|
|
if e.code == errno.EOPNOTSUPP:
|
|
raise InterfaceOperationNotSupported(device=device,
|
|
namespace=namespace)
|
|
|
|
|
|
def _get_link_id(device, namespace):
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
return ip.link_lookup(ifname=device)[0]
|
|
except IndexError:
|
|
raise NetworkInterfaceNotFound(device=device, namespace=namespace)
|
|
|
|
|
|
def _run_iproute_link(command, device, namespace=None, **kwargs):
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
idx = _get_link_id(device, namespace)
|
|
return ip.link(command, index=idx, **kwargs)
|
|
except NetlinkError as e:
|
|
_translate_ip_device_exception(e, device, namespace)
|
|
raise
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
|
|
|
|
def _run_iproute_neigh(command, device, namespace, **kwargs):
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
idx = _get_link_id(device, namespace)
|
|
return ip.neigh(command, ifindex=idx, **kwargs)
|
|
except NetlinkError as e:
|
|
_translate_ip_device_exception(e, device, namespace)
|
|
raise
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
|
|
|
|
def _run_iproute_addr(command, device, namespace, **kwargs):
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
idx = _get_link_id(device, namespace)
|
|
return ip.addr(command, index=idx, **kwargs)
|
|
except NetlinkError as e:
|
|
_translate_ip_device_exception(e, device, namespace)
|
|
raise
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def add_ip_address(ip_version, ip, prefixlen, device, namespace, scope,
|
|
broadcast=None):
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
try:
|
|
_run_iproute_addr('add',
|
|
device,
|
|
namespace,
|
|
address=ip,
|
|
mask=prefixlen,
|
|
family=family,
|
|
broadcast=broadcast,
|
|
scope=_get_scope_name(scope))
|
|
except NetlinkError as e:
|
|
if e.code == errno.EEXIST:
|
|
raise IpAddressAlreadyExists(ip=ip, device=device)
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def delete_ip_address(ip_version, ip, prefixlen, device, namespace):
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
try:
|
|
_run_iproute_addr("delete",
|
|
device,
|
|
namespace,
|
|
address=ip,
|
|
mask=prefixlen,
|
|
family=family)
|
|
except NetlinkError as e:
|
|
# when trying to delete a non-existent IP address, pyroute2 raises
|
|
# NetlinkError with code EADDRNOTAVAIL (99, 'Cannot assign requested
|
|
# address')
|
|
# this shouldn't raise an error
|
|
if e.code == errno.EADDRNOTAVAIL:
|
|
return
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def flush_ip_addresses(ip_version, device, namespace):
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
idx = _get_link_id(device, namespace)
|
|
ip.flush_addr(index=idx, family=family)
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def create_interface(ifname, namespace, kind, **kwargs):
|
|
ifname = ifname[:constants.DEVICE_NAME_MAX_LEN]
|
|
try:
|
|
with _get_iproute(namespace) as ip:
|
|
physical_interface = kwargs.pop("physical_interface", None)
|
|
if physical_interface:
|
|
link_key = "vxlan_link" if kind == "vxlan" else "link"
|
|
kwargs[link_key] = _get_link_id(physical_interface, namespace)
|
|
return ip.link("add", ifname=ifname, kind=kind, **kwargs)
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
raise NetworkNamespaceNotFound(netns_name=namespace)
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def delete_interface(ifname, namespace, **kwargs):
|
|
_run_iproute_link("del", ifname, namespace, **kwargs)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def interface_exists(ifname, namespace):
|
|
try:
|
|
idx = _get_link_id(ifname, namespace)
|
|
return bool(idx)
|
|
except NetworkInterfaceNotFound:
|
|
return False
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
return False
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def set_link_flags(device, namespace, flags):
|
|
link = _run_iproute_link("get", device, namespace)[0]
|
|
new_flags = flags | link['flags']
|
|
return _run_iproute_link("set", device, namespace, flags=new_flags)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def set_link_attribute(device, namespace, **attributes):
|
|
return _run_iproute_link("set", device, namespace, **attributes)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def get_link_attributes(device, namespace):
|
|
link = _run_iproute_link("get", device, namespace)[0]
|
|
return {
|
|
'mtu': link.get_attr('IFLA_MTU'),
|
|
'qlen': link.get_attr('IFLA_TXQLEN'),
|
|
'state': link.get_attr('IFLA_OPERSTATE'),
|
|
'qdisc': link.get_attr('IFLA_QDISC'),
|
|
'brd': link.get_attr('IFLA_BROADCAST'),
|
|
'link/ether': link.get_attr('IFLA_ADDRESS'),
|
|
'alias': link.get_attr('IFLA_IFALIAS'),
|
|
'allmulticast': bool(link['flags'] & ifinfmsg.IFF_ALLMULTI)
|
|
}
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
|
|
**kwargs):
|
|
"""Add a neighbour entry.
|
|
|
|
:param ip_address: IP address of entry to add
|
|
:param mac_address: MAC address of entry to add
|
|
:param device: Device name to use in adding entry
|
|
:param namespace: The name of the namespace in which to add the entry
|
|
"""
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
_run_iproute_neigh('replace',
|
|
device,
|
|
namespace,
|
|
dst=ip_address,
|
|
lladdr=mac_address,
|
|
family=family,
|
|
state=ndmsg.states['permanent'],
|
|
**kwargs)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def delete_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
|
|
**kwargs):
|
|
"""Delete a neighbour entry.
|
|
|
|
:param ip_address: IP address of entry to delete
|
|
:param mac_address: MAC address of entry to delete
|
|
:param device: Device name to use in deleting entry
|
|
:param namespace: The name of the namespace in which to delete the entry
|
|
"""
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
try:
|
|
_run_iproute_neigh('delete',
|
|
device,
|
|
namespace,
|
|
dst=ip_address,
|
|
lladdr=mac_address,
|
|
family=family,
|
|
**kwargs)
|
|
except NetlinkError as e:
|
|
# trying to delete a non-existent entry shouldn't raise an error
|
|
if e.code == errno.ENOENT:
|
|
return
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def dump_neigh_entries(ip_version, device, namespace, **kwargs):
|
|
"""Dump all neighbour entries.
|
|
|
|
:param ip_version: IP version of entries to show (4 or 6)
|
|
:param device: Device name to use in dumping entries
|
|
:param namespace: The name of the namespace in which to dump the entries
|
|
:param kwargs: Callers add any filters they use as kwargs
|
|
:return: a list of dictionaries, each representing a neighbour.
|
|
The dictionary format is: {'dst': ip_address,
|
|
'lladdr': mac_address,
|
|
'device': device_name}
|
|
"""
|
|
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
|
entries = []
|
|
dump = _run_iproute_neigh('dump',
|
|
device,
|
|
namespace,
|
|
family=family,
|
|
**kwargs)
|
|
|
|
for entry in dump:
|
|
attrs = dict(entry['attrs'])
|
|
entries += [{'dst': attrs['NDA_DST'],
|
|
'lladdr': attrs.get('NDA_LLADDR'),
|
|
'device': device}]
|
|
return entries
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def create_netns(name, **kwargs):
|
|
"""Create a network namespace.
|
|
|
|
:param name: The name of the namespace to create
|
|
"""
|
|
try:
|
|
netns.create(name, **kwargs)
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def remove_netns(name, **kwargs):
|
|
"""Remove a network namespace.
|
|
|
|
:param name: The name of the namespace to remove
|
|
"""
|
|
netns.remove(name, **kwargs)
|
|
|
|
|
|
@privileged.default.entrypoint
|
|
def list_netns(**kwargs):
|
|
"""List network namespaces.
|
|
|
|
Caller requires raised priveleges to list namespaces
|
|
"""
|
|
return netns.listnetns(**kwargs)
|