Added method to OpenStackHeatStack to return OpenStackKeypair objects. 39/46439/1
authorspisarski <s.pisarski@cablelabs.com>
Mon, 30 Oct 2017 18:08:58 +0000 (12:08 -0600)
committerspisarski <s.pisarski@cablelabs.com>
Mon, 30 Oct 2017 18:08:58 +0000 (12:08 -0600)
Continuation of the story SNAPS-153 for adding creator/state machine
instances for OpenStack objects deployed via Heat.

JIRA: SNAPS-175

Change-Id: I7196279086b1935b4ec4a01483d46921cc567b15
Signed-off-by: spisarski <s.pisarski@cablelabs.com>
docs/how-to-use/APITests.rst
docs/how-to-use/IntegrationTests.rst
snaps/openstack/create_stack.py
snaps/openstack/tests/create_stack_tests.py
snaps/openstack/tests/heat/keypair_heat_template.yaml [new file with mode: 0644]
snaps/openstack/utils/heat_utils.py
snaps/openstack/utils/nova_utils.py
snaps/openstack/utils/settings_utils.py
snaps/openstack/utils/tests/heat_utils_tests.py
snaps/test_suite_builder.py

index fbd7e67..9110162 100644 (file)
@@ -454,7 +454,7 @@ heat_utils_tests.py - HeatUtilsVolumeTests
 | Test Name                             | Heat API      | Description                                               |
 +=======================================+===============+===========================================================+
 | test_create_vol_with_stack            | 1             | Tests ability of the function                             |
-|                                       |               | heat_utils.get_stack_volumes() to return the correct      |
+|                                       |               | heat_utils.create_stack() to return the correct           |
 |                                       |               | Volume domain objects deployed with Heat                  |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | test_create_vol_types_with_stack      | 1             | Tests ability of the function                             |
@@ -462,6 +462,17 @@ heat_utils_tests.py - HeatUtilsVolumeTests
 |                                       |               | VolumeType domain objects deployed with Heat              |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+heat_utils_tests.py - HeatUtilsKeypairTests
+-------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_keypair_with_stack        | 1             | Tests ability of the function                             |
+|                                       |               | heat_utils.create_stack() to return the correct           |
+|                                       |               | Keypair domain objects deployed with Heat                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 settings_utils_tests.py - SettingsUtilsNetworkingTests
 ------------------------------------------------------
 
index 538c9c0..e00bc40 100644 (file)
@@ -396,6 +396,17 @@ create_stack_tests.py - CreateStackVolumeTests
 |                                       |               | deploying                                                 |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+create_stack_tests.py - CreateStackKeypairTests
+-----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_keypair_creator         | 1             | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackKeypair instance that it was responsible for     |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 create_stack_tests.py - CreateComplexStackTests
 -----------------------------------------------
 
index d8f9d15..b565118 100644 (file)
@@ -19,6 +19,7 @@ import time
 from heatclient.exc import HTTPNotFound
 
 from snaps.openstack.create_instance import OpenStackVmInstance
+from snaps.openstack.create_keypairs import OpenStackKeypair
 from snaps.openstack.create_volume import OpenStackVolume
 from snaps.openstack.create_volume_type import OpenStackVolumeType
 from snaps.openstack.openstack_creator import OpenStackCloudObject
@@ -318,6 +319,34 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
 
         return out
 
+    def get_keypair_creators(self, outputs_pk_key=None):
+        """
+        Returns a list of keypair creator objects as configured by the heat
+        template
+        :return: list() of OpenStackKeypair objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        keypairs = heat_utils.get_stack_keypairs(
+            self.__heat_cli, nova, self.__stack)
+
+        for keypair in keypairs:
+            settings = settings_utils.create_keypair_settings(
+                self.__heat_cli, self.__stack, keypair, outputs_pk_key)
+            creator = OpenStackKeypair(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume type creator - %s',
+                    e)
+
+        return out
+
     def _stack_status_check(self, expected_status_code, block, timeout,
                             poll_interval, fail_status):
         """
