OpenStack Networking (Neutron)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

828 lines
29 KiB

# 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 os
import socket
from neutron_lib import constants
from oslo_log import log as logging
import pyroute2
from pyroute2 import netlink
from pyroute2.netlink import exceptions as netlink_exceptions
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
from neutron.privileged.agent import linux as priv_linux
LOG = logging.getLogger(__name__)
_IP_VERSION_FAMILY_MAP = {4: socket.AF_INET, 6: socket.AF_INET6}
NETNS_RUN_DIR = '/var/run/netns'
NUD_STATES = {state[1]: state[0] for state in ndmsg.states.items()}
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)
# TODO(ralonsoh): move those exceptions out of priv_ip_lib to avoid other
# modules to import this one.
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 InvalidArgument(RuntimeError):
message = _("Invalid parameter/value used 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(InvalidArgument, 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)
class InterfaceAlreadyExists(RuntimeError):
message = _("Interface %(device)s already exists.")
def __init__(self, message=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 % {'device': device}
super(InterfaceAlreadyExists, self).__init__(message)
def _make_route_dict(destination, nexthop, device, scope):
return {'destination': destination,
'nexthop': nexthop,
'device': device,
'scope': scope}
@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
routes = []
with pyroute2.IPDB(nl=netns) as ipdb:
ipdb_routes = ipdb.routes
ipdb_interfaces = ipdb.interfaces
for route in ipdb_routes:
if route['family'] != family:
continue
dst = route['dst']
nexthop = route.get('gateway')
oif = route.get('oif')
scope = _get_scope_name(route['scope'])
# If there is not a valid outgoing interface id, check if
# this is a multipath route (i.e. same destination with
# multiple outgoing interfaces)
if oif:
device = ipdb_interfaces[oif]['ifname']
rt = _make_route_dict(dst, nexthop, device, scope)
routes.append(rt)
elif route.get('multipath'):
for mpr in route['multipath']:
oif = mpr['oif']
device = ipdb_interfaces[oif]['ifname']
rt = _make_route_dict(dst, nexthop, device, scope)
routes.append(rt)
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, libc=priv_linux.get_cdll())
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
@privileged.default.entrypoint
def list_ns_pids(namespace):
"""List namespace process PIDs
Based on Pyroute2.netns.ns_pids(). Remove when
https://github.com/svinota/pyroute2/issues/633 is fixed.
"""
ns_pids = []
try:
ns_path = os.path.join(NETNS_RUN_DIR, namespace)
ns_inode = os.stat(ns_path).st_ino
except (OSError, FileNotFoundError):
return ns_pids
for pid in os.listdir('/proc'):
if not pid.isdigit():
continue
try:
pid_path = os.path.join('/proc', pid, 'ns', 'net')
if os.stat(pid_path).st_ino == ns_inode:
ns_pids.append(int(pid))
except (OSError, FileNotFoundError):
continue
return ns_pids
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)
if e.code == errno.EINVAL:
raise InvalidArgument(device=device, namespace=namespace)
def get_link_id(device, namespace, raise_exception=True):
with get_iproute(namespace) as ip:
link_id = ip.link_lookup(ifname=device)
if not link_id or len(link_id) < 1:
if raise_exception:
raise NetworkInterfaceNotFound(device=device, namespace=namespace)
LOG.debug('Interface %(dev)s not found in namespace %(namespace)s',
{'dev': device, 'namespace': namespace})
return None
return link_id[0]
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 NetlinkError as e:
if e.code == errno.EEXIST:
raise InterfaceAlreadyExists(device=ifname)
raise
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, raise_exception=False)
return bool(idx)
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 set_link_vf_feature(device, namespace, vf_config):
return _run_iproute_link("set", device, namespace=namespace, vf=vf_config)
@privileged.default.entrypoint
def set_link_bridge_forward_delay(device, forward_delay, namespace=None):
return _run_iproute_link('set', device, namespace=namespace,
kind='bridge', br_forward_delay=forward_delay)
@privileged.default.entrypoint
def set_link_bridge_stp(device, stp, namespace=None):
return _run_iproute_link('set', device, namespace=namespace,
kind='bridge', br_stp_state=stp)
@privileged.default.entrypoint
def set_link_bridge_master(device, bridge, namespace=None):
bridge_idx = get_link_id(bridge, namespace) if bridge else 0
return _run_iproute_link('set', device, namespace=namespace,
master=bridge_idx)
@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),
'link_kind': link.get_nested('IFLA_LINKINFO', 'IFLA_INFO_KIND')
}
@privileged.default.entrypoint
def get_link_vfs(device, namespace):
link = _run_iproute_link('get', device, namespace=namespace, ext_mask=1)[0]
num_vfs = link.get_attr('IFLA_NUM_VF')
vfs = {}
if not num_vfs:
return vfs
vfinfo_list = link.get_attr('IFLA_VFINFO_LIST')
for vinfo in vfinfo_list.get_attrs('IFLA_VF_INFO'):
mac = vinfo.get_attr('IFLA_VF_MAC')
link_state = vinfo.get_attr('IFLA_VF_LINK_STATE')
vfs[mac['vf']] = {'mac': mac['mac'],
'link_state': link_state['link_state']}
return vfs
@privileged.default.entrypoint
def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
nud_state, **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
:param nud_state: The NUD (Neighbour Unreachability Detection) state of
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[nud_state],
**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.append({'dst': attrs['NDA_DST'],
'lladdr': attrs.get('NDA_LLADDR'),
'device': device,
'state': NUD_STATES[entry['state']]})
return entries
@privileged.default.entrypoint
def create_netns(name, **kwargs):
"""Create a network namespace.
:param name: The name of the namespace to create
"""
pid = os.fork()
if pid == 0:
try:
netns._create(name, libc=priv_linux.get_cdll())
except OSError as e:
if e.errno != errno.EEXIST:
os._exit(1)
except Exception:
os._exit(1)
os._exit(0)
else:
if os.waitpid(pid, 0)[1]:
raise RuntimeError(_('Error creating namespace %s' % name))
@privileged.default.entrypoint
def remove_netns(name, **kwargs):
"""Remove a network namespace.
:param name: The name of the namespace to remove
"""
try:
netns.remove(name, libc=priv_linux.get_cdll())
except OSError as e:
if e.errno != errno.ENOENT:
raise
@privileged.default.entrypoint
def list_netns(**kwargs):
"""List network namespaces.
Caller requires raised priveleges to list namespaces
"""
return netns.listnetns(**kwargs)
def make_serializable(value):
"""Make a pyroute2 object serializable
This function converts 'netlink.nla_slot' object (key, value) in a list
of two elements.
"""
def _ensure_string(value):
return value.decode() if isinstance(value, bytes) else value
if isinstance(value, list):
return [make_serializable(item) for item in value]
elif isinstance(value, netlink.nla_slot):
return [_ensure_string(value[0]), make_serializable(value[1])]
elif isinstance(value, netlink.nla_base):
return make_serializable(value.dump())
elif isinstance(value, dict):
return {_ensure_string(key): make_serializable(data)
for key, data in value.items()}
elif isinstance(value, tuple):
return tuple(make_serializable(item) for item in value)
return _ensure_string(value)
@privileged.default.entrypoint
def get_link_devices(namespace, **kwargs):
"""List interfaces in a namespace
:return: (list) interfaces in a namespace
"""
try:
with get_iproute(namespace) as ip:
return make_serializable(ip.get_links(**kwargs))
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
def get_device_names(namespace, **kwargs):
"""List interface names in a namespace
:return: a list of strings with the names of the interfaces in a namespace
"""
devices_attrs = [link['attrs'] for link
in get_link_devices(namespace, **kwargs)]
device_names = []
for device_attrs in devices_attrs:
for link_name in (link_attr[1] for link_attr in device_attrs
if link_attr[0] == 'IFLA_IFNAME'):
device_names.append(link_name)
return device_names
@privileged.default.entrypoint
def get_ip_addresses(namespace, **kwargs):
"""List of IP addresses in a namespace
:return: (tuple) IP addresses in a namespace
"""
try:
with get_iproute(namespace) as ip:
return make_serializable(ip.get_addr(**kwargs))
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def list_ip_rules(namespace, ip_version, match=None, **kwargs):
"""List all IP rules"""
try:
with get_iproute(namespace) as ip:
rules = make_serializable(ip.get_rules(
family=_IP_VERSION_FAMILY_MAP[ip_version],
match=match, **kwargs))
for rule in rules:
rule['attrs'] = dict(
(item[0], item[1]) for item in rule['attrs'])
return rules
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def add_ip_rule(namespace, **kwargs):
"""Add a new IP rule"""
try:
with get_iproute(namespace) as ip:
ip.rule('add', **kwargs)
except netlink_exceptions.NetlinkError as e:
if e.code == errno.EEXIST:
return
raise
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def delete_ip_rule(namespace, **kwargs):
"""Delete an IP rule"""
try:
with get_iproute(namespace) as ip:
ip.rule('del', **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
def _make_pyroute2_route_args(namespace, ip_version, cidr, device, via, table,
metric, scope, protocol):
"""Returns a dictionary of arguments to be used in pyroute route commands
:param namespace: (string) name of the namespace
:param ip_version: (int) [4, 6]
:param cidr: (string) source IP or CIDR address (IPv4, IPv6)
:param device: (string) input interface name
:param via: (string) gateway IP address
:param table: (string, int) table number or name
:param metric: (int) route metric
:param scope: (int) route scope
:param protocol: (string) protocol name (pyroute2.netlink.rtnl.rt_proto)
:return: a dictionary with the kwargs needed in pyroute rule commands
"""
args = {'family': _IP_VERSION_FAMILY_MAP[ip_version]}
if not scope:
scope = 'global' if via else 'link'
scope = _get_scope_name(scope)
if scope:
args['scope'] = scope
if cidr:
args['dst'] = cidr
if device:
args['oif'] = get_link_id(device, namespace)
if via:
args['gateway'] = via
if table:
args['table'] = int(table)
if metric:
args['priority'] = int(metric)
if protocol:
args['proto'] = protocol
return args
@privileged.default.entrypoint
def add_ip_route(namespace, cidr, ip_version, device=None, via=None,
table=None, metric=None, scope=None, proto='static',
**kwargs):
"""Add an IP route"""
kwargs.update(_make_pyroute2_route_args(
namespace, ip_version, cidr, device, via, table, metric, scope,
proto))
try:
with get_iproute(namespace) as ip:
ip.route('replace', **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def list_ip_routes(namespace, ip_version, device=None, table=None, **kwargs):
"""List IP routes"""
kwargs.update(_make_pyroute2_route_args(
namespace, ip_version, None, device, None, table, None, None, None))
try:
with get_iproute(namespace) as ip:
return make_serializable(ip.route('show', **kwargs))
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def delete_ip_route(namespace, cidr, ip_version, device=None, via=None,
table=None, scope=None, **kwargs):
"""Delete an IP route"""
kwargs.update(_make_pyroute2_route_args(
namespace, ip_version, cidr, device, via, table, None, scope, None))
try:
with get_iproute(namespace) as ip:
ip.route('del', **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def list_bridge_fdb(namespace=None, **kwargs):
"""List bridge fdb table"""
# NOTE(ralonsoh): fbd does not support ifindex filtering in pyroute2 0.5.14
try:
with get_iproute(namespace) as ip:
return make_serializable(ip.fdb('dump', **kwargs))
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
def _command_bridge_fdb(command, mac, device, dst_ip=None, namespace=None,
**kwargs):
try:
kwargs['lladdr'] = mac
kwargs['ifindex'] = get_link_id(device, namespace)
if dst_ip:
kwargs['dst'] = dst_ip
with get_iproute(namespace) as ip:
return make_serializable(ip.fdb(command, **kwargs))
except OSError as e:
if e.errno == errno.ENOENT:
raise NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def add_bridge_fdb(mac, device, dst_ip=None, namespace=None, **kwargs):
"""Add a FDB entry"""
return _command_bridge_fdb('add', mac, device, dst_ip=dst_ip,
namespace=namespace, **kwargs)
@privileged.default.entrypoint
def append_bridge_fdb(mac, device, dst_ip=None, namespace=None, **kwargs):
"""Add a FDB entry"""
return _command_bridge_fdb('append', mac, device, dst_ip=dst_ip,
namespace=namespace, **kwargs)
@privileged.default.entrypoint
def replace_bridge_fdb(mac, device, dst_ip=None, namespace=None, **kwargs):
"""Add a FDB entry"""
return _command_bridge_fdb('replace', mac, device, dst_ip=dst_ip,
namespace=namespace, **kwargs)
@privileged.default.entrypoint
def delete_bridge_fdb(mac, device, dst_ip=None, namespace=None, **kwargs):
"""Add a FDB entry"""
return _command_bridge_fdb('del', mac, device, dst_ip=dst_ip,
namespace=namespace, **kwargs)