]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Rework functional test case infrasatructure
authorSteven Dake <sdake@redhat.com>
Fri, 7 Sep 2012 20:22:42 +0000 (13:22 -0700)
committerSteven Dake <sdake@redhat.com>
Mon, 10 Sep 2012 00:26:21 +0000 (17:26 -0700)
To support multi-instance, two new classes were made:
Stack - represents a stack
Instance - represents an instance

For multi-instance stacks, create multiple Instance objects
for each instance in the stack.

For each instance in a stack, an instance object can be created which
helps validate the individual instance sets up properly.

test_WordPress_Single_Instance_With_EBS.py fails - see issue #226

Change-Id: Iddec87cd1332a9b5796c5c7e7d382ef723c3544e
Signed-off-by: Steven Dake <sdake@redhat.com>
Signed-off-by: Tomas Sedovic <tsedovic@redhat.com>
Signed-off-by: Ian Main <imain@redhat.com>
heat/tests/functional/test_WordPress_Single_Instance.py
heat/tests/functional/test_WordPress_Single_Instance_With_EBS.py
heat/tests/functional/test_WordPress_Single_Instance_With_EBS_EIP.py
heat/tests/functional/test_WordPress_Single_Instance_With_EIP.py
heat/tests/functional/test_WordPress_Single_Instance_With_HA.py
heat/tests/functional/test_WordPress_With_LB.py
heat/tests/functional/util.py

index c3f57c43037cbea754895fb065fe897719e1c702..ce89afef5f0394c1943ff0cfec3672d4b5b1ba8d 100644 (file)
@@ -20,35 +20,28 @@ import unittest
 
 
 @attr(speed='slow')
-@attr(tag=['func', 'wordpress'])
+@attr(tag=['func', 'wordpress', 'WordPress_Single_Instance.template'])
 class WordPressFunctionalTest(unittest.TestCase):
     def setUp(self):
         template = 'WordPress_Single_Instance.template'
 
-        self.func_utils = util.FuncUtils()
-
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        self.func_utils.check_user_data(template)
-
-        self.ssh = self.func_utils.get_ssh_client()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
+        self.WikiDatabase = util.Instance('WikiDatabase')
+        self.WikiDatabase.check_cfntools()
+        self.WikiDatabase.wait_for_provisioning()
 
     def test_instance(self):
         # ensure wordpress was installed by checking for expected
         # configuration file over ssh
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        assert result == wp_file
+        self.assertTrue(self.WikiDatabase.file_present
+                        ('/etc/wordpress/wp-config.php'))
         print "Wordpress installation detected"
 
         # Verify the output URL parses as expected, ie check that
         # the wordpress installation is operational
-        stack_url = self.func_utils.get_stack_output("WebsiteURL")
+        stack_url = self.stack.get_stack_output("WebsiteURL")
         print "Got stack output WebsiteURL=%s, verifying" % stack_url
         ver = verify.VerifyStack()
-        assert True == ver.verify_wordpress(stack_url)
+        self.assertTrue(ver.verify_wordpress(stack_url))
 
-        self.func_utils.cleanup()
+        self.stack.cleanup()
index 2fcac54ffe53cb2522aefb69e3e8d5bfe45a2d69..7b198d4219ffae86f1914aa65df98c7a74ff88b1 100644 (file)
@@ -23,38 +23,33 @@ import unittest
 
 
 @attr(speed='slow')
-@attr(tag=['func', 'wordpress', 'ebs'])
+@attr(tag=['func', 'wordpress', 'ebs',
+      'WordPress_Single_Instance_With_EBS.template'])
 class WordPressSingleEBSFunctionalTest(unittest.TestCase):
     def setUp(self):
         template = 'WordPress_Single_Instance_With_EBS.template'
 
-        self.func_utils = util.FuncUtils()
-
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        self.func_utils.check_user_data(template)
-
-        self.ssh = self.func_utils.get_ssh_client()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
+        self.WikiDatabase = util.Instance('WikiDatabase')
+        self.WikiDatabase.check_cfntools()
+        self.WikiDatabase.wait_for_provisioning()
 
     def test_instance(self):
-        # 1. ensure wordpress was installed
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        self.assertEqual(result, wp_file)
+        # ensure wordpress was installed
+        self.assertTrue(self.WikiDatabase.file_present
+                        ('/etc/wordpress/wp-config.php'))
         print "Wordpress installation detected"
 
         # Verify the output URL parses as expected, ie check that
         # the wordpress installation is operational
