Initial change to support heat-based environments

Moved the patch from the mcp/mcp-qa repo, ref #1170 to
not lose it after cleaning up.

1.Added envmanager_heat.py to create environment in OpenStack
  and use the created heat stack as the metadata source.
  Current conventions for heat stack metadata:
  - OS::Nova::Server must use 'metadata' property to specify list
    of the node roles, example:

    cfg01_node:
      type: OS::Nova::Server
      ...
      properties:
        ...
        metadata:
          roles:
          - salt_master

  - OS::Neutron::Subnet must use 'tags' property to specify the
    address pool name (L3 network roles), example:

    control_subnet:
      type: OS::Neutron::Subnet
      properties:
        ...
        tags:
        - private_pool01
2. Change underlay.yaml to use the user data file 'as is', without
   indents and jinja blocks. This will allow to use the same
   user data file for fuel-devops envs and heat stack envs.

3. Add an example microcloud-8116.env file with some defaults.
   For other stacks, another .env files can be created, with different
   access keys, networks, images, ...

Related-Bug: PROD-27687

Change-Id: Iaa9e97447bd1b41e5930a1ffbb7312945ba139f4
diff --git a/tcp_tests/helpers/exceptions.py b/tcp_tests/helpers/exceptions.py
index 7bc4abc..64b9db9 100644
--- a/tcp_tests/helpers/exceptions.py
+++ b/tcp_tests/helpers/exceptions.py
@@ -101,6 +101,39 @@
         )
 
 
+class EnvironmentWrongStatus(BaseException):
+    def __init__(self, env_name, env_expected_status, env_actual_status):
+        super(EnvironmentWrongStatus, self).__init__()
+        self.env_name = env_name
+        self.env_expected_status = env_expected_status
+        self.env_actual_status = env_actual_status
+
+    def __str__(self):
+        return ("Environment '{0}' has wrong status: "
+                "expected '{1}', got: '{2}'"
+                .format(self.env_name,
+                        self.env_expected_status,
+                        self.env_actual_status))
+
+
+class EnvironmentBadStatus(BaseException):
+    def __init__(self, env_name, env_expected_status,
+                 env_actual_status, wrong_resources):
+        super(EnvironmentBadStatus, self).__init__()
+        self.env_name = env_name
+        self.env_expected_status = env_expected_status
+        self.env_actual_status = env_actual_status
+        self.wrong_resources = wrong_resources
+
+    def __str__(self):
+        return ("Environment '{0}' has bad status: "
+                "expected '{1}', got: '{2}'\n{3}"
+                .format(self.env_name,
+                        self.env_expected_status,
+                        self.env_actual_status,
+                        self.wrong_resources))
+
+
 class EnvironmentSnapshotMissing(BaseException):
     def __init__(self, env_name, snapshot_name):
         super(EnvironmentSnapshotMissing, self).__init__()
@@ -144,3 +177,14 @@
     def __str__(self):
         return ("Cloud-init failed on node {0} with error: \n{1}"
                 .format(self.node_name, self.message))
+
+
+class EnvironmentNodeAccessError(BaseException):
+    def __init__(self, node_name, message=''):
+        super(EnvironmentNodeAccessError, self).__init__()
+        self.node_name = node_name
+        self.message = message
+
+    def __str__(self):
+        return ("Unable to reach the node {0}: \n{1}"
+                .format(self.node_name, self.message))