index a2b2215..8f9339a 100644 (file)
@@ -495,7 +495,7 @@ class CreateStackFloatingIpTests(OSIntegrationTestCase):
 
 class CreateStackVolumeTests(OSIntegrationTestCase):
     """
-    Tests for the CreateStack class defined in create_stack.py
+    Tests for the CreateStack class as they pertain to volumes
     """
 
     def setUp(self):
@@ -589,6 +589,80 @@ class CreateStackVolumeTests(OSIntegrationTestCase):
         self.assertEqual(volume_type.id, encryption.volume_type_id)
 
 
+class CreateStackKeypairTests(OSIntegrationTestCase):
+    """
+    Tests for the CreateStack class as they pertain to keypairs
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateStack object that is responsible for downloading
+        and creating an OS stack file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.nova = nova_utils.nova_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.keypair_name = self.guid + '-kp'
+
+        self.env_values = {
+            'keypair_name': self.keypair_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'keypair_heat_template.yaml')
+
+        stack_settings = StackSettings(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = create_stack.OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_keypair_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackKeypair creator/state machine instance
+        """
+        kp_creators = self.stack_creator.get_keypair_creators('private_key')
+        self.assertEqual(1, len(kp_creators))
+
+        creator = kp_creators[0]
+
+        self.assertEqual(self.keypair_name, creator.get_keypair().name)
+        self.assertIsNotNone(creator.keypair_settings.private_filepath)
+
+        private_file_contents = file_utils.read_file(
+            creator.keypair_settings.private_filepath)
+        self.assertTrue(private_file_contents.startswith(
+            '-----BEGIN RSA PRIVATE KEY-----'))
+
+        keypair = nova_utils.get_keypair_by_id(
+            self.nova, creator.get_keypair().id)
+        self.assertIsNotNone(keypair)
+        self.assertEqual(creator.get_keypair(), keypair)
+
+
 class CreateStackNegativeTests(OSIntegrationTestCase):
     """
     Negative test cases for the CreateStack class
diff --git a/snaps/openstack/tests/heat/keypair_heat_template.yaml b/snaps/openstack/tests/heat/keypair_heat_template.yaml
new file mode 100644 (file)
index 0000000..ffb8892
--- /dev/null
@@ -0,0 +1,39 @@
+##############################################################################
+# 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.
+##############################################################################
+heat_template_version: 2015-04-30
+
+description: >
+  Test template that simply deploys a keypair with a generated key
+
+parameters:
+  keypair_name:
+    type: string
+    label: Keypair name
+    description: The name of the stack's keypair
+    default: keypair_name
+
+resources:
+  keypair:
+    type: OS::Nova::KeyPair
+    properties:
+      name: { get_param: keypair_name }
+      save_private_key: True
+
+outputs:
+  private_key:
+    description: "SSH Private Key"
+    value: { get_attr: [ keypair, private_key ]}
index f2d4efd..f09857a 100644 (file)
@@ -227,9 +227,31 @@ def get_stack_servers(heat_cli, nova, stack):
     return out
 
 
+def get_stack_keypairs(heat_cli, nova, stack):
+    """
+    Returns a list of Keypair domain objects associated with a Stack
+    :param heat_cli: the OpenStack heat client object
+    :param nova: the OpenStack nova client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of VMInst domain objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack, 'OS::Nova::KeyPair')
+    for resource in resources:
+        try:
+            keypair = nova_utils.get_keypair_by_id(nova, resource.id)
+            if keypair:
+                out.append(keypair)
+        except NotFound:
+            logger.warn('Keypair cannot be located with ID %s', resource.id)
+
+    return out
+
+
 def get_stack_volumes(heat_cli, cinder, stack):
     """
-    Returns an instance of NetworkSettings for each network owned by this stack
+    Returns an instance of Volume domain objects created by this stack
     :param heat_cli: the OpenStack heat client object
     :param cinder: the OpenStack cinder client object
     :param stack: the SNAPS-OO Stack domain object
@@ -251,7 +273,7 @@ def get_stack_volumes(heat_cli, cinder, stack):
 
 def get_stack_volume_types(heat_cli, cinder, stack):
     """