-        stack_url = self.func_utils.get_stack_output("WebsiteURL")
+        stack_url = self.stack.get_stack_output("WebsiteURL")
         print "Got stack output WebsiteURL=%s, verifying" % stack_url
         ver = verify.VerifyStack()
         self.assertTrue(ver.verify_wordpress(stack_url))
 
         # Check EBS volume is present and mounted
-        stdin, stdout, sterr = self.ssh.exec_command('grep vdc /proc/mounts')
+        stdin, stdout, sterr = self.WikiDatabase.exec_command(
+                                'grep vdc /proc/mounts')
         result = stdout.readlines().pop().rstrip()
         self.assertTrue(len(result))
         print "Checking EBS volume is attached : %s" % result
@@ -63,4 +58,4 @@ class WordPressSingleEBSFunctionalTest(unittest.TestCase):
         mountpoint = result.split()[1]
         self.assertEqual(mountpoint, '/var/lib/mysql')
 
-        self.func_utils.cleanup()
+        self.stack.cleanup()
index c069f8fc8ffec46aa5daa0a65bb1fd1153818b1e..3e404ca86233e065bf55d5421c980ea73ae14ac8 100644 (file)
@@ -28,26 +28,19 @@ class WordPressEBSEIPFunctionalTest(unittest.TestCase):
     def setUp(self):
         template = 'WordPress_Single_Instance_With_EBS_EIP.template'
 
-        self.func_utils = util.FuncUtils()
-
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        #self.func_utils.check_user_data(template)
-
-        self.ssh = self.func_utils.get_ssh_client()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
+        self.WikiDatabase = util.Instance('WikiDatabase')
+        self.WikiDatabase.check_cfntools()
+        self.WikiDatabase.wait_for_provisioning()
 
     def test_instance(self):
-        # 1. ensure wordpress was installed
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        self.assertEqual(result, wp_file)
+        # ensure wordpress was installed
+        self.assertTrue(self.WikiDatabase.file_present
+                        ('/etc/wordpress/wp-config.php'))
         print "Wordpress installation detected"
 
         # 2. check floating ip assignment
-        nclient = self.func_utils.get_nova_client()
+        nclient = self.stack.get_nova_client()
         if len(nclient.floating_ips.list()) == 0:
             print 'zero floating IPs detected'
             self.assertTrue(False)
@@ -55,36 +48,37 @@ class WordPressEBSEIPFunctionalTest(unittest.TestCase):
             found = 0
             mylist = nclient.floating_ips.list()
             for item in mylist:
-                if item.instance_id == self.func_utils.phys_rec_id:
+                if item.instance_id == self.stack.phys_rec_id:
                     print 'floating IP found', item.ip
                     found = 1
                     break
             self.assertEqual(found, 1)
 
-        # Check EBS volume is present and mounted
-        stdin, stdout, sterr = self.ssh.exec_command('grep vdc /proc/mounts')
-        result = stdout.readlines().pop().rstrip()
-        self.assertTrue(len(result))
-        print "Checking EBS volume is attached : %s" % result
-        devname = result.split()[0]
-        self.assertEqual(devname, '/dev/vdc1')
-        mountpoint = result.split()[1]
-        self.assertEqual(mountpoint, '/var/lib/mysql')
-
         # Verify the output URL parses as expected, ie check that
         # the wordpress installation is operational
         # Note that the WebsiteURL uses the non-EIP address
-        stack_url = self.func_utils.get_stack_output("WebsiteURL")
+        stack_url = self.stack.get_stack_output("WebsiteURL")
         print "Got stack output WebsiteURL=%s, verifying" % stack_url
         ver = verify.VerifyStack()
         self.assertTrue(ver.verify_wordpress(stack_url))
 
         # Then the InstanceIPAddress is the EIP address
         # which should also render the wordpress page
-        stack_eip = self.func_utils.get_stack_output("InstanceIPAddress")
+        stack_eip = self.stack.get_stack_output("InstanceIPAddress")
         eip_url = "http://%s/wordpress" % stack_eip
         print "Got stack output InstanceIPAddress=%s, verifying url %s" %\
               (stack_eip, eip_url)
         self.assertTrue(ver.verify_wordpress(eip_url))
 
