diff --git a/heat/tests/functional/test_bin_heat.py b/heat/tests/functional/test_bin_heat.py new file mode 100644 index 0000000000..3a3081c0ac --- /dev/null +++ b/heat/tests/functional/test_bin_heat.py @@ -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 +# +# 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. + + +"""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(speed='slow') + @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'], + 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' + + subprocess.call(['heat', '-d', 'jeos-create', + args[0], args[1], args[2]]) + + gclient = glance_client.Client(host="0.0.0.0", 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 + 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 + break + + # technically not necessary, but glance registers image before + # completely through with its operations + time.sleep(10) + + 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 + + subprocess.call(['heat', '-d', 'create', 'teststack', + '--template-file=' + self.basepath + + '/templates/WordPress_Single_Instance.template', + '--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 + time.sleep(10) + + for server in nt.servers.list(): + if server.name == 'WikiDatabase': # TODO: get from template + address = server.addresses + print "Status: %s" % server.status + if address: + ip = address['wordpress'][0]['addr'] + print 'IP found:', ip + break + elif server.status == 'ERROR': + print 'Heat error? Aborting' + assert False + return + + tries = 0 + while True: + try: + subprocess.check_output(['nc', '-z', ip, '22']) + except Exception: + print 'SSH not up yet...' + time.sleep(10) + tries += 1 + assert tries < 100 + else: + print 'SSH daemon response detected' + time.sleep(5) # yuck, sometimes SSH is not *really* up + break + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(ip, username='ec2-user', allow_agent=True, + look_for_keys=True, password='password') + sftp = ssh.open_sftp() + + tries = 0 + while True: + try: + sftp.stat('/var/lib/cloud/instance/boot-finished') + except IOError, e: + tries += 1 + if e[0] == 2: + assert tries < 40 + print "Boot not finished yet..." + time.sleep(15) + else: + raise + else: + print "boot-finished file found, start host checks" + break + + 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', 'cfn_helper.py'] + + cfntools = {} + for file in cfn_tools_files: + with open(self.basepath + '/heat/cfntools/' + file, 'rb') as f: + sha = hashlib.sha1(f.read()).hexdigest() + 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.get_pty() + 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', + 'part-handler.py': self.basepath + + '/heat/cloudinit/part-handler.py' + } + f = open(self.basepath + + '/templates/WordPress_Single_Instance.template') + t = json.loads(f.read()) + f.close() + + 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 = sftp.open('/var/lib/cloud/instance/scripts/startup') + remote_file_list = remote_file.read().split('\n') + remote_file.close() + + 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 = sftp.open('/var/lib/cloud/instance/user-data.txt.i') + msg = email.message_from_file(remote_file) + remote_file.close() + + for part in msg.walk(): + # multipart/* are just containers + if part.get_content_maintype() == 'multipart': + continue + + file = part.get_filename() + data = part.get_payload() + + if file in filepaths.keys(): + with open(filepaths[file]) as f: + assert data == f.read() + + # cleanup + ssh.close() + subprocess.call(['heat', 'delete', 'teststack']) + +if __name__ == '__main__': + sys.argv.append(__file__) + nose.main() diff --git a/tox.ini b/tox.ini index 96afb18b32..3340d5ef16 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ setenv = VIRTUAL_ENV={envdir} NOSE_OPENSTACK_SHOW_ELAPSED=1 deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires -commands = nosetests +commands = nosetests -a tag='unit' [testenv:pep8] deps = pep8 @@ -20,7 +20,7 @@ commands = pep8 --repeat --show-source heat setup.py commands = {posargs} [testenv:cover] -commands = nosetests --cover-erase --cover-package=heat --with-xcoverage +commands = nosetests --cover-erase --cover-package=heat --with-xcoverage -a tag='unit' [tox:jenkins] downloadcache = ~/cache/pip @@ -35,7 +35,7 @@ setenv = NOSE_WITH_XUNIT=1 [testenv:jenkinscover] 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' [testenv:jenkinsvenv] setenv = NOSE_WITH_XUNIT=1