-    Returns an instance of NetworkSettings for each network owned by this stack
+    Returns an instance of VolumeType domain objects created by this stack
     :param heat_cli: the OpenStack heat client object
     :param cinder: the OpenStack cinder client object
     :param stack: the SNAPS-OO Stack domain object
index 42b7356..0820289 100644 (file)
@@ -419,6 +419,18 @@ def get_keypair_by_name(nova, name):
     return None
 
 
+def get_keypair_by_id(nova, kp_id):
+    """
+    Returns a list of all available keypairs
+    :param nova: the Nova client
+    :param kp_id: the ID of the keypair to return
+    :return: the keypair object
+    """
+    keypair = nova.keypairs.get(kp_id)
+    return Keypair(name=keypair.name, kp_id=keypair.id,
+                   public_key=keypair.public_key)
+
+
 def delete_keypair(nova, key):
     """
     Deletes a keypair object from OpenStack
index 7169319..68dbf71 100644 (file)
@@ -109,6 +109,32 @@ def create_volume_type_settings(volume_type):
         qos_spec_name=qos_spec_name, public=volume_type.public)
 
 
+def create_keypair_settings(heat_cli, stack, keypair, pk_output_key):
+    """
+    Instantiates a KeypairSettings object from a Keypair domain objects
+    :param heat_cli: the heat client
+    :param stack: the Stack domain object
+    :param keypair: the Keypair SNAPS domain object
+    :param pk_output_key: the key to the heat template's outputs for retrieval
+                          of the private key file
+    :return: a KeypairSettings object
+    """
+    if pk_output_key:
+        outputs = heat_utils.get_outputs(heat_cli, stack)
+        for output in outputs:
+            if output.key == pk_output_key:
+                # Save to file
+                guid = uuid.uuid4()
+                key_file = file_utils.save_string_to_file(
+                    output.value, str(guid), 0o400)
+
+                # Use outputs, file and resources for the KeypairSettings
+                return KeypairSettings(
+                    name=keypair.name, private_filepath=key_file.name)
+
+    return KeypairSettings(name=keypair.name)
+
+
 def create_vm_inst_settings(nova, neutron, server):
     """
     Returns a NetworkSettings object
index 4f58613..b021701 100644 (file)
@@ -169,22 +169,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
         self.assertIsNotNone(outputs)
         self.assertEqual(0, len(outputs))
 
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack1.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack1))
 
         neutron = neutron_utils.neutron_client(self.os_creds)
         networks = heat_utils.get_stack_networks(
@@ -223,21 +208,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
                                                     self.stack1.id)
         self.assertEqual(self.stack1, stack1_query_3)
 
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack1.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack1))
 
         self.stack2 = heat_utils.create_stack(self.heat_client,
                                               self.stack_settings2)
@@ -319,21 +290,7 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
         self.heat_client = heat_utils.heat_client(self.os_creds)
         self.stack = heat_utils.create_stack(self.heat_client, stack_settings)
 
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
 
     def tearDown(self):
         """
@@ -506,23 +463,7 @@ class HeatUtilsVolumeTests(OSComponentTestCase):
         """
         self.stack = heat_utils.create_stack(
             self.heat_client, self.stack_settings)
-
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
 
         volumes = heat_utils.get_stack_volumes(
             self.heat_client, self.cinder, self.stack)