-        self.func_utils.cleanup()
+        # Check EBS volume is present and mounted
+        stdin, stdout, sterr = self.WikiDatabase.exec_command(
+                                'grep vdc /proc/mounts')
+        result = stdout.readlines().pop().rstrip()
+        self.assertTrue(len(result))
+        print "Checking EBS volume is attached : %s" % result
+        devname = result.split()[0]
+        self.assertEqual(devname, '/dev/vdc1')
+        mountpoint = result.split()[1]
+        self.assertEqual(mountpoint, '/var/lib/mysql')
+
+        self.stack.cleanup()
index 26c72b30327bd57568825ac6b4e2cd021dbf241c..f16eb9c683c59d9ed6c163d440d7b38f9be90230 100644 (file)
@@ -28,44 +28,46 @@ class WordPressEIPFunctionalTest(unittest.TestCase):
     def setUp(self):
         template = 'WordPress_Single_Instance_With_EIP.template'
 
-        self.func_utils = util.FuncUtils()
-
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        #self.func_utils.check_user_data(template)
-
-        self.ssh = self.func_utils.get_ssh_client()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
+        self.WikiDatabase = util.Instance('WikiDatabase')
+        self.WikiDatabase.check_cfntools()
+        self.WikiDatabase.wait_for_provisioning()
 
     def test_instance(self):
-        # 1. ensure wordpress was installed
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        assert result == wp_file
+        # ensure wordpress was installed
+        self.assertTrue(self.WikiDatabase.file_present
+                        ('/etc/wordpress/wp-config.php'))
         print "Wordpress installation detected"
 
         # 2. check floating ip assignment
-        nclient = self.func_utils.get_nova_client()
+        nclient = self.stack.get_nova_client()
         if len(nclient.floating_ips.list()) == 0:
             print 'zero floating IPs detected'
-            assert False
+            self.assertTrue(False)
         else:
             found = 0
             mylist = nclient.floating_ips.list()
             for item in mylist:
-                if item.instance_id == self.func_utils.phys_rec_id:
+                if item.instance_id == self.stack.phys_rec_id:
                     print 'floating IP found', item.ip
                     found = 1
                     break
-            assert found == 1
+            self.assertEqual(found, 1)
 
         # Verify the output URL parses as expected, ie check that
         # the wordpress installation is operational
-        stack_url = self.func_utils.get_stack_output("WebsiteURL")
+        # Note that the WebsiteURL uses the non-EIP address
+        stack_url = self.stack.get_stack_output("WebsiteURL")
         print "Got stack output WebsiteURL=%s, verifying" % stack_url
         ver = verify.VerifyStack()
-        assert True == ver.verify_wordpress(stack_url)
+        self.assertTrue(ver.verify_wordpress(stack_url))
+
+        # Then the InstanceIPAddress is the EIP address
+        # which should also render the wordpress page
+        stack_eip = self.stack.get_stack_output("InstanceIPAddress")
+        eip_url = "http://%s/wordpress" % stack_eip
+        print "Got stack output InstanceIPAddress=%s, verifying url %s" %\
+              (stack_eip, eip_url)
+        self.assertTrue(ver.verify_wordpress(eip_url))
 
-        self.func_utils.cleanup()
+        self.stack.cleanup()
index 253700bbe283108e848dc7d5a1e854dab586ab46..ab2539c9d85cd15e9e69e7ffc95e437010b081a0 100644 (file)
@@ -19,25 +19,21 @@ import unittest
 
 
 @attr(speed='slow')
-@attr(tag=['func', 'wordpress', 'HA'])
+@attr(tag=['func', 'wordpress', 'HA',
+      'WordPress_Single_Instance_With_HA.template'])
 class HaFunctionalTest(unittest.TestCase):
-
-    func_utils = util.FuncUtils()
-
     def setUp(self):
         template = 'WordPress_Single_Instance_With_HA.template'
 
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        self.func_utils.check_user_data(template)
-
-        self.ssh = self.func_utils.get_ssh_client()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
+        self.WikiDatabase = util.Instance('WikiDatabase')
+        self.WikiDatabase.check_cfntools()
+        self.WikiDatabase.wait_for_provisioning()
 
     def service_is_running(self, name):
         stdin, stdout, sterr = \
-            self.ssh.exec_command('systemctl status %s' % name + '.service')
+            self.WikiDatabase.exec_command(
+                'systemctl status %s' % name + '.service')
 
         lines = stdout.readlines()
         for line in lines:
