binding: Add driver based subsystem

This patch introduces a new hierarchy of drivers to perform the port
binding and unbinding in a similar fashion as how it is done with
Neutron plugins.

The initial three drivers are:
* veth: The one that we have been using up until now and that uses
  the usr/libexec/kuryr/* scripts to bind the host side
* ipvlan: L2 ipvlan motivated mostly container-in-vm use cases so that
  the instance interface will have linked devices that get addresses
  of other ports of the same subnet.
* macvlan: bridged mode ipvlan for OSes that do not support vlan.

Co-Authored-by: Louise Daly <louise.m.daly@intel.com>
Implements: blueprint driver-binding-ipvlan
Change-Id: I1d94ab324ab2a65a6d3e782e23ea6c59b110ff67
This commit is contained in:
Antoni Segura Puimedon 2016-09-24 15:27:22 +02:00
parent cae8019e44
commit 854a8028b6
No known key found for this signature in database
GPG Key ID: 2329618D2967720A
21 changed files with 690 additions and 254 deletions

1
.gitignore vendored
View File

@ -14,7 +14,6 @@ bin
var
sdist
develop-eggs
lib
lib64
# Installer logs

View File

@ -76,13 +76,27 @@ Edit keystone section in `/etc/kuryr/kuryr.conf`, replace ADMIN_PASSWORD:
admin_password = ADMIN_PASSWORD
In the same file uncomment the `bindir` parameter with the path for the Kuryr vif binding
executables:
In the same file uncomment the `bindir` parameter with the path for the Kuryr
vif binding executables:
::
bindir = /usr/local/libexec/kuryr
By default, Kuryr will use veth pairs for performing the binding. However, the
Kuryr library ships with two other drivers that you can configure in the
**binding** section::
[binding]
#driver = kuryr.lib.binding.drivers.ipvlan
#driver = kuryr.lib.binding.drivers.macvlan
Drivers may make use of other **binding** options. Both Kuryr library drivers in
the previous snippet can be further configured setting the interface that will
act as link interface for the virtual devices::
link_iface = enp4s0
Running Kuryr
-------------

View File

@ -1,204 +0,0 @@
# 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 os
import ipaddress
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_utils import excutils
import pyroute2
import six
from kuryr.lib import exceptions
from kuryr.lib import utils
BINDING_SUBCOMMAND = 'bind'
DOWN = 'DOWN'
FALLBACK_VIF_TYPE = 'unbound'
FIXED_IP_KEY = 'fixed_ips'
IFF_UP = 0x1 # The last bit represents if the interface is up
IP_ADDRESS_KEY = 'ip_address'
KIND_VETH = 'veth'
MAC_ADDRESS_KEY = 'mac_address'
SUBNET_ID_KEY = 'subnet_id'
UNBINDING_SUBCOMMAND = 'unbind'
VIF_TYPE_KEY = 'binding:vif_type'
VIF_DETAILS_KEY = 'binding:vif_details'
DEFAULT_NETWORK_MTU = 1500
_IPDB_CACHE = None
_IPROUTE_CACHE = None
def get_ipdb():
"""Returns the already cached or a newly created IPDB instance.
IPDB reads the Linux specific file when it's instantiated. This behaviour
prevents Mac OSX users from running unit tests. This function makes the
loading IPDB lazyily and therefore it can be mocked after the import of
modules that import this module.
:returns: The already cached or newly created ``pyroute2.IPDB`` instance
"""
global _IPDB_CACHE
if not _IPDB_CACHE:
_IPDB_CACHE = pyroute2.IPDB()
return _IPDB_CACHE
def get_iproute():
"""Returns the already cached or a newly created IPRoute instance.
IPRoute reads the Linux specific file when it's instantiated. This
behaviour prevents Mac OSX users from running unit tests. This function
makes the loading IPDB lazyily and therefore it can be mocked after the
import of modules that import this module.
:returns: The already cached or newly created ``pyroute2.IPRoute`` instance
"""
global _IPROUTE_CACHE
if not _IPROUTE_CACHE:
_IPROUTE_CACHE = pyroute2.IPRoute()
return _IPROUTE_CACHE
def _is_up(interface):
flags = interface['flags']
if not flags:
return False
return (flags & IFF_UP) == 1
def cleanup_veth(ifname):
"""Cleans the veth passed as an argument up.
:param ifname: the name of the veth endpoint
:returns: the index of the interface which name is the given ifname if it
exists, otherwise None
:raises: pyroute2.NetlinkError
"""
ipr = get_iproute()
veths = ipr.link_lookup(ifname=ifname)
if veths:
host_veth_index = veths[0]
ipr.link_remove(host_veth_index)
return host_veth_index
else:
return None
def port_bind(endpoint_id, neutron_port, neutron_subnets,
neutron_network=None):
"""Binds the Neutron port to the network interface on the host.
:param endpoint_id: the ID of the endpoint as string
:param neutron_port: a port dictionary returned from
python-neutronclient
:param neutron_subnets: a list of all subnets under network to which this
endpoint is trying to join
:param neutron_network: network which this endpoint is trying to join
:returns: the tuple of the names of the veth pair and the tuple of stdout
and stderr returned by processutils.execute invoked with the
executable script for binding
:raises: kuryr.common.exceptions.VethCreationFailure,
processutils.ProcessExecutionError
"""
ip = get_ipdb()
port_id = neutron_port['id']
ifname, peer_name = utils.get_veth_pair_names(port_id)
subnets_dict = {subnet['id']: subnet for subnet in neutron_subnets}
if neutron_network is None:
mtu = DEFAULT_NETWORK_MTU
else:
mtu = neutron_network.get('mtu', DEFAULT_NETWORK_MTU)
try:
with ip.create(ifname=ifname, kind=KIND_VETH,
reuse=True, peer=peer_name) as host_veth:
if not _is_up(host_veth):
host_veth.up()
with ip.interfaces[peer_name] as peer_veth:
fixed_ips = neutron_port.get(FIXED_IP_KEY, [])
if not fixed_ips and (IP_ADDRESS_KEY in neutron_port):
peer_veth.add_ip(neutron_port[IP_ADDRESS_KEY])
for fixed_ip in fixed_ips:
if IP_ADDRESS_KEY in fixed_ip and (SUBNET_ID_KEY in fixed_ip):
subnet_id = fixed_ip[SUBNET_ID_KEY]
subnet = subnets_dict[subnet_id]
cidr = ipaddress.ip_network(six.text_type(subnet['cidr']))
peer_veth.add_ip(fixed_ip[IP_ADDRESS_KEY], cidr.prefixlen)
peer_veth.set_mtu(mtu)
peer_veth.address = neutron_port[MAC_ADDRESS_KEY].lower()
if not _is_up(peer_veth):
peer_veth.up()
except pyroute2.CreateException:
raise exceptions.VethCreationFailure(
'Creating the veth pair was failed.')
except pyroute2.CommitException:
raise exceptions.VethCreationFailure(
'Could not configure the veth endpoint for the container.')
vif_type = neutron_port.get(VIF_TYPE_KEY, FALLBACK_VIF_TYPE)
vif_details = utils.string_mappings(neutron_port.get(VIF_DETAILS_KEY))
binding_exec_path = os.path.join(cfg.CONF.bindir, vif_type)
if not os.path.exists(binding_exec_path):
cleanup_veth(ifname)
raise exceptions.BindingNotSupportedFailure(
"vif_type({0}) is not supported. A binding script for "
"this type can't be found.".format(vif_type))
port_id = neutron_port['id']
network_id = neutron_port['network_id']
tenant_id = neutron_port['tenant_id']
mac_address = neutron_port['mac_address']
try:
stdout, stderr = processutils.execute(
binding_exec_path, BINDING_SUBCOMMAND, port_id, ifname,
endpoint_id, mac_address, network_id, tenant_id, vif_details,
run_as_root=True)
except processutils.ProcessExecutionError:
with excutils.save_and_reraise_exception():
cleanup_veth(ifname)
return (ifname, peer_name, (stdout, stderr))
def port_unbind(endpoint_id, neutron_port):
"""Unbinds the Neutron port from the network interface on the host.
:param endpoint_id: the ID of the Docker container as string
:param neutron_port: a port dictionary returned from python-neutronclient
:returns: the tuple of stdout and stderr returned by processutils.execute
invoked with the executable script for unbinding
:raises: processutils.ProcessExecutionError, pyroute2.NetlinkError
"""
vif_type = neutron_port.get(VIF_TYPE_KEY, FALLBACK_VIF_TYPE)
vif_details = utils.string_mappings(neutron_port.get(VIF_DETAILS_KEY))
unbinding_exec_path = os.path.join(cfg.CONF.bindir, vif_type)
port_id = neutron_port['id']
ifname, _ = utils.get_veth_pair_names(port_id)
mac_address = neutron_port['mac_address']
stdout, stderr = processutils.execute(
unbinding_exec_path, UNBINDING_SUBCOMMAND, port_id, ifname,
endpoint_id, mac_address, vif_details, run_as_root=True)
try:
cleanup_veth(ifname)
except pyroute2.NetlinkError:
raise exceptions.VethDeleteionFailure(
'Deleting the veth pair failed.')
return (stdout, stderr)

View File

@ -0,0 +1,52 @@
# 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.
from oslo_config import cfg
from oslo_utils import importutils
def port_bind(endpoint_id, port, subnets, network=None, nested_port=None):
"""Binds the Neutron port to the network interface on the host.
:param endpoint_id: the ID of the endpoint as string
:param port: the instance Neutron port dictionary as returned by
python-neutronclient
:param subnets: an iterable of all the Neutron subnets which the
endpoint is trying to join
:param network: the Neutron network which the endpoint is trying to
join
:param nested_port: the dictionary, as returned by python-neutronclient,
of the port that that is used when running inside
another instance (either ipvlan/macvlan or a subport)
:returns: the tuple of the names of the veth pair and the tuple of stdout
and stderr returned by processutils.execute invoked with the
executable script for binding
:raises: kuryr.common.exceptions.VethCreationFailure,
processutils.ProcessExecutionError
"""
driver = importutils.import_module(cfg.CONF.binding.driver)
return driver.port_bind(endpoint_id, port, subnets, network=network,
nested_port=nested_port)
def port_unbind(endpoint_id, neutron_port):
"""Unbinds the Neutron port from the network interface on the host.
:param endpoint_id: the ID of the Docker container as string
:param neutron_port: a port dictionary returned from python-neutronclient
:returns: the tuple of stdout and stderr returned by processutils.execute
invoked with the executable script for unbinding
:raises: processutils.ProcessExecutionError, pyroute2.NetlinkError
"""
driver = importutils.import_module(cfg.CONF.binding.driver)
return driver.port_unbind(endpoint_id, neutron_port)

View File

View File

@ -0,0 +1,57 @@
# 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.
"""For now it only supports container-in-vm deployments"""
from pyroute2.netlink.rtnl.ifinfmsg import ifinfmsg
from kuryr.lib.binding.drivers import nested
from kuryr.lib.binding.drivers import utils
KIND = 'ipvlan'
# We use L2 to allow broadcast frames
IPVLAN_MODE_L2 = ifinfmsg.ifinfo.ipvlan_data.modes['IPVLAN_MODE_L2']
def port_bind(endpoint_id, port, subnets, network=None, nested_port=None):
"""Binds the Neutron port to the network interface on the host.
:param endpoint_id: the ID of the endpoint as string
:param port: the instance Neutron port dictionary as returned by
python-neutronclient
:param subnets: an iterable of all the Neutron subnets which the
endpoint is trying to join
:param network: the Neutron network which the endpoint is trying to
join
:param nested_port: the dictionary, as returned by python-neutronclient,
of the port that that is used when running inside
another instance (either ipvlan/macvlan or a subport)
:returns: the tuple of the names of the veth pair and the tuple of stdout
and stderr returned by processutils.execute invoked with the
executable script for binding
:raises: kuryr.common.exceptions.VethCreationFailure,
processutils.ProcessExecutionError
"""
ip = utils.get_ipdb()
port_id = port['id']
_, devname = utils.get_veth_pair_names(port_id)
link_iface = nested.get_link_iface(port)
with ip.create(ifname=devname, kind=KIND,
link=ip.interfaces[link_iface],
mode=IPVLAN_MODE_L2) as container_iface:
utils._configure_container_iface(
container_iface, subnets,
fixed_ips=nested_port.get(utils.FIXED_IP_KEY))
return None, devname, ('', None)
port_unbind = nested.port_unbind

View File

@ -0,0 +1,56 @@
# 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.
"""For now it only supports container-in-vm deployments"""
from kuryr.lib.binding.drivers import nested
from kuryr.lib.binding.drivers import utils
KIND = 'macvlan'
# We use the bridge mode for simplicity and proximity to the usual
# container bridged networking
MACVLAN_MODE_BRIDGE = 'bridge'
def port_bind(endpoint_id, port, subnets, network=None, nested_port=None):
"""Binds the Neutron port to the network interface on the host.
:param endpoint_id: the ID of the endpoint as string
:param port: the instance Neutron port dictionary as returned by
python-neutronclient
:param subnets: an iterable of all the Neutron subnets which the
endpoint is trying to join
:param network: the Neutron network which the endpoint is trying to
join
:param nested_port: the dictionary, as returned by python-neutronclient,
of the port that that is used when running inside
another instance
:returns: the tuple of the names of the veth pair and the tuple of stdout
and stderr returned by processutils.execute invoked with the
executable script for binding
:raises: kuryr.common.exceptions.VethCreationFailure,
processutils.ProcessExecutionError
"""
ip = utils.get_ipdb()
port_id = port['id']
_, devname = utils.get_veth_pair_names(port_id)
link_iface = nested.get_link_iface(port)
with ip.create(ifname=devname, kind=KIND,
link=ip.interfaces[link_iface],
macvlan_mode=MACVLAN_MODE_BRIDGE) as container_iface:
utils._configure_container_iface(
container_iface, subnets,
fixed_ips=nested_port.get(utils.FIXED_IP_KEY))
return None, devname, ('', None)
port_unbind = nested.port_unbind

View File

@ -0,0 +1,52 @@
# 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.
"""Helper module for bindings that usually happen inside OSt instances"""
import pyroute2
from oslo_config import cfg
from kuryr.lib.binding.drivers import utils
from kuryr.lib import exceptions
def get_link_iface(port):
"""Gets the name of the interface to link the container virtual devices"""
link = cfg.CONF.binding.link_iface
if not link:
# Guess the name from the port hwaddr
ip = utils.get_ipdb()
for name, data in ip.interfaces.items():
if data['address'] == port[utils.MAC_ADDRESS_KEY]:
link = data['ifname']
break
return link
def port_unbind(endpoint_id, neutron_port):
"""Unbinds the Neutron port from the network interface on the host.
:param endpoint_id: the ID of the Docker container as string
:param neutron_port: a port dictionary returned from python-neutronclient
:returns: the tuple of stdout and stderr returned by processutils.execute
invoked with the executable script for unbinding
:raises: processutils.ProcessExecutionError, pyroute2.NetlinkError
"""
port_id = neutron_port['id']
_, devname = utils.get_veth_pair_names(port_id)
try:
utils.remove_device(devname)
except pyroute2.NetlinkError:
raise exceptions.VethDeleteionFailure(
'Failed to delete the container device.')
return '', None

View File

@ -0,0 +1,121 @@
# 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 ipaddress
import pyroute2
from pyroute2.netlink.rtnl import ifinfmsg
import six
from kuryr.lib import constants
_IPDB_CACHE = None
_IPROUTE_CACHE = None
FIXED_IP_KEY = 'fixed_ips'
IP_ADDRESS_KEY = 'ip_address'
MAC_ADDRESS_KEY = 'mac_address'
SUBNET_ID_KEY = 'subnet_id'
def get_veth_pair_names(port_id):
ifname = constants.VETH_PREFIX + port_id
ifname = ifname[:constants.NIC_NAME_LEN]
peer_name = constants.CONTAINER_VETH_PREFIX + port_id
peer_name = peer_name[:constants.NIC_NAME_LEN]
return ifname, peer_name
def get_ipdb():
"""Returns the already cached or a newly created IPDB instance.
IPDB reads the Linux specific file when it's instantiated. This behaviour
prevents Mac OSX users from running unit tests. This function makes the
loading IPDB lazyily and therefore it can be mocked after the import of
modules that import this module.
:returns: The already cached or newly created ``pyroute2.IPDB`` instance
"""
global _IPDB_CACHE
if not _IPDB_CACHE:
_IPDB_CACHE = pyroute2.IPDB()
return _IPDB_CACHE
def get_iproute():
"""Returns the already cached or a newly created IPRoute instance.
IPRoute reads the Linux specific file when it's instantiated. This
behaviour prevents Mac OSX users from running unit tests. This function
makes the loading IPDB lazyily and therefore it can be mocked after the
import of modules that import this module.
:returns: The already cached or newly created ``pyroute2.IPRoute`` instance
"""
global _IPROUTE_CACHE
if not _IPROUTE_CACHE:
_IPROUTE_CACHE = pyroute2.IPRoute()
return _IPROUTE_CACHE
def remove_device(ifname):
"""Removes the device with name ifname.
:param ifname: the name of the device to remove
:returns: the index the device identified by ifname had if it
exists, otherwise None
:raises: pyroute2.NetlinkError
"""
ipr = get_iproute()
devices = ipr.link_lookup(ifname=ifname)
if devices:
dev_index = devices[0]
ipr.link_remove(dev_index)
return dev_index
else:
return None
def is_up(interface):
flags = interface['flags']
if not flags:
return False
return (flags & ifinfmsg.IFF_UP) == 1
def _configure_container_iface(iface, subnets, fixed_ips, mtu=None,
hwaddr=None):
"""Configures the interface that is placed in the container net ns
:param iface: the pyroute IPDB interface object to configure
:param subnets: an iterable of all the Neutron subnets which the
endpoint is trying to join
:param fixed_ips: an iterable of fixed IPs to be set for the iface
:param mtu: Maximum Transfer Unit to set for the iface
:param hwaddr: Hardware address to set for the iface
"""
subnets_dict = {subnet['id']: subnet for subnet in subnets}
# We assume containers always work with fixed ips, dhcp does not really
# make a lot of sense
for fixed_ip in fixed_ips:
if IP_ADDRESS_KEY in fixed_ip and (SUBNET_ID_KEY in fixed_ip):
subnet_id = fixed_ip[SUBNET_ID_KEY]
subnet = subnets_dict[subnet_id]
cidr = ipaddress.ip_network(six.text_type(subnet['cidr']))
iface.add_ip(fixed_ip[IP_ADDRESS_KEY], cidr.prefixlen)
if mtu is not None:
iface.set_mtu(mtu)
if hwaddr is not None:
iface.set_address(hwaddr)
if not is_up(iface):
iface.up()

View File

@ -0,0 +1,148 @@
# 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 os
import pyroute2
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_utils import excutils
from kuryr.lib.binding.drivers import utils
from kuryr.lib import exceptions
from kuryr.lib import utils as lib_utils
KIND = 'veth'
BINDING_SUBCOMMAND = 'bind'
DEFAULT_NETWORK_MTU = 1500
FALLBACK_VIF_TYPE = 'unbound'
UNBINDING_SUBCOMMAND = 'unbind'
VIF_DETAILS_KEY = 'binding:vif_details'
VIF_TYPE_KEY = 'binding:vif_type'
def port_bind(endpoint_id, port, subnets, network=None, nested_port=None):
"""Binds the Neutron port to the network interface on the host.
:param endpoint_id: the ID of the endpoint as string
:param port: the instance Neutron port dictionary as returned by
python-neutronclient
:param subnets: an iterable of all the Neutron subnets which the
endpoint is trying to join
:param network: the Neutron network which the endpoint is trying to
join
:param nested_port: the dictionary, as returned by python-neutronclient,
of the port that that is used when running inside
another instance (either ipvlan/macvlan or a subport)
:returns: the tuple of the names of the veth pair and the tuple of stdout
and stderr returned by processutils.execute invoked with the
executable script for binding
:raises: kuryr.common.exceptions.VethCreationFailure,
processutils.ProcessExecutionError
"""
ip = utils.get_ipdb()
port_id = port['id']
host_ifname, container_ifname = utils.get_veth_pair_names(port_id)
if network is None:
mtu = DEFAULT_NETWORK_MTU
else:
mtu = network.get('mtu', DEFAULT_NETWORK_MTU)
try:
with ip.create(ifname=host_ifname, kind=KIND,
reuse=True, peer=container_ifname) as host_veth:
if not utils.is_up(host_veth):
host_veth.up()
with ip.interfaces[container_ifname] as container_veth:
utils._configure_container_iface(
container_veth, subnets,
fixed_ips=port.get(utils.FIXED_IP_KEY),
mtu=mtu, hwaddr=port[utils.MAC_ADDRESS_KEY].lower())
except pyroute2.CreateException:
raise exceptions.VethCreationFailure(
'Virtual device creation failed.')
except pyroute2.CommitException:
raise exceptions.VethCreationFailure(
'Could not configure the container virtual device networking.')
try:
stdout, stderr = _configure_host_iface(
host_ifname, endpoint_id, port_id,
port['network_id'], port['tenant_id'],
port[utils.MAC_ADDRESS_KEY],
kind=port.get(VIF_TYPE_KEY),
details=port.get(VIF_DETAILS_KEY))
except Exception:
with excutils.save_and_reraise_exception():
utils.remove_device(host_ifname)
return host_ifname, container_ifname, (stdout, stderr)
def port_unbind(endpoint_id, neutron_port):
"""Unbinds the Neutron port from the network interface on the host.
:param endpoint_id: the ID of the Docker container as string
:param neutron_port: a port dictionary returned from python-neutronclient
:returns: the tuple of stdout and stderr returned by processutils.execute
invoked with the executable script for unbinding
:raises: processutils.ProcessExecutionError, pyroute2.NetlinkError
"""
vif_type = neutron_port.get(VIF_TYPE_KEY, FALLBACK_VIF_TYPE)
vif_details = lib_utils.string_mappings(neutron_port.get(VIF_DETAILS_KEY))
unbinding_exec_path = os.path.join(cfg.CONF.bindir, vif_type)
port_id = neutron_port['id']
ifname, _ = utils.get_veth_pair_names(port_id)
mac_address = neutron_port['mac_address']
stdout, stderr = processutils.execute(
unbinding_exec_path, UNBINDING_SUBCOMMAND, port_id, ifname,
endpoint_id, mac_address, vif_details, run_as_root=True)
try:
utils.remove_device(ifname)
except pyroute2.NetlinkError:
raise exceptions.VethDeleteionFailure(
'Deleting the veth pair failed.')
return (stdout, stderr)
def _configure_host_iface(ifname, endpoint_id, port_id, net_id, project_id,
hwaddr, kind=None, details=None):
"""Configures the interface that is placed on the default net ns
:param ifname: the name of the interface to configure
:param endpoint_id: the identifier of the endpoint
:param port_id: the Neutron uuid of the port to which this interface
is to be bound
:param net_id: the Neutron uuid of the network the port is part of
:param project_id: the Keystone project the binding is made for
:param hwaddr: the interface hardware address
:param kind: the Neutorn port vif_type
:param details: Neutron vif details
"""
if kind is None:
kind = FALLBACK_VIF_TYPE
binding_exec_path = os.path.join(cfg.CONF.bindir, kind)
if not os.path.exists(binding_exec_path):
raise exceptions.BindingNotSupportedFailure(
"vif_type({0}) is not supported. A binding script for this type "
"can't be found".format(kind))
stdout, stderr = processutils.execute(
binding_exec_path, BINDING_SUBCOMMAND, port_id, ifname,
endpoint_id, hwaddr, net_id, project_id,
lib_utils.string_mappings(details),
run_as_root=True)
return stdout, stderr

View File

@ -67,9 +67,22 @@ binding_opts = [
cfg.StrOpt('veth_dst_prefix',
default='eth',
help=('The name prefix of the veth endpoint put inside the '
'container.'))
'container.')),
cfg.StrOpt('driver',
default='kuryr.lib.binding.drivers.veth',
help=_('Driver to use for binding and unbinding ports.')),
cfg.StrOpt('link_iface',
default='',
help=_('Specifies the name of the Nova instance interface to '
'link the virtual devices to (only applicable to some '
'binding drivers.')),
]
binding_group = cfg.OptGroup(
'binding',
title='binding options',
help=_('Configuration options for container interface binding.'))
def register_neutron_opts(conf):
conf.register_group(neutron_group)

View File

@ -34,7 +34,6 @@ _core_opts_with_logging += _options.generic_log_opts
_kuryr_opts = [
(None, list(itertools.chain(_core_opts_with_logging))),
('binding', config.binding_opts),
]
@ -70,4 +69,5 @@ def list_kuryr_opts():
"""
return ([(k, copy.deepcopy(o)) for k, o in _kuryr_opts] +
list_neutron_opts())
list_neutron_opts() +
[(config.binding_group, config.binding_opts)])

View File

@ -19,7 +19,6 @@ from neutronclient.v2_0 import client
from oslo_config import cfg
from kuryr.lib import config as kuryr_config
from kuryr.lib import constants as const
DOCKER_NETNS_BASE = '/var/run/docker/netns'
PORT_POSTFIX = 'port'
@ -41,14 +40,6 @@ def get_hostname():
return socket.gethostname()
def get_veth_pair_names(port_id):
ifname = const.VETH_PREFIX + port_id
ifname = ifname[:const.NIC_NAME_LEN]
peer_name = const.CONTAINER_VETH_PREFIX + port_id
peer_name = peer_name[:const.NIC_NAME_LEN]
return ifname, peer_name
def get_neutron_subnetpool_name(subnet_cidr):
"""Returns a Neutron subnetpool name.

View File

@ -24,7 +24,7 @@ class TestCase(base.BaseTestCase):
super(TestCase, self).setUp()
CONF = cfg.CONF
CONF.register_opts(config.core_opts)
CONF.register_opts(config.binding_opts, 'binding')
CONF.register_opts(config.binding_opts, group=config.binding_group)
config.register_neutron_opts(CONF)
@staticmethod

View File

View File

@ -0,0 +1,155 @@
# 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 ddt
import mock
import uuid
import pyroute2.ipdb.interface
from pyroute2.netlink.rtnl import ifinfmsg
from kuryr.lib.binding.drivers import utils
from kuryr.lib import constants
from kuryr.tests.unit import base
@ddt.ddt
class BindingDriversUtilsTest(base.TestCase):
"""Unit tests for binding drivers utils"""
def test_get_veth_pair_names(self):
fake_neutron_port_id = str(uuid.uuid4())
generated_ifname, generated_peer = utils.get_veth_pair_names(
fake_neutron_port_id)
namelen = constants.NIC_NAME_LEN
ifname_postlen = namelen - len(constants.VETH_PREFIX)
peer_postlen = namelen - len(constants.CONTAINER_VETH_PREFIX)
self.assertEqual(namelen, len(generated_ifname))
self.assertEqual(namelen, len(generated_peer))
self.assertIn(constants.VETH_PREFIX, generated_ifname)
self.assertIn(constants.CONTAINER_VETH_PREFIX, generated_peer)
self.assertIn(fake_neutron_port_id[:ifname_postlen], generated_ifname)
self.assertIn(fake_neutron_port_id[:peer_postlen], generated_peer)
@ddt.data((False), (True))
def test_is_up(self, interface_flag):
fake_interface = {'flags': 0x0}
if interface_flag:
fake_interface['flags'] = ifinfmsg.IFF_UP
self.assertEqual(True, utils.is_up(fake_interface))
else:
self.assertEqual(False, utils.is_up(fake_interface))
@ddt.data(
(['10.10.10.11'], ['384ac9fc-eefa-4399-8d88-1181433e33b1'], False,
None, None),
(['10.10.10.11'], ['384ac9fc-eefa-4399-8d88-1181433e33b1'],
True, None, None),
(['10.10.10.11', '10.11.0.10'],
['384ac9fc-eefa-4399-8d88-1181433e33b1',
'0a6eab28-9dc1-46c0-997c-cb9f66f6081f'],
False, 1500, 'fa:16:3e:22:a3:3d'))
@ddt.unpack
@mock.patch.object(utils, 'is_up')
def test__configure_container_iface(
self, addrs, subnet_ids, already_up, mtu, mac, mock_is_up):
subnets = [{
'allocation_pools': [{'end': '10.11.0.254', 'start': '10.11.0.2'}],
'cidr': '10.11.0.0/26',
'created_at': '2016-09-27T07:55:12',
'description': '',
'dns_nameservers': [],
'enable_dhcp': True,
'gateway_ip': '10.11.0.1',
'host_routes': [],
'id': '0a6eab28-9dc1-46c0-997c-cb9f66f6081f',
'ip_version': 4,
'ipv6_address_mode': None,
'ipv6_ra_mode': None,
'name': 'subtest',
'network_id': '90146ed2-c3ce-4001-866e-e97e513530a3',
'revision': 2,
'service_types': [],
'subnetpool_id': None,
'tenant_id': '0c0d1f46fa8d485d9534ea0e35f37bd3',
'updated_at': '2016-09-27T07:55:12'
}, {
'allocation_pools': [{'end': '10.10.0.254', 'start': '10.10.0.2'}],
'cidr': '10.10.0.0/24',
'created_at': '2016-09-27T08:57:13',
'description': '',
'dns_nameservers': [],
'enable_dhcp': True,
'gateway_ip': '10.10.0.1',
'host_routes': [],
'id': '384ac9fc-eefa-4399-8d88-1181433e33b1',
'ip_version': 4,
'ipv6_address_mode': None,
'ipv6_ra_mode': None,
'name': '10.10.0.0/24',
'network_id': 'bfb2f525-bedf-48ed-b125-102ee7920253',
'revision': 2,
'service_types': [],
'subnetpool_id': None,
'tenant_id': '51b66b97a12f42a990452967d2c555ac',
'updated_at': '2016-09-27T08:57:13'}]
fake_iface = mock.Mock(spec=pyroute2.ipdb.interface.Interface)
_set_mtu = mock.Mock()
_set_address = mock.Mock()
fake_iface.attach_mock(_set_mtu, 'set_mtu')
fake_iface.attach_mock(_set_address, 'set_address')
mock_is_up.return_value = already_up
fixed_ips = []
for ip, subnet_id in zip(addrs, subnet_ids):
fixed_ips.append({
utils.IP_ADDRESS_KEY: ip,
utils.SUBNET_ID_KEY: subnet_id})
utils._configure_container_iface(
fake_iface,
subnets,
fixed_ips,
mtu=mtu,
hwaddr=mac)
subnets_prefix_by_id = dict(
(subnet['id'], int(subnet['cidr'].split('/')[1])) for
subnet in subnets)
for ip, subnet_id in zip(addrs, subnet_ids):
fake_iface.add_ip.assert_any_call(
ip, subnets_prefix_by_id[subnet_id])
if already_up:
fake_iface.up.assert_not_called()
else:
fake_iface.up.assert_called_once()
if mtu is None:
fake_iface.set_mtu.assert_not_called()
else:
fake_iface.set_mtu.assert_called_with(mtu)
if mac is None:
fake_iface.set_address.assert_not_called()
else:
fake_iface.set_address.assert_called_with(mac)
def test_get_ipdb(self):
ip = utils.get_ipdb()
self.assertEqual(ip, utils.get_ipdb())
def test_get_iproute(self):
ipr = utils.get_iproute()
self.assertEqual(ipr, utils.get_iproute())

View File

@ -9,7 +9,6 @@
# 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 ddt
import mock
import uuid
@ -28,15 +27,6 @@ mock_interface = mock.MagicMock()
class BindingTest(base.TestCase):
"""Unit tests for binding."""
@ddt.data((False), (True))
def test_is_up(self, interface_flag):
fake_interface = {'flags': 0x0}
if interface_flag:
fake_interface['flags'] = binding.IFF_UP
self.assertEqual(True, binding._is_up(fake_interface))
else:
self.assertEqual(False, binding._is_up(fake_interface))
@mock.patch('os.path.exists', return_value=True)
@mock.patch('oslo_concurrency.processutils.execute',
return_value=('fake_stdout', 'fake_stderr'))
@ -63,7 +53,8 @@ class BindingTest(base.TestCase):
fake_network['networks'][0]['mtu'] = fake_mtu
binding.port_bind(fake_docker_endpoint_id, fake_port['port'],
fake_subnets['subnets'], fake_network['networks'][0])
fake_subnets['subnets'],
fake_network['networks'][0])
expect_calls = [call.__enter__().set_mtu(fake_mtu),
call.__enter__().up()]
@ -71,10 +62,10 @@ class BindingTest(base.TestCase):
mock_path_exists.assert_called_once()
mock_execute.assert_called_once()
@mock.patch('kuryr.lib.binding.cleanup_veth')
@mock.patch('kuryr.lib.binding.drivers.utils.remove_device')
@mock.patch('oslo_concurrency.processutils.execute',
return_value=('fake_stdout', 'fake_stderr'))
def test_port_unbind(self, mock_execute, mock_cleanup_veth):
def test_port_unbind(self, mock_execute, mock_remove_device):
fake_docker_network_id = utils.get_hash()
fake_docker_endpoint_id = utils.get_hash()
fake_port_id = str(uuid.uuid4())
@ -86,4 +77,4 @@ class BindingTest(base.TestCase):
fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id)
binding.port_unbind(fake_docker_endpoint_id, fake_port['port'])
mock_execute.assert_called_once()
mock_cleanup_veth.assert_called_once()
mock_remove_device.assert_called_once()

View File

@ -27,3 +27,5 @@ class ConfigurationTest(base.TestCase):
cfg.CONF.neutron.endpoint_type)
self.assertEqual('baremetal',
cfg.CONF.deployment_type)
self.assertEqual('kuryr.lib.binding.drivers.veth',
cfg.CONF.binding.driver)

View File

@ -20,11 +20,18 @@ class OptsTest(base.TestCase):
_fake_kuryr_opts = [(None, 'fakevalue1'), ('Key1', 'fakevalue2')]
_fake_neutron_opts = [('poolv4', 'swimming4'), ('poolv6', 'swimming6')]
_fake_binding_group = 'binding_group'
_fake_binding_opts = [('driver', 'my.ipvlan')]
@mock.patch.multiple(kuryr_opts, _kuryr_opts=_fake_kuryr_opts,
@mock.patch.multiple(kuryr_opts.config,
binding_group=_fake_binding_group,
binding_opts=_fake_binding_opts)
@mock.patch.multiple(kuryr_opts,
_kuryr_opts=_fake_kuryr_opts,
list_neutron_opts=mock.DEFAULT)
def test_list_kuryr_opts(self, list_neutron_opts):
list_neutron_opts.return_value = self._fake_neutron_opts
self.assertEqual(self._fake_kuryr_opts + self._fake_neutron_opts,
self.assertEqual(self._fake_kuryr_opts + self._fake_neutron_opts +
[(self._fake_binding_group, self._fake_binding_opts)],
kuryr_opts.list_kuryr_opts())

View File

@ -13,11 +13,9 @@
import ddt
import mock
import socket
import uuid
from oslo_config import cfg
from kuryr.lib import constants as const
from kuryr.lib import utils
from kuryr.tests.unit import base
@ -30,22 +28,6 @@ class TestKuryrUtils(base.TestCase):
self.fake_url = 'http://127.0.0.1:9696'
self.fake_auth_url = 'http://127.0.0.1:35357/v2.0'
def test_get_veth_pair_names(self):
fake_neutron_port_id = str(uuid.uuid4())
generated_ifname, generated_peer = utils.get_veth_pair_names(
fake_neutron_port_id)
namelen = const.NIC_NAME_LEN
ifname_postlen = namelen - len(const.VETH_PREFIX)
peer_postlen = namelen - len(const.CONTAINER_VETH_PREFIX)
self.assertEqual(namelen, len(generated_ifname))
self.assertEqual(namelen, len(generated_peer))
self.assertIn(const.VETH_PREFIX, generated_ifname)
self.assertIn(const.CONTAINER_VETH_PREFIX, generated_peer)
self.assertIn(fake_neutron_port_id[:ifname_postlen], generated_ifname)
self.assertIn(fake_neutron_port_id[:peer_postlen], generated_peer)
def test_get_subnetpool_name(self):
fake_subnet_cidr = "10.0.0.0/16"
generated_neutron_subnetpool_name = utils.get_neutron_subnetpool_name(