From 1042a7077bba049d51022b7f4914048afb521cb1 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 8 May 2018 16:46:13 +0100 Subject: Add new Kubernetes resource kind: "Network" Add a new Kubernetes resource kind: "Network" [1] [2] Kubernetes network plugins (alpha definition): [3] Network definition example in Kubernetes: apiVersion: "kubernetes.com/v1" kind: Network metadata: name: flannel plugin: flannel args: '[ {delegate": {"isDefaultGateway": true}} ]' Proposed Kubernetes context network definition: context: networks: - name: flannel plugin: flannel args: (string) - name: sriov_upload plugin: sriov args: (string) [1]https://builders.intel.com/docs/networkbuilders/multiple-network-interfaces-in-kubernetes-application-note.pdf [2]http://cdn.opensourcecloud.cn/zt/k8s/01.pdf [3]https://kubernetes.io/docs/concepts/cluster-administration/network-plugins/ JIRA: YARDSTICK-1160 Change-Id: I71a49ac14e3d28ded91d2ed3cd9cc527e40303f7 Signed-off-by: Rodolfo Alonso Hernandez --- yardstick/benchmark/contexts/kubernetes.py | 12 +++ yardstick/common/exceptions.py | 9 ++ yardstick/common/kubernetes_utils.py | 47 ++++++++ yardstick/orchestrator/kubernetes.py | 99 ++++++++++++++++- .../tests/unit/common/test_kubernetes_utils.py | 119 +++++++++++++++++++++ .../tests/unit/orchestrator/test_kubernetes.py | 88 +++++++++++++++ 6 files changed, 370 insertions(+), 4 deletions(-) diff --git a/yardstick/benchmark/contexts/kubernetes.py b/yardstick/benchmark/contexts/kubernetes.py index 8cc5dc994..ddfc530cb 100644 --- a/yardstick/benchmark/contexts/kubernetes.py +++ b/yardstick/benchmark/contexts/kubernetes.py @@ -49,6 +49,7 @@ class KubernetesContext(Context): self._set_ssh_key() self._create_crd() + self._create_networks() LOG.info('Launch containers') self._create_rcs() self._create_services() @@ -62,6 +63,7 @@ class KubernetesContext(Context): self._delete_rcs() self._delete_pods() self._delete_services() + self._delete_networks() self._delete_crd() super(KubernetesContext, self).undeploy() @@ -119,6 +121,16 @@ class KubernetesContext(Context): for crd in self.template.crd: crd.delete() + def _create_networks(self): # pragma: no cover + LOG.info('Create Network elements') + for net in self.template.network_objs: + net.create() + + def _delete_networks(self): # pragma: no cover + LOG.info('Create Network elements') + for net in self.template.network_objs: + net.delete() + def _get_key_path(self): task_id = self.name.split('-')[-1] k = 'files/yardstick_key-{}'.format(task_id) diff --git a/yardstick/common/exceptions.py b/yardstick/common/exceptions.py index e370b92bf..2005bebd2 100644 --- a/yardstick/common/exceptions.py +++ b/yardstick/common/exceptions.py @@ -215,6 +215,15 @@ class KubernetesCRDObjectDefinitionError(YardstickException): 'parameters: %(missing_parameters)s') +class KubernetesNetworkObjectDefinitionError(YardstickException): + message = ('Kubernetes Network object definition error, missing ' + 'parameters: %(missing_parameters)s') + + +class KubernetesNetworkObjectKindMissing(YardstickException): + message = 'Kubernetes kind "Network" is not defined' + + class ScenarioCreateNetworkError(YardstickException): message = 'Create Neutron Network Scenario failed' diff --git a/yardstick/common/kubernetes_utils.py b/yardstick/common/kubernetes_utils.py index a472b6de5..42267fc41 100644 --- a/yardstick/common/kubernetes_utils.py +++ b/yardstick/common/kubernetes_utils.py @@ -36,6 +36,14 @@ def get_extensions_v1beta_api(): return client.ApiextensionsV1beta1Api() +def get_custom_objects_api(): + try: + config.load_kube_config(config_file=consts.K8S_CONF_FILE) + except IOError: + raise exceptions.KubernetesConfigFileNotFound() + return client.CustomObjectsApi() + + def get_node_list(**kwargs): # pragma: no cover core_v1_api = get_core_api() try: @@ -220,6 +228,45 @@ def delete_custom_resource_definition(name): action='delete', resource='CustomResourceDefinition') +def get_custom_resource_definition(kind): + api = get_extensions_v1beta_api() + try: + crd_list = api.list_custom_resource_definition() + for crd_obj in (crd_obj for crd_obj in crd_list.items + if crd_obj.spec.names.kind == kind): + return crd_obj + return None + except ApiException: + raise exceptions.KubernetesApiException( + action='delete', resource='CustomResourceDefinition') + + +def create_network(scope, group, version, plural, body, namespace='default'): + api = get_custom_objects_api() + try: + if scope == consts.SCOPE_CLUSTER: + api.create_cluster_custom_object(group, version, plural, body) + else: + api.create_namespaced_custom_object( + group, version, namespace, plural, body) + except ApiException: + raise exceptions.KubernetesApiException( + action='create', resource='Custom Object: Network') + + +def delete_network(scope, group, version, plural, name, namespace='default'): + api = get_custom_objects_api() + try: + if scope == consts.SCOPE_CLUSTER: + api.delete_cluster_custom_object(group, version, plural, name, {}) + else: + api.delete_namespaced_custom_object( + group, version, namespace, plural, name, {}) + except ApiException: + raise exceptions.KubernetesApiException( + action='delete', resource='Custom Object: Network') + + def get_pod_list(namespace='default'): # pragma: no cover core_v1_api = get_core_api() try: diff --git a/yardstick/orchestrator/kubernetes.py b/yardstick/orchestrator/kubernetes.py index 637abd813..44a333e4b 100644 --- a/yardstick/orchestrator/kubernetes.py +++ b/yardstick/orchestrator/kubernetes.py @@ -91,9 +91,7 @@ class KubernetesObject(object): "replicas": 1, "template": { "metadata": { - "labels": { - "app": name - } + "labels": {"app": name} }, "spec": { "containers": [], @@ -237,6 +235,98 @@ class CustomResourceDefinitionObject(object): k8s_utils.delete_custom_resource_definition(self._name) +class NetworkObject(object): + + MANDATORY_PARAMETERS = {'name', 'plugin', 'args'} + KIND = 'Network' + + def __init__(self, **kwargs): + if not self.MANDATORY_PARAMETERS.issubset(kwargs): + missing_parameters = ', '.join( + str(param) for param in + (self.MANDATORY_PARAMETERS - set(kwargs))) + raise exceptions.KubernetesNetworkObjectDefinitionError( + missing_parameters=missing_parameters) + + self._name = kwargs['name'] + self._plugin = kwargs['plugin'] + self._args = kwargs['args'] + self._crd = None + self._template = None + self._group = None + self._version = None + self._plural = None + self._scope = None + + @property + def crd(self): + if self._crd: + return self._crd + crd = k8s_utils.get_custom_resource_definition(self.KIND) + if not crd: + raise exceptions.KubernetesNetworkObjectKindMissing() + self._crd = crd + return self._crd + + @property + def group(self): + if self._group: + return self._group + self._group = self.crd.spec.group + return self._group + + @property + def version(self): + if self._version: + return self._version + self._version = self.crd.spec.version + return self._version + + @property + def plural(self): + if self._plural: + return self._plural + self._plural = self.crd.spec.names.plural + return self._plural + + @property + def scope(self): + if self._scope: + return self._scope + self._scope = self.crd.spec.scope + return self._scope + + @property + def template(self): + """"Network" object template + + This template can be rendered only once the CRD "Network" is created in + Kubernetes. This function call must be delayed until the creation of + the CRD "Network". + """ + if self._template: + return self._template + + self._template = { + 'apiVersion': '{}/{}'.format(self.group, self.version), + 'kind': self.KIND, + 'metadata': { + 'name': self._name + }, + 'plugin': self._plugin, + 'args': self._args + } + return self._template + + def create(self): + k8s_utils.create_network(self.scope, self.group, self.version, + self.plural, self.template) + + def delete(self): + k8s_utils.delete_network(self.scope, self.group, self.version, + self.plural, self._name) + + class KubernetesTemplate(object): def __init__(self, name, context_cfg): @@ -248,6 +338,7 @@ class KubernetesTemplate(object): context_cfg = copy.deepcopy(context_cfg) servers_cfg = context_cfg.pop('servers', {}) crd_cfg = context_cfg.pop('custom_resources', []) + networks_cfg = context_cfg.pop('networks', []) self.name = name self.ssh_key = '{}-key'.format(name) @@ -259,7 +350,7 @@ class KubernetesTemplate(object): self.service_objs = [ServiceObject(s) for s in self.rcs] self.crd = [CustomResourceDefinitionObject(self.name, **crd) for crd in crd_cfg] - + self.network_objs = [NetworkObject(**nobj) for nobj in networks_cfg] self.pods = [] def _get_rc_name(self, rc_name): diff --git a/yardstick/tests/unit/common/test_kubernetes_utils.py b/yardstick/tests/unit/common/test_kubernetes_utils.py index e264fd94b..bf9992b57 100644 --- a/yardstick/tests/unit/common/test_kubernetes_utils.py +++ b/yardstick/tests/unit/common/test_kubernetes_utils.py @@ -40,6 +40,23 @@ class GetExtensionsV1betaApiTestCase(base.BaseUnitTestCase): kubernetes_utils.get_extensions_v1beta_api() +class GetCustomObjectsApiTestCase(base.BaseUnitTestCase): + + @mock.patch.object(client, 'CustomObjectsApi', return_value='api') + @mock.patch.object(config, 'load_kube_config') + def test_execute_correct(self, mock_load_kube_config, mock_api): + self.assertEqual('api', kubernetes_utils.get_custom_objects_api()) + mock_load_kube_config.assert_called_once_with( + config_file=constants.K8S_CONF_FILE) + mock_api.assert_called_once() + + @mock.patch.object(config, 'load_kube_config') + def test_execute_exception(self, mock_load_kube_config): + mock_load_kube_config.side_effect = IOError + with self.assertRaises(exceptions.KubernetesConfigFileNotFound): + kubernetes_utils.get_custom_objects_api() + + class CreateCustomResourceDefinitionTestCase(base.BaseUnitTestCase): @mock.patch.object(client, 'V1beta1CustomResourceDefinition', @@ -103,3 +120,105 @@ class DeleteCustomResourceDefinitionTestCase(base.BaseUnitTestCase): mock_delobj.assert_called_once() mock_delete_crd.delete_custom_resource_definition.\ assert_called_once_with('name', 'del_obj') + + +class GetCustomResourceDefinitionTestCase(base.BaseUnitTestCase): + + @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api') + def test_execute_value(self, mock_get_api): + crd_obj = mock.Mock() + crd_obj.spec.names.kind = 'some_kind' + crd_list = mock.Mock() + crd_list.items = [crd_obj] + mock_api = mock.Mock() + mock_api.list_custom_resource_definition.return_value = crd_list + mock_get_api.return_value = mock_api + self.assertEqual( + crd_obj, + kubernetes_utils.get_custom_resource_definition('some_kind')) + + @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api') + def test_execute_none(self, mock_get_api): + crd_obj = mock.Mock() + crd_obj.spec.names.kind = 'some_kind' + crd_list = mock.Mock() + crd_list.items = [crd_obj] + mock_api = mock.Mock() + mock_api.list_custom_resource_definition.return_value = crd_list + mock_get_api.return_value = mock_api + self.assertIsNone( + kubernetes_utils.get_custom_resource_definition('other_kind')) + + @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api') + def test_execute_exception(self, mock_get_api): + mock_api = mock.Mock() + mock_api.list_custom_resource_definition.\ + side_effect = rest.ApiException + mock_get_api.return_value = mock_api + with self.assertRaises(exceptions.KubernetesApiException): + kubernetes_utils.get_custom_resource_definition('kind') + + +class CreateNetworkTestCase(base.BaseUnitTestCase): + @mock.patch.object(kubernetes_utils, 'get_custom_objects_api') + def test_execute_correct(self, mock_get_api): + mock_api = mock.Mock() + mock_get_api.return_value = mock_api + group = 'group.com' + version = mock.Mock() + plural = 'networks' + body = mock.Mock() + + kubernetes_utils.create_network( + constants.SCOPE_CLUSTER, group, version, plural, body) + mock_api.create_cluster_custom_object.assert_called_once_with( + group, version, plural, body) + + mock_api.reset_mock() + kubernetes_utils.create_network( + constants.SCOPE_NAMESPACED, group, version, plural, body) + mock_api.create_namespaced_custom_object.assert_called_once_with( + group, version, 'default', plural, body) + + + @mock.patch.object(kubernetes_utils, 'get_custom_objects_api') + def test_execute_exception(self, mock_get_api): + mock_api = mock.Mock() + mock_api.create_cluster_custom_object.side_effect = rest.ApiException + mock_get_api.return_value = mock_api + with self.assertRaises(exceptions.KubernetesApiException): + kubernetes_utils.create_network( + constants.SCOPE_CLUSTER, mock.ANY, mock.ANY, mock.ANY, + mock.ANY) + + +class DeleteNetworkTestCase(base.BaseUnitTestCase): + @mock.patch.object(kubernetes_utils, 'get_custom_objects_api') + def test_execute_correct(self, mock_get_api): + mock_api = mock.Mock() + mock_get_api.return_value = mock_api + group = 'group.com' + version = mock.Mock() + plural = 'networks' + name = 'network' + + kubernetes_utils.delete_network( + constants.SCOPE_CLUSTER, group, version, plural, name) + mock_api.delete_cluster_custom_object.assert_called_once_with( + group, version, plural, name, {}) + + mock_api.reset_mock() + kubernetes_utils.delete_network( + constants.SCOPE_NAMESPACED, group, version, plural, name) + mock_api.delete_namespaced_custom_object.assert_called_once_with( + group, version, 'default', plural, name, {}) + + @mock.patch.object(kubernetes_utils, 'get_custom_objects_api') + def test_execute_exception(self, mock_get_api): + mock_api = mock.Mock() + mock_api.delete_cluster_custom_object.side_effect = rest.ApiException + mock_get_api.return_value = mock_api + with self.assertRaises(exceptions.KubernetesApiException): + kubernetes_utils.delete_network( + constants.SCOPE_CLUSTER, mock.ANY, mock.ANY, mock.ANY, + mock.ANY) diff --git a/yardstick/tests/unit/orchestrator/test_kubernetes.py b/yardstick/tests/unit/orchestrator/test_kubernetes.py index 50c6b2773..e45545d6a 100644 --- a/yardstick/tests/unit/orchestrator/test_kubernetes.py +++ b/yardstick/tests/unit/orchestrator/test_kubernetes.py @@ -297,3 +297,91 @@ class CustomResourceDefinitionObjectTestCase(base.BaseUnitTestCase): with self.assertRaises(exceptions.KubernetesCRDObjectDefinitionError): kubernetes.CustomResourceDefinitionObject('ctx_name', noname='name') + + +class NetworkObjectTestCase(base.BaseUnitTestCase): + + def setUp(self): + self.net_obj = kubernetes.NetworkObject(name='fake_name', + plugin='fake_plugin', + args='fake_args') + + def test__init_missing_parameter(self): + with self.assertRaises( + exceptions.KubernetesNetworkObjectDefinitionError): + kubernetes.NetworkObject(name='name', plugin='plugin') + with self.assertRaises( + exceptions.KubernetesNetworkObjectDefinitionError): + kubernetes.NetworkObject(name='name', args='args') + with self.assertRaises( + exceptions.KubernetesNetworkObjectDefinitionError): + kubernetes.NetworkObject(args='args', plugin='plugin') + + @mock.patch.object(kubernetes_utils, 'get_custom_resource_definition') + def test_crd(self, mock_get_crd): + mock_crd = mock.Mock() + mock_get_crd.return_value = mock_crd + net_obj = copy.deepcopy(self.net_obj) + self.assertEqual(mock_crd, net_obj.crd) + + def test_template(self): + net_obj = copy.deepcopy(self.net_obj) + expected = {'apiVersion': 'group.com/v2', + 'kind': kubernetes.NetworkObject.KIND, + 'metadata': { + 'name': 'fake_name'}, + 'plugin': 'fake_plugin', + 'args': 'fake_args'} + crd = mock.Mock() + crd.spec.group = 'group.com' + crd.spec.version = 'v2' + net_obj._crd = crd + self.assertEqual(expected, net_obj.template) + + def test_group(self): + net_obj = copy.deepcopy(self.net_obj) + net_obj._crd = mock.Mock() + net_obj._crd.spec.group = 'fake_group' + self.assertEqual('fake_group', net_obj.group) + + def test_version(self): + net_obj = copy.deepcopy(self.net_obj) + net_obj._crd = mock.Mock() + net_obj._crd.spec.version = 'version_4' + self.assertEqual('version_4', net_obj.version) + + def test_plural(self): + net_obj = copy.deepcopy(self.net_obj) + net_obj._crd = mock.Mock() + net_obj._crd.spec.names.plural = 'name_ending_in_s' + self.assertEqual('name_ending_in_s', net_obj.plural) + + def test_scope(self): + net_obj = copy.deepcopy(self.net_obj) + net_obj._crd = mock.Mock() + net_obj._crd.spec.scope = 'Cluster' + self.assertEqual('Cluster', net_obj.scope) + + @mock.patch.object(kubernetes_utils, 'create_network') + def test_create(self, mock_create_network): + net_obj = copy.deepcopy(self.net_obj) + net_obj._scope = 'scope' + net_obj._group = 'group' + net_obj._version = 'version' + net_obj._plural = 'plural' + net_obj._template = 'template' + net_obj.create() + mock_create_network.assert_called_once_with( + 'scope', 'group', 'version', 'plural', 'template') + + @mock.patch.object(kubernetes_utils, 'delete_network') + def test_delete(self, mock_delete_network): + net_obj = copy.deepcopy(self.net_obj) + net_obj._scope = 'scope' + net_obj._group = 'group' + net_obj._version = 'version' + net_obj._plural = 'plural' + net_obj._name = 'name' + net_obj.delete() + mock_delete_network.assert_called_once_with( + 'scope', 'group', 'version', 'plural', 'name') -- cgit 1.2.3-korg