@@ -48,17 +44,15 @@ class HaFunctionalTest(unittest.TestCase):
     def test_instance(self):
 
         # ensure wordpress was installed
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        self.assertEqual(result, wp_file)
+        self.assertTrue(self.WikiDatabase.file_present
+                        ('/etc/wordpress/wp-config.php'))
         print "Wordpress installation detected"
 
         # check the httpd service is running
         self.assertTrue(self.service_is_running('httpd'))
 
         # kill httpd
-        self.ssh.exec_command('sudo systemctl stop httpd.service')
+        self.WikiDatabase.exec_command('sudo systemctl stop httpd.service')
 
         # check that httpd service recovers
         # should take less than 60 seconds, but no worse than 70 seconds
@@ -68,4 +62,4 @@ class HaFunctionalTest(unittest.TestCase):
             self.assertTrue(tries < 8)
             time.sleep(10)
 
-        self.func_utils.cleanup()
+        self.stack.cleanup()
index b67b7c0c904457893dea5b58470a523b4a7a7dd9..56dec55731d5c8ae284b627c2f36d124b9397082 100644 (file)
@@ -25,30 +25,30 @@ class WordPressWithLBFunctionalTest(unittest.TestCase):
     def setUp(self):
         template = 'WordPress_With_LB.template'
 
-        self.func_utils = util.FuncUtils()
+        self.stack = util.Stack(template, 'F17', 'x86_64', 'cfntools')
 
-        self.func_utils.prepare_jeos('F17', 'x86_64', 'cfntools')
-        self.func_utils.create_stack(template, 'F17')
-        self.func_utils.check_cfntools()
-        self.func_utils.wait_for_provisioning()
-        self.func_utils.check_user_data(template)
+        self.WikiServerOne = util.Instance('WikiServerOne')
+        self.LBInstance = util.Instance('LB_instance')
+        self.MySqlDatabaseServer = util.Instance('MySqlDatabaseServer')
 
-        self.ssh = self.func_utils.get_ssh_client()
+        self.WikiServerOne.check_cfntools()
+        self.LBInstance.check_cfntools()
+        self.MySqlDatabaseServer.check_cfntools()
+
+        self.WikiServerOne.wait_for_provisioning()
+        self.LBInstance.wait_for_provisioning()
+        self.MySqlDatabaseServer.wait_for_provisioning()
 
     def test_instance(self):
-        # ensure wordpress was installed by checking for expected
-        # configuration file over ssh
-        wp_file = '/etc/wordpress/wp-config.php'
-        stdin, stdout, sterr = self.ssh.exec_command('ls ' + wp_file)
-        result = stdout.readlines().pop().rstrip()
-        self.assertTrue(result == wp_file)
-        print "Wordpress installation detected"
+        self.assertTrue(self.WikiServerOne.file_present
+                        ('/etc/wordpress/wp-config.php'))
+        print 'Wordpress installation detected.'
 
         # Verify the output URL parses as expected, ie check that
-        # the wordpress installation is operational
-        stack_url = self.func_utils.get_stack_output("WebsiteURL")
-        print "Got stack output WebsiteURL=%s, verifying" % stack_url
+        # thewordpress installation is operational
+        stack_url = self.stack.get_stack_output("WebsiteURL")
+        print "Verifying stack output from WebsiteUrl=(%s)." % stack_url
         ver = verify.VerifyStack()
         self.assertTrue(ver.verify_wordpress(stack_url))
 
-        self.func_utils.cleanup()
+        self.stack.cleanup()
index 5f978b72f160999f51e55d184684c84a9c987ca7..e98706c2fbc84c91f1448356569e72e48a896b64 100644 (file)
@@ -38,195 +38,68 @@ from heat.engine import parser
 from heat import client as heat_client
 
 
-class FuncUtils:
+class Instance(object):
+    def __init__(self, instance_name):
+        self.name = instance_name
+
+        # during nose test execution this file will be imported even if
+        # the unit tag was specified
+        try:
+            os.environ['OS_AUTH_STRATEGY']
+        except KeyError:
+            raise SkipTest('OS_AUTH_STRATEGY unset, skipping functional test')
+
+        if os.environ['OS_AUTH_STRATEGY'] != 'keystone':
+            print 'keystone authentication required'
+            assert False
+
+        self.creds = dict(username=os.environ['OS_USERNAME'],
+                password=os.environ['OS_PASSWORD'],
+                tenant=os.environ['OS_TENANT_NAME'],
+                auth_url=os.environ['OS_AUTH_URL'],
+                strategy=os.environ['OS_AUTH_STRATEGY'])
+        dbusername = 'testuser'
+        stackname = 'teststack'
+
+        # this test is in heat/tests/functional, so go up 3 dirs
+        basepath = os.path.abspath(
+                os.path.dirname(os.path.realpath(__file__)) + '/../../..')
 
