Port-to-VIF os-vif translator for hybrid OVS case

This patch introduces Port-to-VIF translation to 'os_vif_util' and
implements a translator that supports hybrid OpenVSwitch plugging
case.

Change-Id: I9f5c36fa32b51da8cccf377455b096270f23a782
Partially-Implements: blueprint kuryr-k8s-integration
This commit is contained in:
Ilya Chukhnakov 2016-11-20 15:45:45 +03:00
parent d20a512600
commit 634290839a
5 changed files with 457 additions and 0 deletions

View File

@ -51,6 +51,9 @@ neutron_defaults = [
help=_("Default Neutron subnet ID for Kubernetes pods")),
cfg.ListOpt('pod_security_groups',
help=_("Default Neutron security groups' IDs for Kubernetes pods")),
cfg.StrOpt('ovs_bridge',
help=_("Default OpenVSwitch integration bridge"),
sample_default="br-int")
]
CONF = cfg.CONF

View File

@ -18,5 +18,9 @@ class K8sClientException(Exception):
pass
class IntegrityError(RuntimeError):
pass
def format_msg(exception):
return "%s: %s" % (exception.__class__.__name__, exception)

View File

@ -13,15 +13,38 @@
# License for the specific language governing permissions and limitations
# under the License.
from kuryr.lib._i18n import _LE
from kuryr.lib.binding.drivers import utils as kl_utils
from kuryr.lib import constants as kl_const
from os_vif.objects import fixed_ip as osv_fixed_ip
from os_vif.objects import network as osv_network
from os_vif.objects import route as osv_route
from os_vif.objects import subnet as osv_subnet
from os_vif.objects import vif as osv_vif
from oslo_config import cfg as oslo_cfg
from oslo_log import log as logging
from stevedore import driver as stv_driver
from kuryr_kubernetes import config
from kuryr_kubernetes import exceptions as k_exc
LOG = logging.getLogger(__name__)
# REVISIT(ivc): consider making this module part of kuryr-lib
_VIF_TRANSLATOR_NAMESPACE = "kuryr_kubernetes.vif_translators"
_VIF_MANAGERS = {}
def neutron_to_osvif_network(neutron_network):
"""Converts Neutron network to os-vif Subnet.
:param neutron_network: dict containing network information as returned by
neutron client's 'show_network'
:return: an os-vif Network object
"""
obj = osv_network.Network(id=neutron_network['id'])
if neutron_network.get('name') is not None:
@ -34,6 +57,13 @@ def neutron_to_osvif_network(neutron_network):
def neutron_to_osvif_subnet(neutron_subnet):
"""Converts Neutron subnet to os-vif Subnet.
:param neutron_subnet: dict containing subnet information as returned by
neutron client's 'show_subnet'
:return: an os-vif Subnet object
"""
obj = osv_subnet.Subnet(
cidr=neutron_subnet['cidr'],
dns=neutron_subnet['dns_nameservers'],
@ -46,8 +76,188 @@ def neutron_to_osvif_subnet(neutron_subnet):
def _neutron_to_osvif_routes(neutron_routes):
"""Converts Neutron host_routes to os-vif RouteList.
:param neutron_routes: list of routes as returned by neutron client's
'show_subnet' in 'host_routes' attribute
:return: an os-vif RouteList object
"""
obj_list = [osv_route.Route(cidr=route['destination'],
gateway=route['nexthop'])
for route in neutron_routes]
return osv_route.RouteList(objects=obj_list)
def _make_vif_subnet(subnets, subnet_id):
"""Makes a copy of an os-vif Subnet from subnets mapping.
:param subnets: subnet mapping as returned by PodSubnetsDriver.get_subnets
:param subnet_id: ID of the subnet to extract from 'subnets' mapping
:return: a copy of an os-vif Subnet object matching 'subnet_id'
"""
network = subnets[subnet_id]
if len(network.subnets.objects) != 1:
raise k_exc.IntegrityError(_LE(
"Network object for subnet %(subnet_id)s is invalid, "
"must contain a single subnet, but %(num_subnets)s found") % {
'subnet_id': subnet_id,
'num_subnets': len(network.subnets.objects)})
subnet = network.subnets.objects[0].obj_clone()
subnet.ips = osv_fixed_ip.FixedIPList(objects=[])
return subnet
def _make_vif_subnets(neutron_port, subnets):
"""Gets a list of os-vif Subnet objects for port.
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
:param subnets: subnet mapping as returned by PodSubnetsDriver.get_subnets
:return: list of os-vif Subnet object
"""
vif_subnets = {}
for neutron_fixed_ip in neutron_port.get('fixed_ips', []):
subnet_id = neutron_fixed_ip['subnet_id']
ip_address = neutron_fixed_ip['ip_address']
if subnet_id not in subnets:
continue
try:
subnet = vif_subnets[subnet_id]
except KeyError:
subnet = _make_vif_subnet(subnets, subnet_id)
vif_subnets[subnet_id] = subnet
subnet.ips.objects.append(osv_fixed_ip.FixedIP(address=ip_address))
if not vif_subnets:
raise k_exc.IntegrityError(_LE(
"No valid subnets found for port %(port_id)s") % {
'port_id': neutron_port.get('id')})
return list(vif_subnets.values())
def _make_vif_network(neutron_port, subnets):
"""Gets a os-vif Network object for port.
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
:param subnets: subnet mapping as returned by PodSubnetsDriver.get_subnets
:return: os-vif Network object
"""
try:
network = next(net.obj_clone() for net in subnets.values()
if net.id == neutron_port.get('network_id'))
except StopIteration:
raise k_exc.IntegrityError(_LE(
"Port %(port_id)s belongs to network %(network_id)s, "
"but requested networks are: %(requested_networks)s") % {
'port_id': neutron_port.get('id'),
'network_id': neutron_port.get('network_id'),
'requested_networks': [net.id for net in subnets.values()]})
network.subnets = osv_subnet.SubnetList(
objects=_make_vif_subnets(neutron_port, subnets))
return network
def _get_vif_name(neutron_port):
"""Gets a VIF device name for port.
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
"""
vif_name, _ = kl_utils.get_veth_pair_names(neutron_port['id'])
return vif_name
def _get_ovs_hybrid_bridge_name(neutron_port):
"""Gets a name of the Linux bridge name for hybrid OpenVSwitch port.
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
"""
return ('qbr' + neutron_port['id'])[:kl_const.NIC_NAME_LEN]
def _is_port_active(neutron_port):
"""Checks if port is active.
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
"""
return (neutron_port['status'] == kl_const.PORT_STATUS_ACTIVE)
def neutron_to_osvif_vif_ovs(vif_plugin, neutron_port, subnets):
"""Converts Neutron port to VIF object for os-vif 'ovs' plugin.
:param vif_plugin: name of the os-vif plugin to use (i.e. 'ovs')
:param neutron_port: dict containing port information as returned by
neutron client's 'show_port'
:param subnets: subnet mapping as returned by PodSubnetsDriver.get_subnets
:return: os-vif VIF object
"""
profile = osv_vif.VIFPortProfileOpenVSwitch(
interface_id=neutron_port['id'])
details = neutron_port.get('binding:vif_details', {})
ovs_bridge = details.get('bridge_name',
config.CONF.neutron_defaults.ovs_bridge)
if not ovs_bridge:
raise oslo_cfg.RequiredOptError('ovs_bridge', 'neutron_defaults')
network = _make_vif_network(neutron_port, subnets)
network.bridge = ovs_bridge
if details.get('ovs_hybrid_plug'):
vif = osv_vif.VIFBridge(
id=neutron_port['id'],
address=neutron_port['mac_address'],
network=network,
has_traffic_filtering=details.get('port_filter', False),
preserve_on_delete=False,
active=_is_port_active(neutron_port),
port_profile=profile,
plugin=vif_plugin,
vif_name=_get_vif_name(neutron_port),
bridge_name=_get_ovs_hybrid_bridge_name(neutron_port))
else:
raise NotImplementedError(_LE(
"Non-hybrid OVS VIF is not supported yet"))
return vif
def neutron_to_osvif_vif(vif_plugin, neutron_port, subnets):
"""Converts Neutron port to os-vif VIF object.
:param vif_plugin: name of the os-vif plugin to use
:param neutron_port: dict containing port information as returned by
neutron client
:param subnets: subnet mapping as returned by PodSubnetsDriver.get_subnets
:return: os-vif VIF object
"""
try:
mgr = _VIF_MANAGERS[vif_plugin]
except KeyError:
mgr = stv_driver.DriverManager(namespace=_VIF_TRANSLATOR_NAMESPACE,
name=vif_plugin, invoke_on_load=False)
_VIF_MANAGERS[vif_plugin] = mgr
return mgr.driver(vif_plugin, neutron_port, subnets)

View File

@ -16,8 +16,10 @@
import mock
from os_vif.objects import route as osv_route
from oslo_config import cfg as o_cfg
from oslo_utils import uuidutils
from kuryr_kubernetes import exceptions as k_exc
from kuryr_kubernetes import os_vif_util as ovu
from kuryr_kubernetes.tests import base as test_base
@ -115,3 +117,238 @@ class TestOSVIFUtils(test_base.TestCase):
self.assertEqual(len(routes), len(route_list.objects))
for route in route_list.objects:
self.assertEqual(routes_map[str(route.cidr)], str(route.gateway))
@mock.patch('kuryr_kubernetes.os_vif_util._VIF_MANAGERS')
def test_neutron_to_osvif_vif(self, m_mgrs):
vif_plugin = mock.sentinel.vif_plugin
port = mock.sentinel.port
subnets = mock.sentinel.subnets
m_mgr = mock.Mock()
m_mgrs.__getitem__.return_value = m_mgr
ovu.neutron_to_osvif_vif(vif_plugin, port, subnets)
m_mgrs.__getitem__.assert_called_with(vif_plugin)
m_mgr.driver.assert_called_with(vif_plugin, port, subnets)
@mock.patch('stevedore.driver.DriverManager')
@mock.patch('kuryr_kubernetes.os_vif_util._VIF_MANAGERS')
def test_neutron_to_osvif_vif_load(self, m_mgrs, m_stv_drm):
vif_plugin = mock.sentinel.vif_plugin
port = mock.sentinel.port
subnets = mock.sentinel.subnets
m_mgr = mock.Mock()
m_mgrs.__getitem__.side_effect = KeyError
m_stv_drm.return_value = m_mgr
ovu.neutron_to_osvif_vif(vif_plugin, port, subnets)
m_stv_drm.assert_called_once_with(
namespace=ovu._VIF_TRANSLATOR_NAMESPACE,
name=vif_plugin,
invoke_on_load=False)
m_mgrs.__setitem__.assert_called_once_with(vif_plugin, m_mgr)
m_mgr.driver.assert_called_once_with(vif_plugin, port, subnets)
@mock.patch('kuryr_kubernetes.os_vif_util._get_ovs_hybrid_bridge_name')
@mock.patch('kuryr_kubernetes.os_vif_util._get_vif_name')
@mock.patch('kuryr_kubernetes.os_vif_util._is_port_active')
@mock.patch('kuryr_kubernetes.os_vif_util._make_vif_network')
@mock.patch('os_vif.objects.vif.VIFBridge')
@mock.patch('os_vif.objects.vif.VIFPortProfileOpenVSwitch')
def test_neutron_to_osvif_vif_ovs_hybrid(self,
m_mk_profile,
m_mk_vif,
m_make_vif_network,
m_is_port_active,
m_get_vif_name,
m_get_ovs_hybrid_bridge_name):
vif_plugin = 'ovs'
port_id = mock.sentinel.port_id
mac_address = mock.sentinel.mac_address
ovs_bridge = mock.sentinel.ovs_bridge
port_filter = mock.sentinel.port_filter
subnets = mock.sentinel.subnets
port_profile = mock.sentinel.port_profile
network = mock.sentinel.network
port_active = mock.sentinel.port_active
vif_name = mock.sentinel.vif_name
hybrid_bridge = mock.sentinel.hybrid_bridge
vif = mock.sentinel.vif
m_mk_profile.return_value = port_profile
m_make_vif_network.return_value = network
m_is_port_active.return_value = port_active
m_get_vif_name.return_value = vif_name
m_get_ovs_hybrid_bridge_name.return_value = hybrid_bridge
m_mk_vif.return_value = vif
port = {'id': port_id,
'mac_address': mac_address,
'binding:vif_details': {
'ovs_hybrid_plug': True,
'bridge_name': ovs_bridge,
'port_filter': port_filter},
}
self.assertEqual(vif, ovu.neutron_to_osvif_vif_ovs(vif_plugin, port,
subnets))
m_mk_profile.assert_called_once_with(interface_id=port_id)
m_make_vif_network.assert_called_once_with(port, subnets)
m_is_port_active.assert_called_once_with(port)
m_get_ovs_hybrid_bridge_name.assert_called_once_with(port)
m_get_vif_name.assert_called_once_with(port)
self.assertEqual(ovs_bridge, network.bridge)
m_mk_vif.assert_called_once_with(
id=port_id,
address=mac_address,
network=network,
has_traffic_filtering=port_filter,
preserve_on_delete=False,
active=port_active,
port_profile=port_profile,
plugin=vif_plugin,
vif_name=vif_name,
bridge_name=hybrid_bridge)
@mock.patch('kuryr_kubernetes.os_vif_util._make_vif_network')
@mock.patch('os_vif.objects.vif.VIFPortProfileOpenVSwitch')
def test_neutron_to_osvif_vif_ovs_native(self,
m_mk_profile,
m_make_vif_network):
vif_plugin = 'ovs'
port_id = mock.sentinel.port_id
mac_address = mock.sentinel.mac_address
ovs_bridge = mock.sentinel.ovs_bridge
subnets = mock.sentinel.subnets
port = {'id': port_id,
'mac_address': mac_address,
'binding:vif_details': {
'ovs_hybrid_plug': False,
'bridge_name': ovs_bridge},
}
# TODO(ivc): implement proper tests once ovs-native VIFs are supported
self.assertRaises(NotImplementedError, ovu.neutron_to_osvif_vif_ovs,
vif_plugin, port, subnets)
def test_neutron_to_osvif_vif_ovs_no_bridge(self):
vif_plugin = 'ovs'
port = {'id': uuidutils.generate_uuid()}
subnets = {}
self.assertRaises(o_cfg.RequiredOptError,
ovu.neutron_to_osvif_vif_ovs,
vif_plugin, port, subnets)
def test_get_ovs_hybrid_bridge_name(self):
port_id = uuidutils.generate_uuid()
port = {'id': port_id}
self.assertEqual("qbr" + port_id[:11],
ovu._get_ovs_hybrid_bridge_name(port))
def test_is_port_active(self):
port = {'status': 'ACTIVE'}
self.assertTrue(ovu._is_port_active(port))
def test_is_port_inactive(self):
port = {'status': 'DOWN'}
self.assertFalse(ovu._is_port_active(port))
@mock.patch('kuryr.lib.binding.drivers.utils.get_veth_pair_names')
def test_get_vif_name(self, m_get_veth_pair_names):
port_id = mock.sentinel.port_id
vif_name = mock.sentinel.vif_name
port = {'id': port_id}
m_get_veth_pair_names.return_value = (vif_name, mock.sentinel.any)
self.assertEqual(vif_name, ovu._get_vif_name(port))
m_get_veth_pair_names.assert_called_once_with(port_id)
@mock.patch('kuryr_kubernetes.os_vif_util._make_vif_subnets')
@mock.patch('os_vif.objects.subnet.SubnetList')
def test_make_vif_network(self, m_mk_subnet_list, m_make_vif_subnets):
network_id = mock.sentinel.network_id
network = mock.Mock()
orig_network = mock.Mock()
orig_network.id = network_id
orig_network.obj_clone.return_value = network
subnet_id = mock.sentinel.subnet_id
subnets = {subnet_id: orig_network}
vif_subnets = mock.sentinel.vif_subnets
subnet_list = mock.sentinel.subnet_list
m_make_vif_subnets.return_value = vif_subnets
m_mk_subnet_list.return_value = subnet_list
port = {'network_id': network_id}
self.assertEqual(network, ovu._make_vif_network(port, subnets))
self.assertEqual(subnet_list, network.subnets)
m_make_vif_subnets.assert_called_once_with(port, subnets)
m_mk_subnet_list.assert_called_once_with(objects=vif_subnets)
def test_make_vif_network_not_found(self):
network_id = mock.sentinel.network_id
port = {'network_id': network_id}
subnets = {}
self.assertRaises(k_exc.IntegrityError, ovu._make_vif_network,
port, subnets)
@mock.patch('kuryr_kubernetes.os_vif_util._make_vif_subnet')
@mock.patch('os_vif.objects.fixed_ip.FixedIP')
def test_make_vif_subnets(self, m_mk_fixed_ip, m_make_vif_subnet):
subnet_id = mock.sentinel.subnet_id
ip_address = mock.sentinel.ip_address
fixed_ip = mock.sentinel.fixed_ip
subnet = mock.Mock()
subnets = mock.MagicMock()
subnets.__contains__.return_value = True
m_mk_fixed_ip.return_value = fixed_ip
m_make_vif_subnet.return_value = subnet
port = {'fixed_ips': [
{'subnet_id': subnet_id, 'ip_address': ip_address}]}
self.assertEqual([subnet], ovu._make_vif_subnets(port, subnets))
m_make_vif_subnet.assert_called_once_with(subnets, subnet_id)
m_mk_fixed_ip.assert_called_once_with(address=ip_address)
subnet.ips.objects.append.assert_called_once_with(fixed_ip)
def test_make_vif_subnets_not_found(self):
subnet_id = mock.sentinel.subnet_id
ip_address = mock.sentinel.ip_address
subnets = mock.MagicMock()
subnets.__contains__.return_value = False
port = {'fixed_ips': [
{'subnet_id': subnet_id, 'ip_address': ip_address}]}
self.assertRaises(k_exc.IntegrityError, ovu._make_vif_subnets,
port, subnets)
@mock.patch('os_vif.objects.fixed_ip.FixedIPList')
def test_make_vif_subnet(self, m_mk_fixed_ip_list):
subnet_id = mock.sentinel.subnet_id
fixed_ip_list = mock.sentinel.fixed_ip_list
subnet = mock.Mock()
orig_subnet = mock.Mock()
orig_subnet.obj_clone.return_value = subnet
orig_network = mock.Mock()
orig_network.subnets.objects = [orig_subnet]
m_mk_fixed_ip_list.return_value = fixed_ip_list
subnets = {subnet_id: orig_network}
self.assertEqual(subnet, ovu._make_vif_subnet(subnets, subnet_id))
self.assertEqual(fixed_ip_list, subnet.ips)
m_mk_fixed_ip_list.assert_called_once_with(objects=[])
def test_make_vif_subnet_invalid(self):
subnet_id = mock.sentinel.subnet_id
orig_network = mock.Mock()
orig_network.subnets.objects = []
subnets = {subnet_id: orig_network}
self.assertRaises(k_exc.IntegrityError, ovu._make_vif_subnet,
subnets, subnet_id)

View File

@ -26,6 +26,9 @@ oslo.config.opts =
console_scripts =
kuryr-k8s-controller = kuryr_kubernetes.cmd.eventlet.controller:start
kuryr_kubernetes.vif_translators =
ovs = kuryr_kubernetes.os_vif_util:neutron_to_osvif_vif_ovs
kuryr_kubernetes.controller.drivers.pod_project =
default = kuryr_kubernetes.controller.drivers.default_project:DefaultPodProjectDriver