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
This commit is contained in:
Ilya Chukhnakov 2016-11-16 15:19:11 +03:00
parent 363575fe4c
commit 9e078d4a5c
10 changed files with 345 additions and 0 deletions

View File

@ -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

View File

@ -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()

View File

@ -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)}

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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