-    # during nose test execution this file will be imported even if
-    # the unit tag was specified
-    try:
-        os.environ['OS_AUTH_STRATEGY']
-    except KeyError:
-        raise SkipTest('OS_AUTH_STRATEGY not set, skipping functional test')
-
-    if os.environ['OS_AUTH_STRATEGY'] != 'keystone':
-        print 'keystone authentication required'
-        assert False
-
-    creds = dict(username=os.environ['OS_USERNAME'],
-            password=os.environ['OS_PASSWORD'],
-            tenant=os.environ['OS_TENANT_NAME'],
-            auth_url=os.environ['OS_AUTH_URL'],
-            strategy=os.environ['OS_AUTH_STRATEGY'])
-    dbusername = 'testuser'
-    stackname = 'teststack'
-
-    # this test is in heat/tests/functional, so go up 3 dirs
-    basepath = os.path.abspath(
-            os.path.dirname(os.path.realpath(__file__)) + '/../../..')
-
-    ssh = paramiko.SSHClient()
-    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    sftp = None
-    novaclient = None
-    glanceclient = None
-    heatclient = None
-    phys_rec_id = None
-
-    def get_ssh_client(self):
-        if self.ssh.get_transport() != None:
-            return self.ssh
-        return None
-
-    def get_sftp_client(self):
-        if self.sftp != None:
-            return self.sftp
-        return None
-
-    def get_nova_client(self):
-        if self.novaclient != None:
-            return self.novaclient
-        return None
-
-    def get_glance_client(self):
-        if self.glanceclient != None:
-            return self.glanceclient
-        return None
-
-    def get_heat_client(self):
-        if self.heatclient != None:
-            return self.heatclient
-        return None
-
-    def prepare_jeos(self, p_os, arch, type):
-        imagename = p_os + '-' + arch + '-' + type
-
-        self.glanceclient = glance_client.Client(host="0.0.0.0", port=9292,
-            use_ssl=False, auth_tok=None, creds=self.creds)
-
-        # skip creating jeos if image already available
-        if not self.poll_glance(self.glanceclient, imagename, False):
-            if os.geteuid() != 0:
-                print 'test must be run as root to create jeos'
-                assert False
-
-            # -d: debug, -G: register with glance
-            subprocess.call(['heat-jeos', '-d', '-G', 'create', imagename])
-
-            # Nose seems to change the behavior of the subprocess call to be
-            # asynchronous. So poll glance until image is registered.
-            self.poll_glance(self.glanceclient, imagename, True)
-
-    def poll_glance(self, gclient, imagename, block):
-        imagelistname = None
-        tries = 0
-        while imagelistname != imagename:
-            tries += 1
-            assert tries < 50
-            if block:
-                time.sleep(15)
-            print "Checking glance for image registration"
-            imageslist = gclient.get_images()
-            for x in imageslist:
-                imagelistname = x['name']
-                if imagelistname == imagename:
-                    print "Found image registration for %s" % imagename
-                    # technically not necessary, but glance registers image
-                    # before completely through with its operations
-                    time.sleep(10)
-                    return True
-            if not block:
-                break
-        return False
-
-    def create_stack(self, template_file, distribution):
         self.novaclient = nova_client.Client(self.creds['username'],
             self.creds['password'], self.creds['tenant'],
             self.creds['auth_url'], service_type='compute')
 
-        keyname = self.novaclient.keypairs.list().pop().name
-
-        self.heatclient = heat_client.get_client('0.0.0.0', 8000,
-            self.creds['username'], self.creds['password'],
-            self.creds['tenant'], self.creds['auth_url'],
-            self.creds['strategy'], None, None, False)
-
-        assert self.heatclient
-
-        # Dummy up the optparse.Values we get from CLI args in bin/heat
-        stack_paramstr = ';'.join(['InstanceType=m1.xlarge',
-                         'DBUsername=' + self.dbusername,
-                         'DBPassword=' + os.environ['OS_PASSWORD'],
-                         'KeyName=' + keyname,
-                         'LinuxDistribution=' + distribution])
-        template_params = optparse.Values({'parameters': stack_paramstr})
+        self.ssh = paramiko.SSHClient()
 
