diff --git a/.gitignore b/.gitignore index 59a7be2f..f3f3d6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ bin var sdist develop-eggs -lib lib64 # Installer logs diff --git a/README.rst b/README.rst index c6421227..f548ca8d 100644 --- a/README.rst +++ b/README.rst @@ -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 ------------- diff --git a/kuryr/lib/binding.py b/kuryr/lib/binding.py deleted file mode 100644 index 6d4ec33d..00000000 --- a/kuryr/lib/binding.py +++ /dev/null @@ -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) diff --git a/kuryr/lib/binding/__init__.py b/kuryr/lib/binding/__init__.py new file mode 100644 index 00000000..c65a6c20 --- /dev/null +++ b/kuryr/lib/binding/__init__.py @@ -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) diff --git a/kuryr/lib/binding/drivers/__init__.py b/kuryr/lib/binding/drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kuryr/lib/binding/drivers/ipvlan.py b/kuryr/lib/binding/drivers/ipvlan.py new file mode 100644 index 00000000..34456d1c --- /dev/null +++ b/kuryr/lib/binding/drivers/ipvlan.py @@ -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 diff --git a/kuryr/lib/binding/drivers/macvlan.py b/kuryr/lib/binding/drivers/macvlan.py new file mode 100644 index 00000000..742e9a44 --- /dev/null +++ b/kuryr/lib/binding/drivers/macvlan.py @@ -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 diff --git a/kuryr/lib/binding/drivers/nested.py b/kuryr/lib/binding/drivers/nested.py new file mode 100644 index 00000000..9a173d0b --- /dev/null +++ b/kuryr/lib/binding/drivers/nested.py @@ -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 diff --git a/kuryr/lib/binding/drivers/utils.py b/kuryr/lib/binding/drivers/utils.py new file mode 100644 index 00000000..7b72b774 --- /dev/null +++ b/kuryr/lib/binding/drivers/utils.py @@ -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() diff --git a/kuryr/lib/binding/drivers/veth.py b/kuryr/lib/binding/drivers/veth.py new file mode 100644 index 00000000..e74f305b --- /dev/null +++ b/kuryr/lib/binding/drivers/veth.py @@ -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 diff --git a/kuryr/lib/config.py b/kuryr/lib/config.py index 807fd727..971900aa 100644 --- a/kuryr/lib/config.py +++ b/kuryr/lib/config.py @@ -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) diff --git a/kuryr/lib/opts.py b/kuryr/lib/opts.py index 34a7acf5..27995c0a 100644 --- a/kuryr/lib/opts.py +++ b/kuryr/lib/opts.py @@ -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)]) diff --git a/kuryr/lib/utils.py b/kuryr/lib/utils.py index 76155480..508c2e71 100644 --- a/kuryr/lib/utils.py +++ b/kuryr/lib/utils.py @@ -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. diff --git a/kuryr/tests/unit/base.py b/kuryr/tests/unit/base.py index a5690860..2c87aa01 100644 --- a/kuryr/tests/unit/base.py +++ b/kuryr/tests/unit/base.py @@ -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 diff --git a/kuryr/tests/unit/binding/__init__.py b/kuryr/tests/unit/binding/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kuryr/tests/unit/binding/drivers/__init__.py b/kuryr/tests/unit/binding/drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kuryr/tests/unit/binding/drivers/test_utils.py b/kuryr/tests/unit/binding/drivers/test_utils.py new file mode 100644 index 00000000..fcb55dab --- /dev/null +++ b/kuryr/tests/unit/binding/drivers/test_utils.py @@ -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()) diff --git a/kuryr/tests/unit/test_binding.py b/kuryr/tests/unit/binding/test_package.py similarity index 85% rename from kuryr/tests/unit/test_binding.py rename to kuryr/tests/unit/binding/test_package.py index d3bcec8d..9dae256d 100644 --- a/kuryr/tests/unit/test_binding.py +++ b/kuryr/tests/unit/binding/test_package.py @@ -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() diff --git a/kuryr/tests/unit/test_config.py b/kuryr/tests/unit/test_config.py index 6ae6fcae..adf91997 100644 --- a/kuryr/tests/unit/test_config.py +++ b/kuryr/tests/unit/test_config.py @@ -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) diff --git a/kuryr/tests/unit/test_opts.py b/kuryr/tests/unit/test_opts.py index 8f44e0e0..5291622c 100644 --- a/kuryr/tests/unit/test_opts.py +++ b/kuryr/tests/unit/test_opts.py @@ -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()) diff --git a/kuryr/tests/unit/test_utils.py b/kuryr/tests/unit/test_utils.py index 0ea9b450..06186e39 100644 --- a/kuryr/tests/unit/test_utils.py +++ b/kuryr/tests/unit/test_utils.py @@ -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(