From 9e078d4a5c49715d67a80a203144f543594f22e8 Mon Sep 17 00:00:00 2001 From: Ilya Chukhnakov Date: Wed, 16 Nov 2016 15:19:11 +0300 Subject: [PATCH] Default pod subnet driver and os-vif utils This patch adds a new driver type used to determine Neutron subnet that should be used for Kubernetes pods' ports. This patch also provides a default subnet driver implementation that uses a subnet set in configuration file. This patch also introduces the 'os_vif_util' module that contains functions to translate data structures returned by Neutron client to os-vif objects. Only the subnet-related functions are added in this patch. Change-Id: I643b22858239ce7f64e6ba81822b31e788fc9990 Partially-Implements: blueprint kuryr-k8s-integration --- kuryr_kubernetes/config.py | 5 + kuryr_kubernetes/controller/drivers/base.py | 20 +++ .../controller/drivers/default_subnet.py | 52 ++++++++ kuryr_kubernetes/controller/service.py | 2 + kuryr_kubernetes/os_vif_util.py | 53 ++++++++ .../controller/drivers/test_default_subnet.py | 84 +++++++++++++ kuryr_kubernetes/tests/unit/kuryr_fixtures.py | 8 ++ .../tests/unit/test_os_vif_util.py | 117 ++++++++++++++++++ requirements.txt | 1 + setup.cfg | 3 + 10 files changed, 345 insertions(+) create mode 100644 kuryr_kubernetes/controller/drivers/default_subnet.py create mode 100644 kuryr_kubernetes/os_vif_util.py create mode 100644 kuryr_kubernetes/tests/unit/controller/drivers/test_default_subnet.py create mode 100644 kuryr_kubernetes/tests/unit/test_os_vif_util.py diff --git a/kuryr_kubernetes/config.py b/kuryr_kubernetes/config.py index dbc04c2af..de37bc76a 100644 --- a/kuryr_kubernetes/config.py +++ b/kuryr_kubernetes/config.py @@ -36,11 +36,16 @@ k8s_opts = [ cfg.StrOpt('pod_project_driver', help=_("The driver to determine OpenStack project for pod ports"), default='default'), + cfg.StrOpt('pod_subnets_driver', + help=_("The driver to determine Neutron subnets for pod ports"), + default='default'), ] neutron_defaults = [ cfg.StrOpt('project', help=_("Default OpenStack project ID for Kubernetes resources")), + cfg.StrOpt('pod_subnet', + help=_("Default Neutron subnet ID for Kubernetes pods")), ] CONF = cfg.CONF diff --git a/kuryr_kubernetes/controller/drivers/base.py b/kuryr_kubernetes/controller/drivers/base.py index e74dbffd0..18a9b5580 100644 --- a/kuryr_kubernetes/controller/drivers/base.py +++ b/kuryr_kubernetes/controller/drivers/base.py @@ -93,3 +93,23 @@ class PodProjectDriver(DriverBase): """ raise NotImplementedError() + + +@six.add_metaclass(abc.ABCMeta) +class PodSubnetsDriver(DriverBase): + """Provides subnets for Kubernetes Pods.""" + + ALIAS = 'pod_subnets' + + @abc.abstractmethod + def get_subnets(self, pod, project_id): + """Get subnets for Pod. + + :param pod: dict containing Kubernetes Pod object + :param project_id: OpenStack project ID + :return: dict containing the mapping 'subnet_id' -> 'network' for all + the subnets we want to create ports on, where 'network' is an + `os_vif.network.Network` object containing a single + `os_vif.subnet.Subnet` object corresponding to the 'subnet_id' + """ + raise NotImplementedError() diff --git a/kuryr_kubernetes/controller/drivers/default_subnet.py b/kuryr_kubernetes/controller/drivers/default_subnet.py new file mode 100644 index 000000000..a65ba5519 --- /dev/null +++ b/kuryr_kubernetes/controller/drivers/default_subnet.py @@ -0,0 +1,52 @@ +# Copyright (c) 2016 Mirantis, Inc. +# All Rights Reserved. +# +# 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 kuryr_kubernetes import clients +from kuryr_kubernetes import config +from kuryr_kubernetes.controller.drivers import base +from kuryr_kubernetes import os_vif_util + + +def _get_subnet(subnet_id): + # TODO(ivc): add caching (e.g. oslo.cache with dict backend) + neutron = clients.get_neutron_client() + + n_subnet = neutron.show_subnet(subnet_id).get('subnet') + network_id = n_subnet['network_id'] + n_network = neutron.show_network(network_id).get('network') + + subnet = os_vif_util.neutron_to_osvif_subnet(n_subnet) + network = os_vif_util.neutron_to_osvif_network(n_network) + network.subnets.objects.append(subnet) + + return network + + +class DefaultPodSubnetDriver(base.PodSubnetsDriver): + """Provides subnet for Pod port based on a configuration option.""" + + def get_subnets(self, pod, project_id): + subnet_id = config.CONF.neutron_defaults.pod_subnet + + if not subnet_id: + # NOTE(ivc): this option is only required for + # DefaultPodSubnetDriver and its subclasses, but it may be + # optional for other drivers (e.g. when each namespace has own + # subnet) + raise cfg.RequiredOptError('pod_subnet', 'neutron_defaults') + + return {subnet_id: _get_subnet(subnet_id)} diff --git a/kuryr_kubernetes/controller/service.py b/kuryr_kubernetes/controller/service.py index 4a84b18af..3eb961b39 100644 --- a/kuryr_kubernetes/controller/service.py +++ b/kuryr_kubernetes/controller/service.py @@ -16,6 +16,7 @@ import sys from kuryr.lib._i18n import _LI, _LE +import os_vif from oslo_log import log as logging from oslo_service import service @@ -101,5 +102,6 @@ def start(): config.init(sys.argv[1:]) config.setup_logging() clients.setup_clients() + os_vif.initialize() kuryrk8s_launcher = service.launch(config.CONF, KuryrK8sService()) kuryrk8s_launcher.wait() diff --git a/kuryr_kubernetes/os_vif_util.py b/kuryr_kubernetes/os_vif_util.py new file mode 100644 index 000000000..9678a635a --- /dev/null +++ b/kuryr_kubernetes/os_vif_util.py @@ -0,0 +1,53 @@ +# Copyright (c) 2016 Mirantis, Inc. +# All Rights Reserved. +# +# 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 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 + + +# REVISIT(ivc): consider making this module part of kuryr-lib + + +def neutron_to_osvif_network(neutron_network): + obj = osv_network.Network(id=neutron_network['id']) + + if neutron_network.get('name') is not None: + obj.label = neutron_network['name'] + + if neutron_network.get('mtu') is not None: + obj.mtu = neutron_network['mtu'] + + return obj + + +def neutron_to_osvif_subnet(neutron_subnet): + obj = osv_subnet.Subnet( + cidr=neutron_subnet['cidr'], + dns=neutron_subnet['dns_nameservers'], + routes=_neutron_to_osvif_routes(neutron_subnet['host_routes'])) + + if neutron_subnet.get('gateway_ip') is not None: + obj.gateway = neutron_subnet['gateway_ip'] + + return obj + + +def _neutron_to_osvif_routes(neutron_routes): + obj_list = [osv_route.Route(cidr=route['destination'], + gateway=route['nexthop']) + for route in neutron_routes] + + return osv_route.RouteList(objects=obj_list) diff --git a/kuryr_kubernetes/tests/unit/controller/drivers/test_default_subnet.py b/kuryr_kubernetes/tests/unit/controller/drivers/test_default_subnet.py new file mode 100644 index 000000000..da680cfcc --- /dev/null +++ b/kuryr_kubernetes/tests/unit/controller/drivers/test_default_subnet.py @@ -0,0 +1,84 @@ +# Copyright (c) 2016 Mirantis, Inc. +# All Rights Reserved. +# +# 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 mock + +from oslo_config import cfg + +from kuryr_kubernetes.controller.drivers import default_subnet +from kuryr_kubernetes.tests import base as test_base +from kuryr_kubernetes.tests.unit import kuryr_fixtures as k_fix + + +class TestDefaultPodSubnetDriver(test_base.TestCase): + + @mock.patch('kuryr_kubernetes.controller.drivers' + '.default_subnet._get_subnet') + @mock.patch('kuryr_kubernetes.config.CONF') + def test_get_subnets(self, m_cfg, m_get_subnet): + subnet_id = mock.sentinel.subnet_id + subnet = mock.sentinel.subnet + pod = mock.sentinel.pod + project_id = mock.sentinel.project_id + m_cfg.neutron_defaults.pod_subnet = subnet_id + m_get_subnet.return_value = subnet + driver = default_subnet.DefaultPodSubnetDriver() + + subnets = driver.get_subnets(pod, project_id) + + self.assertEqual({subnet_id: subnet}, subnets) + m_get_subnet.assert_called_once_with(subnet_id) + + @mock.patch('kuryr_kubernetes.controller.drivers' + '.default_subnet._get_subnet') + def test_get_subnets_not_set(self, m_get_subnet): + pod = mock.sentinel.pod + project_id = mock.sentinel.project_id + driver = default_subnet.DefaultPodSubnetDriver() + + self.assertRaises(cfg.RequiredOptError, driver.get_subnets, pod, + project_id) + m_get_subnet.assert_not_called() + + +class TestGetSubnet(test_base.TestCase): + + @mock.patch('kuryr_kubernetes.os_vif_util.neutron_to_osvif_network') + @mock.patch('kuryr_kubernetes.os_vif_util.neutron_to_osvif_subnet') + def test_get_subnet(self, m_osv_subnet, m_osv_network): + neutron = self.useFixture(k_fix.MockNeutronClient()).client + + subnet = mock.MagicMock() + network = mock.MagicMock() + subnet_id = mock.sentinel.subnet_id + network_id = mock.sentinel.network_id + + neutron_subnet = {'network_id': network_id} + neutron_network = mock.sentinel.neutron_network + + neutron.show_subnet.return_value = {'subnet': neutron_subnet} + neutron.show_network.return_value = {'network': neutron_network} + + m_osv_subnet.return_value = subnet + m_osv_network.return_value = network + + ret = default_subnet._get_subnet(subnet_id) + + self.assertEqual(network, ret) + neutron.show_subnet.assert_called_once_with(subnet_id) + neutron.show_network.assert_called_once_with(network_id) + m_osv_subnet.assert_called_once_with(neutron_subnet) + m_osv_network.assert_called_once_with(neutron_network) + network.subnets.objects.append.assert_called_once_with(subnet) diff --git a/kuryr_kubernetes/tests/unit/kuryr_fixtures.py b/kuryr_kubernetes/tests/unit/kuryr_fixtures.py index 00cc452c4..6cf57e80a 100644 --- a/kuryr_kubernetes/tests/unit/kuryr_fixtures.py +++ b/kuryr_kubernetes/tests/unit/kuryr_fixtures.py @@ -25,3 +25,11 @@ class MockK8sClient(fixtures.Fixture): self.useFixture(fixtures.MockPatch( 'kuryr_kubernetes.clients.get_kubernetes_client', lambda: self.client)) + + +class MockNeutronClient(fixtures.Fixture): + def _setUp(self): + self.client = mock.Mock() + self.useFixture(fixtures.MockPatch( + 'kuryr_kubernetes.clients.get_neutron_client', + lambda: self.client)) diff --git a/kuryr_kubernetes/tests/unit/test_os_vif_util.py b/kuryr_kubernetes/tests/unit/test_os_vif_util.py new file mode 100644 index 000000000..53e69ba2b --- /dev/null +++ b/kuryr_kubernetes/tests/unit/test_os_vif_util.py @@ -0,0 +1,117 @@ +# Copyright (c) 2016 Mirantis, Inc. +# All Rights Reserved. +# +# 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 mock + +from os_vif.objects import route as osv_route +from oslo_utils import uuidutils + +from kuryr_kubernetes import os_vif_util as ovu +from kuryr_kubernetes.tests import base as test_base + + +# REVISIT(ivc): move to kuryr-lib along with 'os_vif_util' + + +class TestOSVIFUtils(test_base.TestCase): + def test_neutron_to_osvif_network(self): + network_id = uuidutils.generate_uuid() + network_name = 'test-net' + network_mtu = 1500 + neutron_network = { + 'id': network_id, + 'name': network_name, + 'mtu': network_mtu, + } + + network = ovu.neutron_to_osvif_network(neutron_network) + + self.assertEqual(network_id, network.id) + self.assertEqual(network_name, network.label) + self.assertEqual(network_mtu, network.mtu) + + def test_neutron_to_osvif_network_no_name(self): + network_id = uuidutils.generate_uuid() + network_mtu = 1500 + neutron_network = { + 'id': network_id, + 'mtu': network_mtu, + } + + network = ovu.neutron_to_osvif_network(neutron_network) + + self.assertFalse(network.obj_attr_is_set('label')) + + def test_neutron_to_osvif_network_no_mtu(self): + network_id = uuidutils.generate_uuid() + network_name = 'test-net' + neutron_network = { + 'id': network_id, + 'name': network_name, + } + + network = ovu.neutron_to_osvif_network(neutron_network) + + self.assertEqual(None, network.mtu) + + @mock.patch('kuryr_kubernetes.os_vif_util._neutron_to_osvif_routes') + def test_neutron_to_osvif_subnet(self, m_conv_routes): + gateway = '1.1.1.1' + cidr = '1.1.1.1/8' + dns = ['2.2.2.2', '3.3.3.3'] + host_routes = mock.sentinel.host_routes + route_list = osv_route.RouteList(objects=[ + osv_route.Route(cidr='4.4.4.4/8', gateway='5.5.5.5')]) + m_conv_routes.return_value = route_list + neutron_subnet = { + 'cidr': cidr, + 'dns_nameservers': dns, + 'host_routes': host_routes, + 'gateway_ip': gateway, + } + + subnet = ovu.neutron_to_osvif_subnet(neutron_subnet) + + self.assertEqual(cidr, str(subnet.cidr)) + self.assertEqual(route_list, subnet.routes) + self.assertEqual(set(dns), set([str(addr) for addr in subnet.dns])) + self.assertEqual(gateway, str(subnet.gateway)) + m_conv_routes.assert_called_once_with(host_routes) + + @mock.patch('kuryr_kubernetes.os_vif_util._neutron_to_osvif_routes') + def test_neutron_to_osvif_subnet_no_gateway(self, m_conv_routes): + cidr = '1.1.1.1/8' + route_list = osv_route.RouteList() + m_conv_routes.return_value = route_list + neutron_subnet = { + 'cidr': cidr, + 'dns_nameservers': [], + 'host_routes': [], + } + + subnet = ovu.neutron_to_osvif_subnet(neutron_subnet) + + self.assertFalse(subnet.obj_attr_is_set('gateway')) + + def test_neutron_to_osvif_routes(self): + routes_map = {'%s.0.0.0/8' % i: '10.0.0.%s' % i for i in range(3)} + routes = [{'destination': k, 'nexthop': v} + for k, v in routes_map.items()] + + route_list = ovu._neutron_to_osvif_routes(routes) + + self.assertEqual(len(routes), len(route_list.objects)) + for route in route_list.objects: + self.assertEqual(routes_map[str(route.cidr)], str(route.gateway)) diff --git a/requirements.txt b/requirements.txt index 6a751e562..5bf02ebb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,6 @@ oslo.log>=3.11.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0 oslo.utils>=3.16.0 # Apache-2.0 +os-vif>=1.3.0 # Apache-2.0 six>=1.9.0 # MIT stevedore>=1.17.1 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 84b9616bb..d47211801 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,9 @@ console_scripts = kuryr_kubernetes.controller.drivers.pod_project = default = kuryr_kubernetes.controller.drivers.default_project:DefaultPodProjectDriver +kuryr_kubernetes.controller.drivers.pod_subnets = + default = kuryr_kubernetes.controller.drivers.default_subnet:DefaultPodSubnetDriver + [files] packages = kuryr_kubernetes