-        # Format parameters and create the stack
-        parameters = {}
-        parameters['StackName'] = self.stackname
-        template_path = self.basepath + '/templates/' + template_file
-        parameters['TemplateBody'] = open(template_path).read()
-        parameters.update(self.heatclient.format_parameters(template_params))
-        result = self.heatclient.create_stack(**parameters)
+        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
 
-        # Check result looks OK
-        root = etree.fromstring(result)
-        create_list = root.xpath('/CreateStackResponse/CreateStackResult')
-        assert create_list
-        assert len(create_list) == 1
-
-        # Extract StackId from the result, and check the StackName part
-        stackid = create_list[0].findtext('StackId')
-        idname = stackid.split('/')[1]
-        print "Checking %s contains name %s" % (stackid, self.stackname)
-        assert idname == self.stackname
-
-        alist = None
-        tries = 0
-        print 'Waiting for stack creation to be completed'
-        while not alist:
-            tries += 1
-            assert tries < 500
-            time.sleep(10)
-            events = self.heatclient.list_stack_events(**parameters)
-            root = etree.fromstring(events)
-            alist = root.xpath('//member[StackName="' + self.stackname +
-                '" and ResourceStatus="CREATE_COMPLETE" \
-                and ResourceType="AWS::EC2::Instance"]')
-
-        elem = alist.pop()
-        self.phys_rec_id = elem.findtext('PhysicalResourceId')
-
-        print "Checking network address assignment"
         ip = None
         tries = 0
         while ip is None:
-            tries += 1
-            assert tries < 500
-            time.sleep(10)
-
-            for server in self.novaclient.servers.list():
-                if server.id == self.phys_rec_id:
+            servers = self.novaclient.servers.list()
+            for server in servers:
+                if server.name == instance_name:
                     address = server.addresses
-                    print "Status: %s" % server.status
                     if address:
                         ip = address.items()[0][1][0]['addr']
-                        print 'IP found:', ip
-                        break
-                    elif server.status == 'ERROR':
-                        print 'Heat error? Aborting'
-                        assert False
-                        return
+                time.sleep(10)
+                tries += 1
+                assert tries < 500
+            print 'Instance (%s) ip (%s) status (%s)' % (self.name, ip,
+                 server.status)
 
         tries = 0
         while True:
             try:
                 subprocess.check_output(['nc', '-z', ip, '22'])
             except Exception:
-                print 'SSH not up yet...'
+                print('Instance (%s) ip (%s) SSH not up yet, waiting...' %
+                      (self.name, ip))
                 time.sleep(10)
                 tries += 1
                 assert tries < 50
             else:
-                print 'SSH daemon response detected'
+                print 'Instance (%s) ip (%s) SSH detected.' % (self.name, ip)
                 break
 
         tries = 0
@@ -239,13 +112,15 @@ class FuncUtils:
             except paramiko.AuthenticationException:
                 print 'Authentication error'
                 time.sleep(2)
-            except Exception, e:
+            except Exception as e:
                 if e.errno != errno.EHOSTUNREACH:
                     raise
-                print 'Preparing to connect over SSH'
+                print('Instance (%s) ip (%s) connecting via SSH.' %
+                      (self.name, ip))
                 time.sleep(2)
             else:
-                print 'SSH connected'
+                print('Instance (%s) ip (%s) connected via SSH.' %
+                      (self.name, ip))
                 break
         self.sftp = self.ssh.open_sftp()
 
@@ -257,15 +132,28 @@ class FuncUtils:
                 tries += 1
                 if e.errno == errno.ENOENT:
                     assert tries < 50
-                    print "Boot not finished yet..."
+                    print("Instance (%s) ip (%s) not booted, waiting..." %
+                          (self.name, ip))
                     time.sleep(15)
                 else:
                     print e.errno
                     raise
             else:
-                print "Guest fully booted"
+                print("Instance (%s) ip (%s) finished booting." %
+                      (self.name, ip))
                 break
 
