# 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. import sys import os import optparse import paramiko import subprocess import hashlib import email import json import time # for sleep import nose import errno from pkg_resources import resource_string from nose.plugins.attrib import attr from nose import with_setup from nose.exc import SkipTest from novaclient.v1_1 import client from heat import utils from heat.engine import parser class FuncUtils: # 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') 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 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 create_stack(self, template_file, distribution): 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', self.stackname, '--template-file=' + self.basepath + '/templates/' + template_file, '--parameters=InstanceType=m1.xlarge;DBUsername=' + self.dbusername + ';DBPassword=' + os.environ['OS_PASSWORD'] + ';KeyName=' + keyname + ';LinuxDistribution=' + distribution]) print "Waiting for OpenStack to initialize and assign network address" ip = None tries = 0 while ip is None: tries += 1 assert tries < 500 time.sleep(10) for server in nt.servers.list(): # TODO: get PhysicalResourceId instead if server.name == 'WikiDatabase': 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 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 < 50 else: print 'SSH daemon response detected' break tries = 0 while True: try: tries += 1 assert tries < 50 self.ssh.connect(ip, username='ec2-user', allow_agent=True, look_for_keys=True, password='password') except paramiko.AuthenticationException: print 'Authentication error' time.sleep(2) except Exception, e: if e.errno != errno.EHOSTUNREACH: raise print 'Preparing to connect over SSH' time.sleep(2) else: print 'SSH connected' break self.sftp = self.ssh.open_sftp() tries = 0 while True: try: self.sftp.stat('/var/lib/cloud/instance/boot-finished') except IOError, e: tries += 1 if e.errno == errno.ENOENT: assert tries < 50 print "Boot not finished yet..." time.sleep(15) else: print e.errno raise else: print "Guest fully booted" break def check_cfntools(self): stdin, stdout, stderr = \ self.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: file_data = resource_string('heat_jeos', 'cfntools/' + file) sha = hashlib.sha1(file_data).hexdigest() cfntools[file] = sha # 1. make sure installed cfntools SHA 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] print 'VM cfntools integrity verified' def wait_for_provisioning(self): print "Waiting for provisioning to complete" tries = 0 while True: try: self.sftp.stat('/var/lib/cloud/instance/provision-finished') except IOError, e: tries += 1 if e.errno == errno.ENOENT: assert tries < 500 print "Provisioning not finished yet..." time.sleep(15) else: print e.errno raise else: print "Provisioning completed" break def check_user_data(self, template_file): 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()) f.close() template = parser.Template(t) params = parser.Parameters('test', t, {'KeyName': 'required_parameter', 'DBUsername': self.dbusername, 'DBPassword': self.creds['password']}) stack = parser.Stack(None, 'test', template, params) parsed_t = stack.resolve_static_data(t) remote_file = self.sftp.open('/var/lib/cloud/data/cfn-userdata') remote_file_list = remote_file.read().split('\n') remote_file_list_u = map(unicode, remote_file_list) 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') # must match user data injection t_data_list.insert(len(t_data_list) - 1, u'touch /var/lib/cloud/instance/provision-finished') assert t_data_list == remote_file_list_u remote_file = self.sftp.open('/var/lib/cloud/instance/user-data.txt.i') msg = email.message_from_file(remote_file) remote_file.close() filepaths = { 'cloud-config': self.basepath + '/heat/cloudinit/config', 'part-handler.py': self.basepath + '/heat/cloudinit/part-handler.py' } # check multipart mime accuracy 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() def cleanup(self): self.ssh.close() subprocess.call(['heat', 'delete', self.stackname]) if __name__ == '__main__': sys.argv.append(__file__) nose.main()