@@ -541,23 +482,7 @@ class HeatUtilsVolumeTests(OSComponentTestCase):
         """
         self.stack = heat_utils.create_stack(
             self.heat_client, self.stack_settings)
-
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
 
         volume_types = heat_utils.get_stack_volume_types(
             self.heat_client, self.cinder, self.stack)
@@ -578,3 +503,91 @@ class HeatUtilsVolumeTests(OSComponentTestCase):
         self.assertEqual(u'nova.volume.encryptors.luks.LuksEncryptor',
                          encryption.provider)
         self.assertEqual(volume_type.id, encryption.volume_type_id)
+
+
+class HeatUtilsKeypairTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        stack_name = guid + '-stack'
+        self.keypair_name = guid + '-kp'
+
+        env_values = {
+            'keypair_name': self.keypair_name}
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'keypair_heat_template.yaml')
+        self.stack_settings = StackSettings(
+            name=stack_name, template_path=heat_tmplt_path,
+            env_values=env_values)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.nova = nova_utils.nova_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the image and downloaded image file
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_keypair_with_stack(self):
+        """
+        Tests the creation of an OpenStack keypair with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        keypairs = heat_utils.get_stack_keypairs(
+            self.heat_client, self.nova, self.stack)
+
+        self.assertEqual(1, len(keypairs))
+        keypair = keypairs[0]
+
+        self.assertEqual(self.keypair_name, keypair.name)
+
+        outputs = heat_utils.get_outputs(self.heat_client, self.stack)
+
+        for output in outputs:
+            if output.key == 'private_key':
+                self.assertTrue(output.value.startswith(
+                    '-----BEGIN RSA PRIVATE KEY-----'))
+
+        keypair = nova_utils.get_keypair_by_id(self.nova, keypair.id)
+        self.assertIsNotNone(keypair)
+
+        self.assertEqual(self.keypair_name, keypair.name)
+
+
+def stack_active(heat_cli, stack):
+    """
+    Blocks until stack application has successfully completed or failed
+    :param heat_cli: the Heat client
+    :param stack: the Stack domain object
+    :return: T/F
+    """
+    # Wait until stack deployment has completed
+    end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
+    is_active = False
+    while time.time() < end_time:
+        status = heat_utils.get_stack_status(heat_cli, stack.id)
+        if status == create_stack.STATUS_CREATE_COMPLETE:
+            is_active = True
+            break
+        elif status == create_stack.STATUS_CREATE_FAILED:
+            is_active = False
+            break
+
+        time.sleep(3)
+
+    return is_active
index 77a8a2a..1795a52 100644 (file)
@@ -68,7 +68,8 @@ from snaps.openstack.tests.create_security_group_tests import (
     SecurityGroupSettingsUnitTests)
 from snaps.openstack.tests.create_stack_tests import (
     StackSettingsUnitTests, CreateStackSuccessTests, CreateStackNegativeTests,
-    CreateStackFloatingIpTests, CreateStackVolumeTests)
+    CreateStackFloatingIpTests, CreateStackKeypairTests,
+    CreateStackVolumeTests)
 from snaps.openstack.tests.create_user_tests import (
     UserSettingsUnitTests, CreateUserSuccessTests)
 from snaps.openstack.tests.create_volume_tests import (
@@ -88,7 +89,8 @@ from snaps.openstack.utils.tests.glance_utils_tests import (
     GlanceSmokeTests, GlanceUtilsTests)
 from snaps.openstack.utils.tests.heat_utils_tests import (
     HeatSmokeTests, HeatUtilsCreateSimpleStackTests,
-    HeatUtilsCreateComplexStackTests, HeatUtilsVolumeTests)
+    HeatUtilsCreateComplexStackTests, HeatUtilsVolumeTests,
+    HeatUtilsKeypairTests)
 from snaps.openstack.utils.tests.keystone_utils_tests import (
     KeystoneSmokeTests, KeystoneUtilsTests)
 from snaps.openstack.utils.tests.neutron_utils_tests import (
@@ -329,6 +331,10 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True,
         HeatUtilsVolumeTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level,
         image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        HeatUtilsKeypairTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
     suite.addTest(OSComponentTestCase.parameterize(
         CinderUtilsQoSTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level,
@@ -515,6 +521,11 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
         use_keystone=use_keystone,
         flavor_metadata=flavor_metadata, image_metadata=image_metadata,
         log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateStackKeypairTests, os_creds=os_creds, ext_net_name=ext_net_name,
+        use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
     suite.addTest(OSIntegrationTestCase.parameterize(
         CreateStackNegativeTests, os_creds=os_creds, ext_net_name=ext_net_name,
         use_keystone=use_keystone,