+    def exec_command(self, cmd):
+        return self.ssh.exec_command(cmd)
+
+    def file_present(self, path):
+        print "Verifying file '%s' exists" % path
+        stdin, stdout, sterr = self.ssh.exec_command('ls "%s"' % path)
+        lines = stdout.readlines()
+        assert len(lines) == 1
+        result = lines.pop().rstrip()
+        return result == path
+
     def check_cfntools(self):
         stdin, stdout, stderr = \
             self.ssh.exec_command('cd /opt/aws/bin; sha1sum *')
@@ -286,10 +174,10 @@ class FuncUtils:
             cur_file = data[1].rstrip()
             if cur_file in cfn_tools_files:
                 assert data[0] == cfntools[cur_file]
-        print 'VM cfntools integrity verified'
+        print 'Instance (%s) cfntools integrity verified.' % self.name
 
     def wait_for_provisioning(self):
-        print "Waiting for provisioning to complete"
+        print "Instance (%s) waiting for provisioning to complete." % self.name
         tries = 0
         while True:
             try:
@@ -298,25 +186,26 @@ class FuncUtils:
                 tries += 1
                 if e.errno == errno.ENOENT:
                     assert tries < 500
-                    print "Provisioning not finished yet..."
+                    print("Instance (%s) provisioning incomplete, waiting..." %
+                          self.name)
                     time.sleep(15)
                 else:
                     print e.errno
                     raise
             else:
-                print "Provisioning completed"
+                print "Instance (%s) provisioning completed." % self.name
                 break
 
     def check_user_data(self, template_file):
         return  # until TODO is fixed
 
-        transport = self.ssh.get_transport()
-        channel = transport.open_session()
-        channel.get_pty()
-        channel.invoke_shell()  # sudo requires tty
-        channel.sendall('sudo chmod 777 \
-            sudo chmod 777 /var/lib/cloud/instance/user-data.txt.i\n')
-        time.sleep(1)  # necessary for sendall to complete
+#        transport = self.ssh.get_transport()
+#        channel = transport.open_session()
+#        channel.get_pty()
+#        channel.invoke_shell()  # sudo requires tty
+#        channel.sendall('sudo chmod 777 \
+#            sudo chmod 777 /var/lib/cloud/instance/user-data.txt.i\n')
+#        time.sleep(1)  # necessary for sendall to complete
 
         f = open(self.basepath + '/templates/' + template_file)
         t = json.loads(f.read())
@@ -369,6 +258,170 @@ class FuncUtils:
                 with open(filepaths[file]) as f:
                     assert data == f.read()
 
