Implemented the ability to create Magnum Cluster Type objects. 73/47673/3
authorspisarski <s.pisarski@cablelabs.com>
Wed, 22 Nov 2017 21:55:43 +0000 (14:55 -0700)
committerspisarski <s.pisarski@cablelabs.com>
Wed, 22 Nov 2017 23:24:12 +0000 (16:24 -0700)
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 <s.pisarski@cablelabs.com>
12 files changed:
docs/how-to-use/APITests.rst
docs/how-to-use/UnitTests.rst
snaps/config/cluster_template.py [new file with mode: 0644]
snaps/config/network.py
snaps/config/tests/cluster_template_tests.py [new file with mode: 0644]
snaps/config/volume.py
snaps/domain/cluster_template.py [new file with mode: 0644]
snaps/domain/test/cluster_template_tests.py [new file with mode: 0644]
snaps/openstack/tests/openstack_tests.py
snaps/openstack/utils/magnum_utils.py
snaps/openstack/utils/tests/magnum_utils_tests.py
snaps/test_suite_builder.py

index ee0d894..6a7c317 100644 (file)
@@ -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
 ------------------------------------------------------
 
index cb0c5f3..5bd4f08 100644 (file)
@@ -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 (file)
index 0000000..a20225a
--- /dev/null
@@ -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)
index bc6ae1b..f48cd27 100644 (file)
@@ -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 (file)
index 0000000..5c695b9
--- /dev/null
@@ -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)
index 20ca985..a31e8f5 100644 (file)
@@ -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 (file)
index 0000000..01af88a
--- /dev/null
@@ -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 (file)
index 0000000..76e5663
--- /dev/null
@@ -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)
index 78e8601..4b00922 100644 (file)
@@ -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,
index abc771a..c744666 100644 (file)
@@ -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
+    )
index 48ead80..9e47900 100644 (file)
 # 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)
index dba60f7..496caf7 100644 (file)
@@ -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))