From fb6654aafdc5303077325691b7f468b27d7493c6 Mon Sep 17 00:00:00 2001 From: spisarski Date: Wed, 22 Nov 2017 14:55:43 -0700 Subject: Implemented the ability to create Magnum Cluster Type objects. This patch is the second of several necessary for the addition of Magnum support. This one implements a create and delete function to magnum_utils.py as well as the necessary config and domain classes for sending and receiving Cluster Type data to OpenStack. JIRA: SNAPS-233 Change-Id: Iad1959b98eaabc4ef5f41b70a23f6b1306259650 Signed-off-by: spisarski --- docs/how-to-use/APITests.rst | 11 + docs/how-to-use/UnitTests.rst | 12 + snaps/config/cluster_template.py | 306 ++++++++++++++++++++++ snaps/config/network.py | 9 +- snaps/config/tests/cluster_template_tests.py | 180 +++++++++++++ snaps/config/volume.py | 5 +- snaps/domain/cluster_template.py | 133 ++++++++++ snaps/domain/test/cluster_template_tests.py | 109 ++++++++ snaps/openstack/tests/openstack_tests.py | 16 +- snaps/openstack/utils/magnum_utils.py | 73 +++++- snaps/openstack/utils/tests/magnum_utils_tests.py | 126 ++++++++- snaps/test_suite_builder.py | 12 +- 12 files changed, 970 insertions(+), 22 deletions(-) create mode 100644 snaps/config/cluster_template.py create mode 100644 snaps/config/tests/cluster_template_tests.py create mode 100644 snaps/domain/cluster_template.py create mode 100644 snaps/domain/test/cluster_template_tests.py diff --git a/docs/how-to-use/APITests.rst b/docs/how-to-use/APITests.rst index ee0d894..6a7c317 100644 --- a/docs/how-to-use/APITests.rst +++ b/docs/how-to-use/APITests.rst @@ -544,6 +544,17 @@ heat_utils_tests.py - HeatUtilsFlavorTests | | | Flavor domain objects deployed with Heat | +---------------------------------------+---------------+-----------------------------------------------------------+ +magnum_utils_tests.py - MagnumUtilsTests +---------------------------------------- + ++---------------------------------------+---------------+-----------------------------------------------------------+ +| Test Name | Magnum API | Description | ++=======================================+===============+===========================================================+ +| test_create_cluster_template_simple | 1 | Tests ability of the function | +| | | magnum_utils.create_cluster_template() to create a simple | +| | | cluster template OpenStack object with minimal config | ++---------------------------------------+---------------+-----------------------------------------------------------+ + settings_utils_tests.py - SettingsUtilsNetworkingTests ------------------------------------------------------ diff --git a/docs/how-to-use/UnitTests.rst b/docs/how-to-use/UnitTests.rst index cb0c5f3..5bd4f08 100644 --- a/docs/how-to-use/UnitTests.rst +++ b/docs/how-to-use/UnitTests.rst @@ -396,6 +396,18 @@ VmInstDomainObjectTests Ensures that all required members are included when constructing a VmInst domain object +ClusterTemplateConfigUnitTests +------------------------------ + +Ensures that all required members are included when constructing a +ClusterTemplateConfig object + +ClusterTemplateUnitTests +------------------------ + +Ensures that all required members are included when constructing a +ClusterTemplate object + SettingsUtilsUnitTests ---------------------- diff --git a/snaps/config/cluster_template.py b/snaps/config/cluster_template.py new file mode 100644 index 0000000..a20225a --- /dev/null +++ b/snaps/config/cluster_template.py @@ -0,0 +1,306 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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 enum +from neutronclient.common.utils import str2bool + + +class ServerType(enum.Enum): + """ + The cluter server types supported + """ + vm = 'vm' + baremetal = 'baremetal' + + +class ContainerOrchestrationEngine(enum.Enum): + """ + The types of supported COEs + """ + kubernetes = 'kubernetes' + swarm = 'swarm' + mesos = 'mesos' + + +class DockerStorageDriver(enum.Enum): + """ + Drivers for managing storage for the images in the container's writable + layer + """ + devicemapper = 'devicemapper' + overlay = 'overlay' + + +class ClusterTemplateConfig(object): + """ + Configuration settings for OpenStack cluster template creation + """ + + def __init__(self, **kwargs): + """ + Constructor + :param name: the cluster type's name (required) + :param image: name or ID of the base image in Glance used to boot the + cluster's servers. The image must have the attribute + 'os-distro' defined as appropriate for the cluster + driver (required) + :param keypair: name or ID of the keypair to gain cluster machine + access (required) + :param network_driver: The name of a network driver for providing the + networks for the containers. Note that this is + different and separate from the Neutron network + for the bay/cluster. The operation and + networking model are specific to the particular + driver (optional) + :param external_net: name or IDof the external Neutron network to + provide connectivity to the cluster (required) + :param floating_ip_enabled: Whether enable or not using the floating IP + of cloud provider. Some cloud providers + used floating IP, some used public IP, + thus Magnum provide this option for + specifying the choice of using floating IP + (default - True) + :param docker_volume_size: The size in GB for the local storage on each + server for the Docker daemon to cache the + images and host the containers. Cinder + volumes provide the storage. The default is + 25 GB. For the devicemapper storage driver, + the minimum value is 3GB. For the overlay + storage driver, the minimum value is 1GB. + (default - 3) + :param server_type: ServerType enumeration (default - vm) + :param flavor: name or ID of the nova flavor for booting the node + servers (default - m1.small) + :param master_flavor: name or ID of the nova flavor of the master node + for this cluster (optional) + :param coe: ContainerOrchestrationEngine enum instance + (default - kubernetes) + :param fixed_net: name of a Neutron network to provide connectivity + to the internal network for the cluster + (optional) + :param fixed_subnet: Fixed subnet that are using to allocate network + address for nodes in bay/cluster (optional) + :param registry_enabled: Docker images by default are pulled from the + public Docker registry, but in some cases, + users may want to use a private registry. + This option provides an alternative registry + based on the Registry V2: Magnum will create a + local registry in the bay/cluster backed by + swift to host the images (default - True) + :param insecure_registry: The URL pointing to the user's own private + insecure docker registry to deploy and run + docker containers (optional) + :param docker_storage_driver: DockerStorageDriver enum instance to + manage storage for the images and + container's writable layer + (default - devicemapper) + :param dns_nameserver: The DNS nameserver for the servers and + containers in the bay/cluster to use. + This is configured in the private Neutron + network for the bay/cluster. + (default provided by Magnum - 8.8.8.8) + :param public: denotes whether or not the cluster type is public + (default False) + :param tls_disabled: denotes whether or not TLS should be enabled + (default False) + :param http_proxy: host:port for a proxy to use when direct HTTP + access from the servers to sites on the external + internet is blocked (optional) + :param https_proxy: host:port for a proxy to use when direct HTTPS + access from the servers to sites on the external + internet is blocked (optional) + :param no_proxy: comma separated list of IPs that should not be + redirected through the proxy (optional) + :param volume_driver: The name of a volume driver for managing the + persistent storage for the containers. The + functionality supported are specific to the + driver (optional) + :param master_lb_enabled: Since multiple masters may exist in a + bay/cluster, a Neutron load balancer is + created to provide the API endpoint for the + bay/cluster and to direct requests to the + masters. In some cases, such as when the + LBaaS service is not available, this option + can be set to false to create a bay/cluster + without the load balancer. In this case, one + of the masters will serve as the API endpoint + (default - True) + :param labels: Arbitrary labels in the form of a dict. The accepted + keys and valid values are defined in the bay/cluster + drivers. They are used as a way to pass additional + parameters that are specific to a bay/cluster driver. + (optional) + """ + self.name = kwargs.get('name') + self.image = kwargs.get('image') + self.keypair = kwargs.get('keypair') + self.network_driver = kwargs.get('network_driver') + self.external_net = kwargs.get('external_net') + self.floating_ip_enabled = str2bool( + str(kwargs.get('floating_ip_enabled', True))) + self.docker_volume_size = int(kwargs.get('docker_volume_size', 3)) + self.server_type = map_server_type( + kwargs.get('server_type', ServerType.vm)) + self.flavor = kwargs.get('flavor') + self.master_flavor = kwargs.get('master_flavor') + self.coe = map_coe( + kwargs.get('coe', ContainerOrchestrationEngine.kubernetes)) + self.fixed_net = kwargs.get('fixed_net') + self.fixed_subnet = kwargs.get('fixed_subnet') + self.registry_enabled = str2bool( + str(kwargs.get('registry_enabled', True))) + self.insecure_registry = kwargs.get('insecure_registry') + self.docker_storage_driver = map_docker_storage_driver( + kwargs.get('docker_storage_driver', + DockerStorageDriver.devicemapper)) + self.dns_nameserver = kwargs.get('dns_nameserver') + self.public = str2bool(str(kwargs.get('public', False))) + self.tls_disabled = str2bool(str(kwargs.get('tls_disabled', False))) + self.http_proxy = kwargs.get('http_proxy') + self.https_proxy = kwargs.get('https_proxy') + self.no_proxy = kwargs.get('no_proxy') + self.volume_driver = kwargs.get('volume_driver') + self.master_lb_enabled = str2bool( + str(kwargs.get('master_lb_enabled', True))) + self.labels = kwargs.get('labels') + + if (not self.name or not self.image or not self.keypair + or not self.external_net): + raise ClusterTypeConfigError( + 'The attributes name, image, keypair, and ' + 'external_net are required for ClusterTypeConfig') + + def magnum_dict(self): + """ + Returns a dictionary object representing this object. + This is meant to be sent into as kwargs into the Magnum client + + :return: the dictionary object + """ + out = dict() + + if self.name: + out['name'] = self.name + if self.image: + out['image_id'] = self.image + if self.keypair: + out['keypair_id'] = self.keypair + if self.network_driver: + out['network_driver'] = self.network_driver + if self.external_net: + out['external_network_id'] = self.external_net + if self.floating_ip_enabled: + out['floating_ip_enabled'] = self.floating_ip_enabled + if self.docker_volume_size: + out['docker_volume_size'] = self.docker_volume_size + if self.server_type: + out['server_type'] = self.server_type.value + if self.flavor: + out['flavor_id'] = self.flavor + if self.master_flavor: + out['master_flavor_id'] = self.master_flavor + if self.coe: + out['coe'] = self.coe.value + if self.fixed_net: + out['fixed_network'] = self.fixed_net + if self.fixed_subnet: + out['fixed_subnet'] = self.fixed_subnet + if self.registry_enabled: + out['registry_enabled'] = self.registry_enabled + if self.insecure_registry: + out['insecure_registry'] = self.insecure_registry + if self.docker_storage_driver: + out['docker_storage_driver'] = self.docker_storage_driver.value + if self.dns_nameserver: + out['dns_nameserver'] = self.dns_nameserver + if self.public: + out['public'] = self.public + if self.tls_disabled: + out['tls_disabled'] = self.tls_disabled + if self.http_proxy: + out['http_proxy'] = self.http_proxy + if self.https_proxy: + out['https_proxy'] = self.https_proxy + if self.no_proxy: + out['no_proxy'] = self.no_proxy + if self.volume_driver: + out['volume_driver'] = self.volume_driver + if self.master_lb_enabled: + out['master_lb_enabled'] = self.master_lb_enabled + if self.labels: + out['labels'] = self.labels + return out + + +class ClusterTypeConfigError(Exception): + """ + Exception to be thrown when a cluster type configuration is incorrect + """ + + +def map_server_type(server_type): + """ + Takes a the server_type value maps it to the ServerType enum. When None + return None + :param server_type: the server_type value to map + :return: the ServerType enum object + :raise: ClusterTypeConfigError if value is invalid + """ + if not server_type: + return None + if isinstance(server_type, ServerType): + return server_type + elif isinstance(server_type, str): + for this_type in ServerType: + if this_type.value == server_type: + return this_type + raise ClusterTypeConfigError('Invalid server type - ' + server_type) + + +def map_coe(coe): + """ + Takes a the coe value maps it to the ContainerOrchestrationEngine enum. + When None return None + :param coe: the COE value to map + :return: the ContainerOrchestrationEngine enum object + :raise: ClusterTypeConfigError if value is invalid + """ + if not coe: + return None + if isinstance(coe, ContainerOrchestrationEngine): + return coe + elif isinstance(coe, str): + for this_type in ContainerOrchestrationEngine: + if this_type.value == coe: + return this_type + raise ClusterTypeConfigError('Invalid COE - ' + coe) + + +def map_docker_storage_driver(driver): + """ + Takes a the coe value maps it to the ContainerOrchestrationEngine enum. + When None return None + :param driver: the docker storage driver value to map + :return: the DockerStorageDriver enum object + :raise: ClusterTypeConfigError if value is invalid + """ + if not driver: + return None + if isinstance(driver, DockerStorageDriver): + return driver + elif isinstance(driver, str): + for this_type in DockerStorageDriver: + if this_type.value == driver: + return this_type + raise ClusterTypeConfigError('Invalid DockerStorageDriver - ' + driver) diff --git a/snaps/config/network.py b/snaps/config/network.py index bc6ae1b..f48cd27 100644 --- a/snaps/config/network.py +++ b/snaps/config/network.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import enum +from neutronclient.common.utils import str2bool from snaps.openstack.utils import keystone_utils, neutron_utils @@ -52,19 +53,19 @@ class NetworkConfig(object): self.name = kwargs.get('name') if kwargs.get('admin_state_up') is not None: - self.admin_state_up = bool(kwargs['admin_state_up']) + self.admin_state_up = str2bool(str(kwargs['admin_state_up'])) else: self.admin_state_up = True if kwargs.get('shared') is not None: - self.shared = bool(kwargs['shared']) + self.shared = str2bool(str(kwargs['shared'])) else: self.shared = None self.project_name = kwargs.get('project_name') if kwargs.get('external') is not None: - self.external = bool(kwargs.get('external')) + self.external = str2bool(str(kwargs.get('external'))) else: self.external = False @@ -370,7 +371,7 @@ class PortConfig(object): self.network_name = kwargs.get('network_name') if kwargs.get('admin_state_up') is not None: - self.admin_state_up = bool(kwargs['admin_state_up']) + self.admin_state_up = str2bool(str(kwargs['admin_state_up'])) else: self.admin_state_up = True diff --git a/snaps/config/tests/cluster_template_tests.py b/snaps/config/tests/cluster_template_tests.py new file mode 100644 index 0000000..5c695b9 --- /dev/null +++ b/snaps/config/tests/cluster_template_tests.py @@ -0,0 +1,180 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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 unittest + +from snaps.config.cluster_template import ClusterTemplateConfig, \ + ClusterTypeConfigError, ServerType, DockerStorageDriver, \ + ContainerOrchestrationEngine + + +class ClusterTemplateConfigUnitTests(unittest.TestCase): + """ + Tests the construction of the ClusterTemplateConfig class + """ + + def test_no_params(self): + with self.assertRaises(ClusterTypeConfigError): + ClusterTemplateConfig() + + def test_empty_config(self): + with self.assertRaises(ClusterTypeConfigError): + ClusterTemplateConfig(config=dict()) + + def test_name_only(self): + with self.assertRaises(ClusterTypeConfigError): + ClusterTemplateConfig(name='foo') + + def test_minimal_named(self): + config = ClusterTemplateConfig( + name='foo', image='bar', keypair='keys', external_net='external') + self.assertIsNotNone(config) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertIsNone(config.network_driver) + self.assertEqual('external', config.external_net) + self.assertTrue(config.floating_ip_enabled) + self.assertEqual(3, config.docker_volume_size) + self.assertEqual(ServerType.vm, config.server_type) + self.assertIsNone(config.flavor) + self.assertIsNone(config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe) + self.assertIsNone(config.fixed_net) + self.assertIsNone(config.fixed_subnet) + self.assertTrue(config.registry_enabled) + self.assertIsNone(config.insecure_registry) + self.assertEqual(DockerStorageDriver.devicemapper, + config.docker_storage_driver) + self.assertIsNone(config.dns_nameserver) + self.assertFalse(config.public) + self.assertFalse(config.tls_disabled) + self.assertIsNone(config.http_proxy) + self.assertIsNone(config.https_proxy) + self.assertIsNone(config.no_proxy) + self.assertIsNone(config.volume_driver) + self.assertTrue(config.master_lb_enabled) + self.assertIsNone(config.labels) + + def test_minimal_config(self): + config = ClusterTemplateConfig( + **{'name': 'foo', 'image': 'bar', 'keypair': 'keys', + 'external_net': 'external'}) + self.assertIsNotNone(config) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertIsNone(config.network_driver) + self.assertEqual('external', config.external_net) + self.assertTrue(config.floating_ip_enabled) + self.assertEqual(3, config.docker_volume_size) + self.assertEqual(ServerType.vm, config.server_type) + self.assertIsNone(config.flavor) + self.assertIsNone(config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe) + self.assertIsNone(config.fixed_net) + self.assertIsNone(config.fixed_subnet) + self.assertTrue(config.registry_enabled) + self.assertIsNone(config.insecure_registry) + self.assertEqual(DockerStorageDriver.devicemapper, + config.docker_storage_driver) + self.assertIsNone(config.dns_nameserver) + self.assertFalse(config.public) + self.assertFalse(config.tls_disabled) + self.assertIsNone(config.http_proxy) + self.assertIsNone(config.https_proxy) + self.assertIsNone(config.no_proxy) + self.assertIsNone(config.volume_driver) + self.assertTrue(config.master_lb_enabled) + self.assertIsNone(config.labels) + + def test_all_named(self): + labels = {'foo': 'bar'} + config = ClusterTemplateConfig( + name='foo', image='bar', keypair='keys', network_driver='driver', + external_net='external', docker_volume_size=99, + server_type=ServerType.baremetal, flavor='testFlavor', + master_flavor='masterFlavor', + coe=ContainerOrchestrationEngine.kubernetes, fixed_net='fixedNet', + fixed_subnet='fixedSubnet', registry_enabled=False, + docker_storage_driver=DockerStorageDriver.overlay, + dns_nameserver='8.8.4.4', public=True, tls=False, + http_proxy='http://foo:8080', https_proxy='https://foo:443', + no_proxy='foo,bar', volume_driver='volDriver', + master_lb_enabled=False, labels=labels) + self.assertIsNotNone(config) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertEqual('driver', config.network_driver) + self.assertEqual('external', config.external_net) + self.assertEqual(99, config.docker_volume_size) + self.assertEqual(ServerType.baremetal, config.server_type) + self.assertEqual('testFlavor', config.flavor) + self.assertEqual('masterFlavor', config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe) + self.assertEqual('fixedNet', config.fixed_net) + self.assertEqual('fixedSubnet', config.fixed_subnet) + self.assertFalse(config.registry_enabled) + self.assertEqual(DockerStorageDriver.overlay, + config.docker_storage_driver) + self.assertEqual('8.8.4.4', config.dns_nameserver) + self.assertTrue(config.public) + self.assertFalse(config.tls_disabled) + self.assertEqual('http://foo:8080', config.http_proxy) + self.assertEqual('https://foo:443', config.https_proxy) + self.assertEqual('foo,bar', config.no_proxy) + self.assertEqual('volDriver', config.volume_driver) + self.assertFalse(config.master_lb_enabled) + self.assertEqual(labels, config.labels) + + def test_all_config(self): + labels = {'foo': 'bar'} + config = ClusterTemplateConfig(**{ + 'name': 'foo', 'image': 'bar', 'keypair': 'keys', + 'network_driver': 'driver', 'external_net': 'external', + 'docker_volume_size': '99', 'server_type': 'baremetal', + 'flavor': 'testFlavor', 'master_flavor': 'masterFlavor', + 'coe': 'kubernetes', 'fixed_net': 'fixedNet', + 'fixed_subnet': 'fixedSubnet', 'registry_enabled': 'false', + 'docker_storage_driver': 'overlay', 'dns_nameserver': '8.8.4.4', + 'public': 'true', 'tls': 'false', 'http_proxy': 'http://foo:8080', + 'https_proxy': 'https://foo:443', 'no_proxy': 'foo,bar', + 'volume_driver': 'volDriver', 'master_lb_enabled': 'false', + 'labels': labels}) + self.assertIsNotNone(config) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertEqual('driver', config.network_driver) + self.assertEqual('external', config.external_net) + self.assertEqual(99, config.docker_volume_size) + self.assertEqual(ServerType.baremetal, config.server_type) + self.assertEqual('testFlavor', config.flavor) + self.assertEqual('masterFlavor', config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe) + self.assertEqual('fixedNet', config.fixed_net) + self.assertEqual('fixedSubnet', config.fixed_subnet) + self.assertFalse(config.registry_enabled) + self.assertEqual(DockerStorageDriver.overlay, + config.docker_storage_driver) + self.assertEqual('8.8.4.4', config.dns_nameserver) + self.assertTrue(config.public) + self.assertFalse(config.tls_disabled) + self.assertEqual('http://foo:8080', config.http_proxy) + self.assertEqual('https://foo:443', config.https_proxy) + self.assertEqual('foo,bar', config.no_proxy) + self.assertEqual('volDriver', config.volume_driver) + self.assertFalse(config.master_lb_enabled) + self.assertEqual(labels, config.labels) diff --git a/snaps/config/volume.py b/snaps/config/volume.py index 20ca985..a31e8f5 100644 --- a/snaps/config/volume.py +++ b/snaps/config/volume.py @@ -12,6 +12,7 @@ # 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 neutronclient.common.utils import str2bool class VolumeConfig(object): @@ -37,8 +38,8 @@ class VolumeConfig(object): self.type_name = kwargs.get('type_name') self.availability_zone = kwargs.get('availability_zone') - if kwargs.get('availability_zone'): - self.multi_attach = bool(kwargs.get('availability_zone')) + if kwargs.get('multi_attach'): + self.multi_attach = str2bool(str(kwargs.get('multi_attach'))) else: self.multi_attach = False diff --git a/snaps/domain/cluster_template.py b/snaps/domain/cluster_template.py new file mode 100644 index 0000000..01af88a --- /dev/null +++ b/snaps/domain/cluster_template.py @@ -0,0 +1,133 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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. + + +class ClusterTemplate(object): + """ + Class for OpenStack cluster template domain object + """ + + def __init__(self, **kwargs): + """ + Constructor + :param id: the cluster template's UUID + :param name: the cluster template's name + :param image: name or ID of the base image in Glance used to boot the + cluster's servers. The image must have the attribute + 'os-distro' defined as appropriate for the cluster + driver + :param keypair: name or ID of the keypair to gain cluster machine + access + :param network_driver: The name of a network driver for providing the + networks for the containers. Note that this is + different and separate from the Neutron network + for the bay/cluster. The operation and + networking model are specific to the particular + driver + :param external_net: name or IDof the external Neutron network to + provide connectivity to the cluster + :param floating_ip_enabled: Whether enable or not using the floating IP + of cloud provider. Some cloud providers + used floating IP, some used public IP, + thus Magnum provide this option for + specifying the choice of using floating IP + :param docker_volume_size: The size in GB for the local storage on each + server for the Docker daemon to cache the + images and host the containers. Cinder + volumes provide the storage. The default is + 25 GB. For the devicemapper storage driver, + the minimum value is 3GB. For the overlay + storage driver, the minimum value is 1GB. + :param server_type: server type string + :param flavor: name or ID of the nova flavor for booting the node + servers + :param master_flavor: name or ID of the nova flavor of the master node + for this cluster + :param coe: ContainerOrchestrationEngine enum instance + :param fixed_net: name of a Neutron network to provide connectivity + to the internal network for the cluster + :param fixed_subnet: Fixed subnet that are using to allocate network + address for nodes in bay/cluster + :param registry_enabled: Docker images by default are pulled from the + public Docker registry, but in some cases, + users may want to use a private registry. + This option provides an alternative registry + based on the Registry V2: Magnum will create a + local registry in the bay/cluster backed by + swift to host the images + :param insecure_registry: The URL pointing to the user's own private + insecure docker registry to deploy and run + docker containers + :param docker_storage_driver: DockerStorageDriver enum instance to + manage storage for the images and + container's writable layer + :param dns_nameserver: The DNS nameserver for the servers and + containers in the bay/cluster to use. + This is configured in the private Neutron + network for the bay/cluster. + :param public: denotes whether or not the cluster type is public + :param tls_disabled: denotes whether or not TLS should be enabled + :param http_proxy: host:port for a proxy to use when direct HTTP + access from the servers to sites on the external + internet is blocked + :param https_proxy: host:port for a proxy to use when direct HTTPS + access from the servers to sites on the external + internet is blocked + :param no_proxy: comma separated list of IPs that should not be + redirected through the proxy + :param volume_driver: The name of a volume driver for managing the + persistent storage for the containers. The + functionality supported are specific to the + driver + :param master_lb_enabled: Since multiple masters may exist in a + bay/cluster, a Neutron load balancer is + created to provide the API endpoint for the + bay/cluster and to direct requests to the + masters. In some cases, such as when the + LBaaS service is not available, this option + can be set to false to create a bay/cluster + without the load balancer. In this case, one + of the masters will serve as the API endpoint + :param labels: Arbitrary labels in the form of a dict. The accepted + keys and valid values are defined in the bay/cluster + drivers. They are used as a way to pass additional + parameters that are specific to a bay/cluster driver. + """ + self.id = kwargs.get('id') + self.name = kwargs.get('name') + self.image = kwargs.get('image') + self.keypair = kwargs.get('keypair') + self.network_driver = kwargs.get('network_driver') + self.external_net = kwargs.get('external_net') + self.floating_ip_enabled = kwargs.get('floating_ip_enabled') + self.docker_volume_size = int(kwargs.get('docker_volume_size', 3)) + self.server_type = kwargs.get('server_type') + self.flavor = kwargs.get('flavor') + self.master_flavor = kwargs.get('master_flavor') + self.coe = kwargs.get('coe') + self.fixed_net = kwargs.get('fixed_net') + self.fixed_subnet = kwargs.get('fixed_subnet') + self.registry_enabled = kwargs.get('registry_enabled') + self.insecure_registry = kwargs.get('insecure_registry') + self.docker_storage_driver = kwargs.get('docker_storage_driver') + self.dns_nameserver = kwargs.get('dns_nameserver') + self.public = kwargs.get('public', False) + self.tls_disabled = kwargs.get('tls_disabled') + self.http_proxy = kwargs.get('http_proxy') + self.https_proxy = kwargs.get('https_proxy') + self.no_proxy = kwargs.get('no_proxy') + self.volume_driver = kwargs.get('volume_driver') + self.master_lb_enabled = kwargs.get('master_lb_enabled', True) + self.labels = kwargs.get('labels') diff --git a/snaps/domain/test/cluster_template_tests.py b/snaps/domain/test/cluster_template_tests.py new file mode 100644 index 0000000..76e5663 --- /dev/null +++ b/snaps/domain/test/cluster_template_tests.py @@ -0,0 +1,109 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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 unittest + +from snaps.config.cluster_template import ( + ContainerOrchestrationEngine, ServerType, DockerStorageDriver) +from snaps.domain.cluster_template import ClusterTemplate + + +class ClusterTemplateUnitTests(unittest.TestCase): + """ + Tests the construction of the ClusterTypeConfig class + """ + def test_all_named(self): + labels = {'foo': 'bar'} + config = ClusterTemplate( + id='tmplt-id', name='foo', image='bar', keypair='keys', + network_driver='driver', external_net='external', + docker_volume_size=99, server_type=ServerType.baremetal.value, + flavor='testFlavor', master_flavor='masterFlavor', + coe=ContainerOrchestrationEngine.kubernetes.value, + fixed_net='fixedNet', fixed_subnet='fixedSubnet', + registry_enabled=False, + docker_storage_driver=DockerStorageDriver.overlay.value, + dns_nameserver='8.8.4.4', public=True, tls=False, + http_proxy='http://foo:8080', https_proxy='https://foo:443', + no_proxy='foo,bar', volume_driver='volDriver', + master_lb_enabled=False, labels=labels) + self.assertIsNotNone(config) + self.assertEqual('tmplt-id', config.id) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertEqual('driver', config.network_driver) + self.assertEqual('external', config.external_net) + self.assertEqual(99, config.docker_volume_size) + self.assertEqual(ServerType.baremetal.value, config.server_type) + self.assertEqual('testFlavor', config.flavor) + self.assertEqual('masterFlavor', config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes.value, + config.coe) + self.assertEqual('fixedNet', config.fixed_net) + self.assertEqual('fixedSubnet', config.fixed_subnet) + self.assertFalse(config.registry_enabled) + self.assertEqual(DockerStorageDriver.overlay.value, + config.docker_storage_driver) + self.assertEqual('8.8.4.4', config.dns_nameserver) + self.assertTrue(config.public) + self.assertFalse(config.tls_disabled) + self.assertEqual('http://foo:8080', config.http_proxy) + self.assertEqual('https://foo:443', config.https_proxy) + self.assertEqual('foo,bar', config.no_proxy) + self.assertEqual('volDriver', config.volume_driver) + self.assertFalse(config.master_lb_enabled) + self.assertEqual(labels, config.labels) + + def test_all_config(self): + labels = {'foo': 'bar'} + config = ClusterTemplate(**{ + 'id': 'tmplt-id', 'name': 'foo', 'image': 'bar', 'keypair': 'keys', + 'network_driver': 'driver', 'external_net': 'external', + 'docker_volume_size': '99', 'server_type': 'baremetal', + 'flavor': 'testFlavor', 'master_flavor': 'masterFlavor', + 'coe': 'kubernetes', 'fixed_net': 'fixedNet', + 'fixed_subnet': 'fixedSubnet', 'registry_enabled': False, + 'docker_storage_driver': 'overlay', 'dns_nameserver': '8.8.4.4', + 'public': 'true', 'tls': 'false', 'http_proxy': 'http://foo:8080', + 'https_proxy': 'https://foo:443', 'no_proxy': 'foo,bar', + 'volume_driver': 'volDriver', 'master_lb_enabled': False, + 'labels': labels}) + self.assertIsNotNone(config) + self.assertEqual('tmplt-id', config.id) + self.assertEqual('foo', config.name) + self.assertEqual('bar', config.image) + self.assertEqual('keys', config.keypair) + self.assertEqual('driver', config.network_driver) + self.assertEqual('external', config.external_net) + self.assertEqual(99, config.docker_volume_size) + self.assertEqual(ServerType.baremetal.value, config.server_type) + self.assertEqual('testFlavor', config.flavor) + self.assertEqual('masterFlavor', config.master_flavor) + self.assertEqual(ContainerOrchestrationEngine.kubernetes.value, + config.coe) + self.assertEqual('fixedNet', config.fixed_net) + self.assertEqual('fixedSubnet', config.fixed_subnet) + self.assertFalse(config.registry_enabled) + self.assertEqual(DockerStorageDriver.overlay.value, + config.docker_storage_driver) + self.assertEqual('8.8.4.4', config.dns_nameserver) + self.assertTrue(config.public) + self.assertFalse(config.tls_disabled) + self.assertEqual('http://foo:8080', config.http_proxy) + self.assertEqual('https://foo:443', config.https_proxy) + self.assertEqual('foo,bar', config.no_proxy) + self.assertEqual('volDriver', config.volume_driver) + self.assertFalse(config.master_lb_enabled) + self.assertEqual(labels, config.labels) diff --git a/snaps/openstack/tests/openstack_tests.py b/snaps/openstack/tests/openstack_tests.py index 78e8601..4b00922 100644 --- a/snaps/openstack/tests/openstack_tests.py +++ b/snaps/openstack/tests/openstack_tests.py @@ -174,17 +174,15 @@ def create_image_settings(image_name, image_user, image_format, metadata, return ImageConfig(**metadata['config']) disk_file = None - if metadata: + if metadata and ('disk_url' in metadata or 'disk_file' in metadata): disk_url = metadata.get('disk_url') disk_file = metadata.get('disk_file') elif not disk_url: disk_url = default_url - else: - disk_url = disk_url - if metadata and \ - ('kernel_file' in metadata or 'kernel_url' in metadata) and \ - kernel_settings is None: + if (metadata + and ('kernel_file' in metadata or 'kernel_url' in metadata) + and kernel_settings is None): kernel_image_settings = ImageConfig( name=image_name + '-kernel', image_user=image_user, img_format=image_format, image_file=metadata.get('kernel_file'), @@ -192,9 +190,9 @@ def create_image_settings(image_name, image_user, image_format, metadata, else: kernel_image_settings = kernel_settings - if metadata and \ - ('ramdisk_file' in metadata or 'ramdisk_url' in metadata) and \ - ramdisk_settings is None: + if (metadata + and ('ramdisk_file' in metadata or 'ramdisk_url' in metadata) + and ramdisk_settings is None): ramdisk_image_settings = ImageConfig( name=image_name + '-ramdisk', image_user=image_user, img_format=image_format, diff --git a/snaps/openstack/utils/magnum_utils.py b/snaps/openstack/utils/magnum_utils.py index abc771a..c744666 100644 --- a/snaps/openstack/utils/magnum_utils.py +++ b/snaps/openstack/utils/magnum_utils.py @@ -16,18 +16,81 @@ import logging from magnumclient.client import Client +from snaps.domain.cluster_template import ClusterTemplate from snaps.openstack.utils import keystone_utils __author__ = 'spisarski' -logger = logging.getLogger('heat_utils') +logger = logging.getLogger('magnum_utils') def magnum_client(os_creds): """ - Retrieves the Heat client - :param os_creds: the OpenStack credentials + Retrieves the Magnum client + :param os_creds: the OpenStack credentialsf :return: the client """ - logger.debug('Retrieving Nova Client') - return Client(session=keystone_utils.keystone_session(os_creds)) + logger.debug('Retrieving Magnum Client') + return Client(str(os_creds.magnum_api_version), + session=keystone_utils.keystone_session(os_creds)) + + +def create_cluster_template(magnum, cluster_template_config): + """ + Creates a Magnum Cluster Template object in OpenStack + :param magnum: the Magnum client + :param cluster_template_config: a ClusterTemplateConfig object + :return: a SNAPS ClusterTemplate domain object + """ + config_dict = cluster_template_config.magnum_dict() + os_cluster_template = magnum.cluster_templates.create(**config_dict) + logger.info('Creating cluster template named [%s]', + cluster_template_config.name) + return __map_os_cluster_template(os_cluster_template) + + +def delete_cluster_template(magnum, tmplt_id): + """ + Deletes a Cluster Template from OpenStack + :param magnum: the Magnum client + :param tmplt_id: the cluster template ID to delete + """ + logger.info('Deleting cluster template with ID [%s]', tmplt_id) + magnum.cluster_templates.delete(tmplt_id) + + +def __map_os_cluster_template(os_tmplt): + """ + Returns a SNAPS ClusterTemplate object from an OpenStack ClusterTemplate + object + :param os_tmplt: the OpenStack ClusterTemplate object + :return: SNAPS ClusterTemplate object + """ + return ClusterTemplate( + id=os_tmplt.uuid, + name=os_tmplt.name, + image=os_tmplt.image_id, + keypair=os_tmplt.keypair_id, + network_driver=os_tmplt.network_driver, + external_net=os_tmplt.external_network_id, + floating_ip_enabled=os_tmplt.floating_ip_enabled, + docker_volume_size=os_tmplt.docker_volume_size, + server_type=os_tmplt.server_type, + flavor=os_tmplt.flavor_id, + master_flavor=os_tmplt.master_flavor_id, + coe=os_tmplt.coe, + fixed_net=os_tmplt.fixed_network, + fixed_subnet=os_tmplt.fixed_subnet, + registry_enabled=os_tmplt.registry_enabled, + insecure_registry=os_tmplt.insecure_registry, + docker_storage_driver=os_tmplt.docker_storage_driver, + dns_nameserver=os_tmplt.dns_nameserver, + public=os_tmplt.public, + tls_disabled=os_tmplt.tls_disabled, + http_proxy=os_tmplt.http_proxy, + https_proxy=os_tmplt.https_proxy, + no_proxy=os_tmplt.no_proxy, + volume_driver=os_tmplt.volume_driver, + master_lb_enabled=os_tmplt.master_lb_enabled, + labels=os_tmplt.labels + ) diff --git a/snaps/openstack/utils/tests/magnum_utils_tests.py b/snaps/openstack/utils/tests/magnum_utils_tests.py index 48ead80..9e47900 100644 --- a/snaps/openstack/utils/tests/magnum_utils_tests.py +++ b/snaps/openstack/utils/tests/magnum_utils_tests.py @@ -13,8 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +import uuid +from snaps.config.cluster_template import ClusterTemplateConfig +from snaps.config.keypair import KeypairConfig +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_keypairs import OpenStackKeypair from snaps.openstack.os_credentials import OSCreds +from snaps.openstack.tests import openstack_tests from snaps.openstack.tests.os_source_file_test import OSComponentTestCase from snaps.openstack.utils import magnum_utils @@ -35,7 +41,7 @@ class MagnumSmokeTests(OSComponentTestCase): magnum = magnum_utils.magnum_client(self.os_creds) # This should not throw an exception - magnum.clusters.list() + self.assertIsNotNone(magnum.clusters.list()) def test_nova_connect_fail(self): """ @@ -48,3 +54,121 @@ class MagnumSmokeTests(OSComponentTestCase): auth_url=self.os_creds.auth_url, project_name=self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings)) + + +class MagnumUtilsTests(OSComponentTestCase): + """ + Tests individual functions within magnum_utils.py + """ + + def setUp(self): + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.cluster_type_name = self.guid + '-cluster-type' + self.magnum = magnum_utils.magnum_client(self.os_creds) + + metadata = self.image_metadata + if not metadata: + metadata = dict() + if 'extra_properties' not in metadata: + metadata['extra_properties'] = dict() + metadata['extra_properties']['os_distro'] = 'cirros' + + os_image_settings = openstack_tests.cirros_image_settings( + name=self.guid + '-image', image_metadata=metadata) + + self.image_creator = OpenStackImage(self.os_creds, os_image_settings) + + keypair_priv_filepath = 'tmp/' + self.guid + keypair_pub_filepath = keypair_priv_filepath + '.pub' + + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairConfig( + name=self.guid + '-keypair', + public_filepath=keypair_pub_filepath, + private_filepath=keypair_priv_filepath)) + + self.cluster_template = None + + try: + self.image_creator.create() + self.keypair_creator.create() + except: + self.tearDown() + raise + + def tearDown(self): + if self.cluster_template: + try: + magnum_utils.delete_cluster_template( + self.magnum, self.cluster_template.id) + except: + pass + if self.keypair_creator: + try: + self.keypair_creator.clean() + except: + pass + if self.image_creator: + try: + self.image_creator.clean() + except: + pass + + def test_create_cluster_template_simple(self): + config = ClusterTemplateConfig( + name=self.cluster_type_name, + image=self.image_creator.image_settings.name, + keypair=self.keypair_creator.keypair_settings.name, + external_net=self.ext_net_name) + + self.cluster_template = magnum_utils.create_cluster_template( + self.magnum, config) + self.assertIsNotNone(self.cluster_template) + self.assertTrue( + validate_cluster_template(config, self.cluster_template)) + + +def validate_cluster_template(tmplt_config, tmplt_obj): + """ + Returns true if the configuration matches the ClusterTemplate object + :param tmplt_config: the ClusterTemplateConfig object + :param tmplt_obj: the ClusterTemplate domain object + :return: T/F + """ + if not tmplt_config.network_driver: + network_driver = 'flannel' + else: + network_driver = tmplt_config.network_driver + + return ( + tmplt_config.coe.value == tmplt_obj.coe and + tmplt_config.dns_nameserver == tmplt_obj.dns_nameserver and + tmplt_config.docker_storage_driver.value + == tmplt_obj.docker_storage_driver and + tmplt_config.docker_volume_size == tmplt_obj.docker_volume_size and + tmplt_config.external_net == tmplt_obj.external_net and + tmplt_config.fixed_net == tmplt_obj.fixed_net and + tmplt_config.fixed_subnet == tmplt_obj.fixed_subnet and + tmplt_config.flavor == tmplt_obj.flavor and + tmplt_config.floating_ip_enabled == tmplt_obj.floating_ip_enabled and + tmplt_config.http_proxy == tmplt_obj.http_proxy and + tmplt_config.https_proxy == tmplt_obj.https_proxy and + tmplt_config.no_proxy == tmplt_obj.no_proxy and + tmplt_config.image == tmplt_obj.image and + tmplt_config.insecure_registry == tmplt_obj.insecure_registry and + tmplt_config.keypair == tmplt_obj.keypair and + tmplt_config.labels == tmplt_obj.labels and + tmplt_config.master_flavor == tmplt_obj.master_flavor and + tmplt_config.master_lb_enabled == tmplt_obj.master_lb_enabled and + tmplt_config.name == tmplt_obj.name and + network_driver == tmplt_obj.network_driver and + tmplt_config.no_proxy == tmplt_obj.no_proxy and + tmplt_config.public == tmplt_obj.public and + tmplt_config.registry_enabled == tmplt_obj.registry_enabled and + tmplt_config.server_type.value == tmplt_obj.server_type and + tmplt_config.tls_disabled == tmplt_obj.tls_disabled and + tmplt_config.volume_driver == tmplt_obj.volume_driver + ) + # def test_create_cluster_simple(self): + # cluster = magnum_utils.create_cluster(self.magnum, 'foo') + # self.assertIsNotNone(cluster) diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py index dba60f7..496caf7 100644 --- a/snaps/test_suite_builder.py +++ b/snaps/test_suite_builder.py @@ -16,6 +16,8 @@ import logging import unittest +from snaps.config.tests.cluster_template_tests import ( + ClusterTemplateConfigUnitTests) from snaps.config.tests.network_tests import ( NetworkConfigUnitTests, SubnetConfigUnitTests, PortConfigUnitTests) from snaps.config.tests.security_group_tests import ( @@ -33,6 +35,7 @@ from snaps.config.tests.keypair_tests import KeypairConfigUnitTests from snaps.config.tests.flavor_tests import FlavorConfigUnitTests import snaps.config.tests.image_tests as image_tests import snaps.openstack.tests.create_image_tests as creator_tests +from snaps.domain.test.cluster_template_tests import ClusterTemplateUnitTests from snaps.domain.test.flavor_tests import FlavorDomainObjectTests from snaps.domain.test.image_tests import ImageDomainObjectTests from snaps.domain.test.keypair_tests import KeypairDomainObjectTests @@ -121,7 +124,7 @@ from snaps.openstack.utils.tests.nova_utils_tests import ( from snaps.openstack.utils.tests.settings_utils_tests import ( SettingsUtilsUnitTests) from snaps.openstack.utils.tests.magnum_utils_tests import ( - MagnumSmokeTests) + MagnumSmokeTests, MagnumUtilsTests) from snaps.provisioning.tests.ansible_utils_tests import ( AnsibleProvisioningTests) from snaps.tests.file_utils_tests import FileUtilsTests @@ -256,6 +259,10 @@ def add_unit_tests(suite): VolumeConfigUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase( VolumeSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase( + ClusterTemplateConfigUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase( + ClusterTemplateUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase( SettingsUtilsUnitTests)) @@ -719,3 +726,6 @@ def add_openstack_staging_tests(suite, os_creds, ext_net_name, suite.addTest(OSComponentTestCase.parameterize( MagnumSmokeTests, os_creds=os_creds, ext_net_name=ext_net_name, log_level=log_level)) + suite.addTest(OSComponentTestCase.parameterize( + MagnumUtilsTests, os_creds=os_creds, + ext_net_name=ext_net_name, log_level=log_level)) -- cgit 1.2.3-korg