+    def get_ssh_client(self):
+        if self.ssh.get_transport() != None:
+            return self.ssh
+        return None
+
+    def get_sftp_client(self):
+        if self.sftp != None:
+            return self.sftp
+        return None
+
+    def close_ssh_client(self):
+        self.ssh.close()
+
+
+class Stack(object):
+    def __init__(self, template_file, distribution, arch, jeos_type):
+
+        self.prepare_jeos(distribution, arch, jeos_type)
+
+        self.novaclient = nova_client.Client(self.creds['username'],
+            self.creds['password'], self.creds['tenant'],
+            self.creds['auth_url'], service_type='compute')
+
+        keyname = self.novaclient.keypairs.list().pop().name
+
+        self.heatclient = heat_client.get_client('0.0.0.0', 8000,
+            self.creds['username'], self.creds['password'],
+            self.creds['tenant'], self.creds['auth_url'],
+            self.creds['strategy'], None, None, False)
+
+        assert self.heatclient
+
+        # Dummy up the optparse.Values we get from CLI args in bin/heat
+        stack_paramstr = ';'.join(['InstanceType=m1.xlarge',
+                         'DBUsername=' + self.dbusername,
+                         'DBPassword=' + os.environ['OS_PASSWORD'],
+                         'KeyName=' + keyname,
+                         'LinuxDistribution=' + distribution])
+        template_params = optparse.Values({'parameters': stack_paramstr})
+
+        # Format parameters and create the stack
+        parameters = {}
+        parameters['StackName'] = self.stackname
+        template_path = self.basepath + '/templates/' + template_file
+        parameters['TemplateBody'] = open(template_path).read()
+        parameters.update(self.heatclient.format_parameters(template_params))
+        result = self.heatclient.create_stack(**parameters)
+
+        # Check result looks OK
+        root = etree.fromstring(result)
+        create_list = root.xpath('/CreateStackResponse/CreateStackResult')
+        assert create_list
+        assert len(create_list) == 1
+
+        # Extract StackId from the result, and check the StackName part
+        stackid = create_list[0].findtext('StackId')
+        idname = stackid.split('/')[1]
+        print "Checking %s contains name %s" % (stackid, self.stackname)
+        assert idname == self.stackname
+
+        alist = None
+        tries = 0
+        print 'Waiting for stack creation to be completed'
+        while not alist:
+            tries += 1
+            assert tries < 500
+            time.sleep(10)
+            events = self.heatclient.list_stack_events(**parameters)
+            root = etree.fromstring(events)
+            alist = root.xpath('//member[StackName="' + self.stackname +
+                '" and ResourceStatus="CREATE_COMPLETE" \
+                and ResourceType="AWS::EC2::Instance"]')
+
+        elem = alist.pop()
+        self.phys_rec_id = elem.findtext('PhysicalResourceId')
+
+    # during nose test execution this file will be imported even if
+    # the unit tag was specified
+    try:
+        os.environ['OS_AUTH_STRATEGY']
+    except KeyError:
+        raise SkipTest('OS_AUTH_STRATEGY not set, skipping functional test')
+
+    if os.environ['OS_AUTH_STRATEGY'] != 'keystone':
+        print 'keystone authentication required'
+        assert False
+
+    creds = dict(username=os.environ['OS_USERNAME'],
+            password=os.environ['OS_PASSWORD'],
+            tenant=os.environ['OS_TENANT_NAME'],
+            auth_url=os.environ['OS_AUTH_URL'],
+            strategy=os.environ['OS_AUTH_STRATEGY'])
+    dbusername = 'testuser'
+    stackname = 'teststack'
+
+    # this test is in heat/tests/functional, so go up 3 dirs
+    basepath = os.path.abspath(
+            os.path.dirname(os.path.realpath(__file__)) + '/../../..')
+
+    novaclient = None
+    glanceclient = None
+    heatclient = None
+
+    def cleanup(self):
+        parameters = {'StackName': self.stackname}
+        c = self.get_heat_client()
+        c.delete_stack(**parameters)
+
+    def get_nova_client(self):
+        if self.novaclient != None:
+            return self.novaclient
+        return None
+
+    def get_glance_client(self):
+        if self.glanceclient != None:
+            return self.glanceclient
+        return None
+
+    def get_heat_client(self):
+        if self.heatclient != None:
+            return self.heatclient
+        return None
+
+    def prepare_jeos(self, p_os, arch, type):
+        imagename = p_os + '-' + arch + '-' + type
+
+        self.glanceclient = glance_client.Client(host="0.0.0.0", port=9292,
+            use_ssl=False, auth_tok=None, creds=self.creds)
+
+        # skip creating jeos if image already available
+        if not self.poll_glance(self.glanceclient, imagename, False):
+            if os.geteuid() != 0:
+                print 'test must be run as root to create jeos'
+                assert False
+
+            # -d: debug, -G: register with glance
+            subprocess.call(['heat-jeos', '-d', '-G', 'create', imagename])
+
+            # Nose seems to change the behavior of the subprocess call to be
+            # asynchronous. So poll glance until image is registered.
+            self.poll_glance(self.glanceclient, imagename, True)
+
+    def poll_glance(self, gclient, imagename, block):
+        imagelistname = None
+        tries = 0
+        while imagelistname != imagename:
+            tries += 1
+            assert tries < 50
+            if block:
+                time.sleep(15)
+            print "Checking glance for image registration"
+            imageslist = gclient.get_images()
+            for x in imageslist:
+                imagelistname = x['name']
+                if imagelistname == imagename:
+                    print "Found image registration for %s" % imagename
+                    # technically not necessary, but glance registers image
+                    # before completely through with its operations
+                    time.sleep(10)
+                    return True
+            if not block:
+                break
+        return False
+
     def get_stack_output(self, output_key):
         '''
         Extract a specified output from the DescribeStacks details
@@ -385,11 +438,6 @@ class FuncUtils:
         value = output.findtext('OutputValue')
         return value
 
-    def cleanup(self):
-        self.ssh.close()
-        parameters = {'StackName': self.stackname}
-        c = self.get_heat_client()
-        c.delete_stack(**parameters)
 
 if __name__ == '__main__':
     sys.argv.append(__file__)