Add functional test to verify jeos and stack ops

(Tox.ini has been modified to only run tests tagged with 'unit'
to prevent this test from running with unit tests.)

This test requires an OpenStack install present and will not run on
StackForge. This test creates a JEOS, waits for glance registration,
detects key registered with keystone, creates stack, and verifies over
SSH that:
- cfn helper script SHAs match tree
- verifies presence of wordpress
- verifies expected user data is present in multipart mime file

closes #112

Change-Id: I22a0dfe41986d466ac689c050fc33585e3e6229e
Signed-off-by: Jeff Peeler <>
Jeff Peeler 11 years ago
parent a4f5ae264d
commit a83fcc6aa3

@ -0,0 +1,250 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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
# 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.
"""Functional test case that utilizes the bin/heat CLI tool"""
import sys
import os
import optparse
import paramiko
import subprocess
import hashlib
import email
import json
import time # for sleep
import nose
from nose.plugins.attrib import attr
from nose import with_setup
from novaclient.v1_1 import client
from glance import client as glance_client
from heat import utils
from heat.engine import parser
class TestBinHeat():
"""Functional tests for the bin/heat CLI tool"""
def setUp(self):
if os.geteuid() != 0:
print 'test must be run as root'
assert False
if os.environ['OS_AUTH_STRATEGY'] != 'keystone':
print 'keystone authentication required'
assert False
# this test is in heat/tests/functional, so go up 3 dirs
self.basepath = os.path.abspath(
os.path.dirname(os.path.realpath(__file__)) + '/../../..')
@attr(tag=['func', 'jeos'])
def test_jeos_create(self):
# 0. Verify JEOS and cloud init
args = ('F16', 'x86_64', 'cfntools')
creds = dict(username=os.environ['OS_USERNAME'],
dbusername = 'testuser'['heat', '-d', 'jeos-create',
args[0], args[1], args[2]])
gclient = glance_client.Client(host="", port=9292,
use_ssl=False, auth_tok=None, creds=creds)
# Nose seems to change the behavior of the subprocess call to be
# asynchronous. So poll glance until image is registered.
imagename = '-'.join(str(i) for i in args)
imagelistname = None
tries = 0
while imagelistname != imagename:
tries += 1
assert tries < 5000
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
nt = client.Client(os.environ['OS_USERNAME'],
os.environ['OS_PASSWORD'], os.environ['OS_TENANT_NAME'],
os.environ['OS_AUTH_URL'], service_type='compute')
keyname = nt.keypairs.list().pop().name['heat', '-d', 'create', 'teststack',
'--template-file=' + self.basepath +
'--parameters=InstanceType=m1.xlarge;DBUsername=' + dbusername +
';DBPassword=' + os.environ['OS_PASSWORD'] +
';KeyName=' + keyname])
print "Waiting for OpenStack to initialize and assign network address"
ip = None
tries = 0
while ip == None:
tries += 1
assert tries < 500
for server in nt.servers.list():
if == 'WikiDatabase': # TODO: get from template
address = server.addresses
print "Status: %s" % server.status
if address:
ip = address['wordpress'][0]['addr']
print 'IP found:', ip
elif server.status == 'ERROR':
print 'Heat error? Aborting'
assert False
tries = 0
while True:
subprocess.check_output(['nc', '-z', ip, '22'])
except Exception:
print 'SSH not up yet...'
tries += 1
assert tries < 100
print 'SSH daemon response detected'
time.sleep(5) # yuck, sometimes SSH is not *really* up
ssh = paramiko.SSHClient()
ssh.connect(ip, username='ec2-user', allow_agent=True,
look_for_keys=True, password='password')
sftp = ssh.open_sftp()
tries = 0
while True:
except IOError, e:
tries += 1
if e[0] == 2:
assert tries < 40
print "Boot not finished yet..."
print "boot-finished file found, start host checks"
stdin, stdout, stderr = ssh.exec_command('cd /opt/aws/bin; sha1sum *')
files = stdout.readlines()
cfn_tools_files = ['cfn-init', 'cfn-hup', 'cfn-signal',
'cfn-get-metadata', '']
cfntools = {}
for file in cfn_tools_files:
with open(self.basepath + '/heat/cfntools/' + file, 'rb') as f:
sha = hashlib.sha1(
cfntools[file] = sha
# 1. make sure cfntools SHA in tree match VM's version
for x in range(len(files)):
data = files.pop().split(' ')
cur_file = data[1].rstrip()
if cur_file in cfn_tools_files:
assert data[0] == cfntools[cur_file]
# 2. ensure wordpress was installed
wp_file = '/etc/wordpress/wp-config.php'
stdin, stdout, sterr = ssh.exec_command('ls ' + wp_file)
result = stdout.readlines().pop().rstrip()
assert result == wp_file
# 3. check multipart mime accuracy
transport = ssh.get_transport()
channel = transport.open_session()
channel.invoke_shell() # sudo requires tty
channel.sendall('sudo chmod 777 \
/var/lib/cloud/instance/scripts/startup; \
sudo chmod 777 /var/lib/cloud/instance/user-data.txt.i\n')
time.sleep(1) # necessary for sendall to complete
filepaths = {
'cloud-config': self.basepath + '/heat/cloudinit/config',
'': self.basepath +
f = open(self.basepath +
t = json.loads(
params = {}
params['KeyStoneCreds'] = None
t['Parameters']['KeyName']['Value'] = keyname
t['Parameters']['DBUsername']['Value'] = dbusername
t['Parameters']['DBPassword']['Value'] = creds['password']
stack = parser.Stack('test', t, 0, params)
parsed_t = stack.resolve_static_refs(t)
remote_file ='/var/lib/cloud/instance/scripts/startup')
remote_file_list ='\n')
t_data = parsed_t['Resources']['WikiDatabase']['Properties']
t_data = t_data['UserData']['Fn::Base64']['Fn::Join'].pop()
joined_t_data = ''.join(t_data)
t_data_list = joined_t_data.split('\n')
assert t_data_list == remote_file_list
remote_file ='/var/lib/cloud/instance/user-data.txt.i')
msg = email.message_from_file(remote_file)
for part in msg.walk():
# multipart/* are just containers
if part.get_content_maintype() == 'multipart':
file = part.get_filename()
data = part.get_payload()
if file in filepaths.keys():
with open(filepaths[file]) as f:
assert data ==
# cleanup
ssh.close()['heat', 'delete', 'teststack'])
if __name__ == '__main__':

@ -10,7 +10,7 @@ setenv = VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/tools/pip-requires
commands = nosetests
commands = nosetests -a tag='unit'
deps = pep8
@ -20,7 +20,7 @@ commands = pep8 --repeat --show-source heat
commands = {posargs}
commands = nosetests --cover-erase --cover-package=heat --with-xcoverage
commands = nosetests --cover-erase --cover-package=heat --with-xcoverage -a tag='unit'
downloadcache = ~/cache/pip
@ -35,7 +35,7 @@ setenv = NOSE_WITH_XUNIT=1
setenv = NOSE_WITH_XUNIT=1
commands = nosetests --cover-erase --cover-package=heat --with-xcoverage
commands = nosetests --cover-erase --cover-package=heat --with-xcoverage -a tag='unit'
setenv = NOSE_WITH_XUNIT=1