From 581c061ca0513926fcda9d96996de6bf3b881588 Mon Sep 17 00:00:00 2001 From: "Harold Spencer, Jr" Date: Sat, 21 Dec 2013 23:11:52 +0000 Subject: [PATCH 1/5] UserData Testcase: added UserData test to instancetest.py; add get_userdata to euinstance.py --- eucaops/ec2ops.py | 10 +++++ eutester/euinstance.py | 4 ++ eutester/eutestcase.py | 3 ++ .../cloud_user/instances/instancetest.py | 37 +++++++++++++++++-- .../user-data-tests/userdata-max-size.txt | 1 + 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt diff --git a/eucaops/ec2ops.py b/eucaops/ec2ops.py index 2266595a..c09b027c 100644 --- a/eucaops/ec2ops.py +++ b/eucaops/ec2ops.py @@ -2389,6 +2389,7 @@ def run_instance(self, min=1, max=1, user_data=None, + user_data_file=None, private_addressing=False, username="root", password=None, @@ -2406,6 +2407,7 @@ def run_instance(self, :param min: Minimum instnaces to launch, default 1 :param max: Maxiumum instances to launch, default 1 :param user_data: User-data string to pass to instance + :param user_data_file: User-data file to pass to instance :param private_addressing: Runs an instance with only private IP address :param username: username to use when connecting via ssh :param password: password to use when connecting via ssh @@ -2420,6 +2422,9 @@ def run_instance(self, image = self.get_emi(emi=str(image)) if image is None: raise Exception("emi is None. run_instance could not auto find an emi?") + if user_data_file: + with open(user_data_file) as userdata_file: + user_data = userdata_file.read() if not user_data: user_data = self.enable_root_user_data if private_addressing is True: @@ -2512,6 +2517,7 @@ def run_image(self, max=1, block_device_map=None, user_data=None, + user_data_file=None, private_addressing=False, username="root", password=None, @@ -2529,6 +2535,7 @@ def run_image(self, :param min: minimum amount of instances to try to run :param max: max amount of instances to try to run :param user_data: user_data to run instances with + :param user_data_file: user_data file to run instances with :param private_addressing: boolean to run instances without public ips :param username: username for connecting ssh to instances :param password: password for connnecting ssh to instances @@ -2551,6 +2558,9 @@ def run_image(self, image = self.get_emi(emi=str(image)) if image is None: raise Exception("emi is None. run_instance could not auto find an emi?") + if user_data_file: + with open(user_data_file) as userdata_file: + user_data = userdata_file.read() if not user_data: user_data = self.enable_root_user_data if private_addressing is True: diff --git a/eutester/euinstance.py b/eutester/euinstance.py index 50486199..790f1398 100644 --- a/eutester/euinstance.py +++ b/eutester/euinstance.py @@ -668,6 +668,10 @@ def get_metadata(self, element_path, prefix='latest/meta-data/', timeout=10, sta return self.sys("curl http://" + self.tester.get_ec2_ip() + ":8773/"+str(prefix) + str(element_path), code=0) else: raise(se) + + def get_userdata(self, element_path, prefix='latest/user-data/'): + """Return the lines of metadata from the element path provided""" + return self.get_metadata(element_path, prefix) def set_block_device_prefix(self): return self.set_rootfs_device() diff --git a/eutester/eutestcase.py b/eutester/eutestcase.py index f54d4cc6..dae97c7b 100644 --- a/eutester/eutestcase.py +++ b/eutester/eutestcase.py @@ -479,6 +479,9 @@ def setup_parser(self, if userdata: parser.add_argument('--user-data', help="User data string to provide instance run within this test", default=None) + if userdata: + parser.add_argument('--user-data-file', + help="User data file to provide instance run within this test", default=None) if instance_user: parser.add_argument('--instance-user', help="Username used for ssh login. Default:'root'", default='root') diff --git a/testcases/cloud_user/instances/instancetest.py b/testcases/cloud_user/instances/instancetest.py index c21b67bc..61e12cfd 100755 --- a/testcases/cloud_user/instances/instancetest.py +++ b/testcases/cloud_user/instances/instancetest.py @@ -15,6 +15,8 @@ import os import re import random +import StringIO +import difflib class InstanceBasics(EutesterTestCase): @@ -55,6 +57,10 @@ def __init__( self, name="InstanceBasics", credpath=None, region=None, config_fi self.address = None self.volume = None self.private_addressing = False + if not user_data: + self.user_data_file = 'testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt' + else: + self.user_data_file = None if not zone: zones = self.tester.ec2.get_all_zones() self.zone = random.choice(zones).name @@ -62,9 +68,9 @@ def __init__( self, name="InstanceBasics", credpath=None, region=None, config_fi self.zone = zone self.reservation = None self.reservation_lock = threading.Lock() - self.run_instance_params = {'image': self.image, 'user_data': user_data, 'username': instance_user, - 'keypair': self.keypair.name, 'group': self.group.name, 'zone': self.zone, - 'timeout': self.instance_timeout} + self.run_instance_params = {'image': self.image, 'user_data': user_data, 'user_data_file': self.user_data_file, + 'username': instance_user, 'keypair': self.keypair.name, 'group': self.group.name, + 'zone': self.zone, 'timeout': self.instance_timeout} self.managed_network = True ### If I have access to the underlying infrastructure I can look @@ -238,6 +244,29 @@ def MetaData(self): self.set_reservation(reservation) return reservation + def UserData(self): + """ + This case was developed to test the user-data service of an instance for consistency. + This case does a comparison of the user data passed in by the user-data argument to + the data supplied by the user-data service within the instance. Supported + user data formats can be found here: https://cloudinit.readthedocs.org/en/latest/topics/format.html + If this test fails, the test case will error out; logging the results. + The userdata tested is 16K string (maximum size of userdata string defined by AWS) + """ + if not self.reservation: + reservation = self.tester.run_instance(**self.run_instance_params) + else: + reservation = self.reservation + for instance in reservation.instances: + """ + For aesthetics, the user data value is a 16K file thats converted to string then compare, + """ + if self.user_data_file: + with open(self.user_data_file) as user_data_file: + user_data = user_data_file.read() + instance_user_data = StringIO.StringIO(instance.get_userdata()) + self.assertTrue(difflib.SequenceMatcher(None, instance_user_data.getvalue(), user_data), 'Incorrect User Data File') + def DNSResolveCheck(self): """ This case was developed to test DNS resolution information for public/private DNS @@ -419,7 +448,7 @@ def ReuseAddresses(self): ### Either use the list of tests passed from config/command line to determine what subset of tests to run test_list = testcase.args.tests or ["BasicInstanceChecks", "DNSResolveCheck", "Reboot", "MetaData", "ElasticIps", - "MultipleInstances", "LargestInstance", "PrivateIPAddressing", "Churn"] + "UserData", "MultipleInstances", "LargestInstance", "PrivateIPAddressing", "Churn"] ### Convert test suite methods to EutesterUnitTest objects unit_list = [] for test in test_list: diff --git a/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt b/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt new file mode 100644 index 00000000..51ad0f4f --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt @@ -0,0 +1 @@ +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 \ No newline at end of file From 0ff1d764807252ed355245213c2ccdd94f785050 Mon Sep 17 00:00:00 2001 From: "Harold Spencer, Jr" Date: Sat, 21 Dec 2013 23:23:46 +0000 Subject: [PATCH 2/5] Added userdatatest.py with additional user-data-test files --- .../user-data-tests/cloud-init.config | 2 + .../user-data-tests/include-once.txt | 7 + .../instances/user-data-tests/include.txt | 5 + .../user-data-tests/part-handler-v2.txt | 38 +++++ .../user-data-tests/part-handler.txt | 23 +++ .../user-data-tests/plain-ignored.txt | 2 + .../user-data-tests/upstart-cloud-config.txt | 12 ++ .../user-data-tests/upstart-rclocal.txt | 12 ++ .../instances/user-data-tests/user-script.txt | 8 + .../instances/user-data-tests/userdata.txt | Bin 0 -> 260 bytes .../cloud_user/instances/userdatatest.py | 137 ++++++++++++++++++ 11 files changed, 246 insertions(+) create mode 100644 testcases/cloud_user/instances/user-data-tests/cloud-init.config create mode 100644 testcases/cloud_user/instances/user-data-tests/include-once.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/include.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/part-handler-v2.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/part-handler.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/plain-ignored.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/upstart-cloud-config.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/upstart-rclocal.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/user-script.txt create mode 100644 testcases/cloud_user/instances/user-data-tests/userdata.txt create mode 100755 testcases/cloud_user/instances/userdatatest.py diff --git a/testcases/cloud_user/instances/user-data-tests/cloud-init.config b/testcases/cloud_user/instances/user-data-tests/cloud-init.config new file mode 100644 index 00000000..fefc48a7 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/cloud-init.config @@ -0,0 +1,2 @@ +#cloud-config +disable_root: false diff --git a/testcases/cloud_user/instances/user-data-tests/include-once.txt b/testcases/cloud_user/instances/user-data-tests/include-once.txt new file mode 100644 index 00000000..0cf74e5e --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/include-once.txt @@ -0,0 +1,7 @@ +#include-once +# entries are one url per line. comment lines beginning with '#' are allowed +# urls are passed to urllib.urlopen, so the format must be supported there +# This entries will just be processed ONE TIME by cloud-init, any further +# iterations won't process this file +http://www.ubuntu.com/robots.txt +http://www.w3schools.com/html/lastpage.htm diff --git a/testcases/cloud_user/instances/user-data-tests/include.txt b/testcases/cloud_user/instances/user-data-tests/include.txt new file mode 100644 index 00000000..5bdc7991 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/include.txt @@ -0,0 +1,5 @@ +#include +# entries are one url per line. comment lines beginning with '#' are allowed +# urls are passed to urllib.urlopen, so the format must be supported there +http://www.ubuntu.com/robots.txt +http://www.w3schools.com/html/lastpage.htm diff --git a/testcases/cloud_user/instances/user-data-tests/part-handler-v2.txt b/testcases/cloud_user/instances/user-data-tests/part-handler-v2.txt new file mode 100644 index 00000000..554c34a5 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/part-handler-v2.txt @@ -0,0 +1,38 @@ +#part-handler +# vi: syntax=python ts=4 +# this is an example of a version 2 part handler. +# the differences between the initial part-handler version +# and v2 is: +# * handle_part receives a 5th argument, 'frequency' +# frequency will be either 'always' or 'per-instance' +# * handler_version must be set +# +# A handler declaring version 2 will be called on all instance boots, with a +# different 'frequency' argument. + +handler_version = 2 + +def list_types(): + # return a list of mime-types that are handled by this module + return(["text/plain", "text/go-cubs-go"]) + +def handle_part(data,ctype,filename,payload,frequency): + # data: the cloudinit object + # ctype: '__begin__', '__end__', or the specific mime-type of the part + # filename: the filename for the part, or dynamically generated part if + # no filename is given attribute is present + # payload: the content of the part (empty for begin or end) + # frequency: the frequency that this cloud-init run is running for + # this is either 'per-instance' or 'always'. 'per-instance' + # will be invoked only on the first boot. 'always' will + # will be called on subsequent boots. + if ctype == "__begin__": + print "my handler is beginning, frequency=%s" % frequency + return + if ctype == "__end__": + print "my handler is ending, frequency=%s" % frequency + return + + print "==== received ctype=%s filename=%s ====" % (ctype,filename) + print payload + print "==== end ctype=%s filename=%s" % (ctype, filename) diff --git a/testcases/cloud_user/instances/user-data-tests/part-handler.txt b/testcases/cloud_user/instances/user-data-tests/part-handler.txt new file mode 100644 index 00000000..a6e66415 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/part-handler.txt @@ -0,0 +1,23 @@ +#part-handler +# vi: syntax=python ts=4 + +def list_types(): + # return a list of mime-types that are handled by this module + return(["text/plain", "text/go-cubs-go"]) + +def handle_part(data,ctype,filename,payload): + # data: the cloudinit object + # ctype: '__begin__', '__end__', or the specific mime-type of the part + # filename: the filename for the part, or dynamically generated part if + # no filename is given attribute is present + # payload: the content of the part (empty for begin or end) + if ctype == "__begin__": + print "my handler is beginning" + return + if ctype == "__end__": + print "my handler is ending" + return + + print "==== received ctype=%s filename=%s ====" % (ctype,filename) + print payload + print "==== end ctype=%s filename=%s" % (ctype, filename) diff --git a/testcases/cloud_user/instances/user-data-tests/plain-ignored.txt b/testcases/cloud_user/instances/user-data-tests/plain-ignored.txt new file mode 100644 index 00000000..fb2b59dc --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/plain-ignored.txt @@ -0,0 +1,2 @@ +#ignored +Nothing will be done with this part by the UserDataHandler diff --git a/testcases/cloud_user/instances/user-data-tests/upstart-cloud-config.txt b/testcases/cloud_user/instances/user-data-tests/upstart-cloud-config.txt new file mode 100644 index 00000000..1fcec34d --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/upstart-cloud-config.txt @@ -0,0 +1,12 @@ +#upstart-job +description "My test job" + +start on cloud-config +console output +task + +script +echo "====BEGIN=======" +echo "HELLO WORLD: $UPSTART_JOB" +echo "=====END========" +end script diff --git a/testcases/cloud_user/instances/user-data-tests/upstart-rclocal.txt b/testcases/cloud_user/instances/user-data-tests/upstart-rclocal.txt new file mode 100644 index 00000000..5cd049a9 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/upstart-rclocal.txt @@ -0,0 +1,12 @@ +#upstart-job +description "a test upstart job" + +start on stopped rc RUNLEVEL=[2345] +console output +task + +script +echo "====BEGIN=======" +echo "HELLO RC.LOCAL LIKE WORLD: $UPSTART_JOB" +echo "=====END========" +end script diff --git a/testcases/cloud_user/instances/user-data-tests/user-script.txt b/testcases/cloud_user/instances/user-data-tests/user-script.txt new file mode 100644 index 00000000..6a87cad5 --- /dev/null +++ b/testcases/cloud_user/instances/user-data-tests/user-script.txt @@ -0,0 +1,8 @@ +#!/bin/sh + +cat <udbO4Qzy>7!W41{+*h2YG}j}z3f4NYl= z4%v#X1})Pz0b4XAx=7x>Y0(A=&>hN%g;8|8~;JGo5 zeM?=bT@N9m*kq0dq9gh*?Y1R=k1Yms^o|?yU7@p@A zh&H)TU&>h$r55*0H58#9unNpl=(QTbLx&ZLt9p(SuPwrie>h;|;j{Yo{t8ci#+WYy KAyUhv0RR9duza@w literal 0 HcmV?d00001 diff --git a/testcases/cloud_user/instances/userdatatest.py b/testcases/cloud_user/instances/userdatatest.py new file mode 100755 index 00000000..14352237 --- /dev/null +++ b/testcases/cloud_user/instances/userdatatest.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# +# Description: This script encompasses test cases/modules concerning instance specific behavior +# regarding userdata. The test cases/modules that are executed can be +# found in the script under the "tests" list. + +import time +from concurrent.futures import ThreadPoolExecutor +import threading +from eucaops import Eucaops +from eutester.euinstance import EuInstance +from eutester.eutestcase import EutesterTestCase +from eucaops import EC2ops +import os +import re +import random +import StringIO +import difflib + + +class InstanceBasics(EutesterTestCase): + def __init__( self, name="InstanceBasics", credpath=None, region=None, config_file=None, password=None, emi=None, zone=None, + user_data=None, user_data_file=None, instance_user=None, **kwargs): + """ + EC2 API tests focused on instance store instances + + :param credpath: Path to directory containing eucarc file + :param region: EC2 Region to run testcase in + :param config_file: Configuration file path + :param password: SSH password for bare metal machines if config is passed and keys arent synced + :param emi: Image id to use for test + :param zone: Availability Zone to run test in + :param user_data: User Data to pass to instance + :param user_data_file: User Data file to pass to instance + :param instance_user: User to login to instance as + :param kwargs: Additional arguments + """ + super(InstanceBasics, self).__init__(name=name) + if region: + self.tester = EC2ops(credpath=credpath, region=region) + else: + self.tester = Eucaops(config_file=config_file, password=password, credpath=credpath) + self.instance_timeout = 480 + + ### Add and authorize a group for the instance + self.group = self.tester.add_group(group_name="group-" + str(time.time())) + self.tester.authorize_group_by_name(group_name=self.group.name) + self.tester.authorize_group_by_name(group_name=self.group.name, port=-1, protocol="icmp" ) + ### Generate a keypair for the instance + self.keypair = self.tester.add_keypair( "keypair-" + str(time.time())) + self.keypath = '%s/%s.pem' % (os.curdir, self.keypair.name) + if emi: + self.image = emi + else: + self.image = self.tester.get_emi(root_device_type="instance-store",not_location="loadbalancer") + if user_data_file: + self.user_data_file = user_data_file + self.user_data = None + elif user_data: + self.user_data = user_data + self.user_data_file = None + + self.address = None + self.volume = None + self.private_addressing = False + if not zone: + zones = self.tester.ec2.get_all_zones() + self.zone = random.choice(zones).name + else: + self.zone = zone + self.reservation = None + self.reservation_lock = threading.Lock() + self.run_instance_params = {'image': self.image, 'user_data': self.user_data, 'user_data_file': self.user_data_file, + 'username': instance_user, 'keypair': self.keypair.name, 'group': self.group.name,'zone': self.zone, + 'timeout': self.instance_timeout} + self.managed_network = True + + ### If I have access to the underlying infrastructure I can look + ### at the network mode and only run certain tests where it makes sense + if hasattr(self.tester,"service_manager"): + cc = self.tester.get_component_machines("cc")[0] + network_mode = cc.sys("cat " + self.tester.eucapath + "/etc/eucalyptus/eucalyptus.conf | grep MODE")[0] + if re.search("(SYSTEM|STATIC)", network_mode): + self.managed_network = False + + def set_reservation(self, reservation): + self.reservation_lock.acquire() + self.reservation = reservation + self.reservation_lock.release() + + def clean_method(self): + self.tester.cleanup_artifacts() + + def UserData(self): + """ + This case was developed to test the user-data service of an instance for consistency. + This case does a comparison of the user data passed in by the user-data argument to + the data supplied by the user-data service within the instance. Supported + user data formats can be found here: https://cloudinit.readthedocs.org/en/latest/topics/format.html + If this test fails, the test case will error out; logging the results. + """ + if not self.reservation: + reservation = self.tester.run_instance(**self.run_instance_params) + else: + reservation = self.reservation + for instance in reservation.instances: + """ + Test to see if user data value is a file; if its a file, convert to string then compare, + if not, do a string compare + """ + if self.user_data_file: + with open(self.user_data_file) as user_data_file: + user_data = user_data_file.read() + instance_user_data = StringIO.StringIO(instance.get_userdata()) + self.assertTrue(difflib.SequenceMatcher(None, instance_user_data.getvalue(), user_data), 'Incorrect User Data File') + elif self.user_data: + self.assertTrue(re.search(instance.get_userdata(), self.user_data), 'Incorrect User Data String') + + +if __name__ == "__main__": + testcase= EutesterTestCase(name='userdatatest') + testcase.setup_parser(description="Test the Eucalyptus EC2 instance store userdata functionality.") + testcase.get_args() + instancetestsuite= testcase.do_with_args(InstanceBasics) + + ### Either use the list of tests passed from config/command line to determine what subset of tests to run + list = testcase.args.tests or [ "UserData"] + ### Convert test suite methods to EutesterUnitTest objects + unit_list = [] + for test in list: + test = getattr(instancetestsuite,test) + unit_list.append(testcase.create_testunit_from_method(test)) + testcase.clean_method = instancetestsuite.clean_method + result = testcase.run_test_case_list(unit_list) + exit(result) + From 1de1bc8ebc5bd614f9971379ca3a18db5986e65e Mon Sep 17 00:00:00 2001 From: "Harold Spencer, Jr" Date: Sun, 5 Jan 2014 07:04:29 +0000 Subject: [PATCH 3/5] Updated UserData test in userdatatest.py to handle string tests better --- eutester/euinstance.py | 5 +++-- testcases/cloud_user/instances/userdatatest.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/eutester/euinstance.py b/eutester/euinstance.py index 790f1398..d1acc0c7 100644 --- a/eutester/euinstance.py +++ b/eutester/euinstance.py @@ -669,8 +669,9 @@ def get_metadata(self, element_path, prefix='latest/meta-data/', timeout=10, sta else: raise(se) - def get_userdata(self, element_path, prefix='latest/user-data/'): - """Return the lines of metadata from the element path provided""" + def get_userdata(self, prefix='latest/user-data/'): + """Return the userdata""" + element_path = "" return self.get_metadata(element_path, prefix) def set_block_device_prefix(self): diff --git a/testcases/cloud_user/instances/userdatatest.py b/testcases/cloud_user/instances/userdatatest.py index 14352237..c0af612c 100755 --- a/testcases/cloud_user/instances/userdatatest.py +++ b/testcases/cloud_user/instances/userdatatest.py @@ -115,7 +115,7 @@ def UserData(self): instance_user_data = StringIO.StringIO(instance.get_userdata()) self.assertTrue(difflib.SequenceMatcher(None, instance_user_data.getvalue(), user_data), 'Incorrect User Data File') elif self.user_data: - self.assertTrue(re.search(instance.get_userdata(), self.user_data), 'Incorrect User Data String') + self.assertEqual(instance.get_userdata()[0], self.user_data, 'Incorrect User Data String') if __name__ == "__main__": From e8dab5703e4d778f12d882c12c79c9269545cc5c Mon Sep 17 00:00:00 2001 From: "Harold Spencer, Jr" Date: Sun, 5 Jan 2014 07:07:37 +0000 Subject: [PATCH 4/5] Updated instancetest.py to use id_generator in UserData test --- .../cloud_user/instances/instancetest.py | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/testcases/cloud_user/instances/instancetest.py b/testcases/cloud_user/instances/instancetest.py index 61e12cfd..cf13b6b2 100755 --- a/testcases/cloud_user/instances/instancetest.py +++ b/testcases/cloud_user/instances/instancetest.py @@ -15,8 +15,6 @@ import os import re import random -import StringIO -import difflib class InstanceBasics(EutesterTestCase): @@ -58,9 +56,8 @@ def __init__( self, name="InstanceBasics", credpath=None, region=None, config_fi self.volume = None self.private_addressing = False if not user_data: - self.user_data_file = 'testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt' - else: - self.user_data_file = None + ### Set userdata string to 16K to test max string size for userdata + self.user_data = self.tester.id_generator(16000) if not zone: zones = self.tester.ec2.get_all_zones() self.zone = random.choice(zones).name @@ -68,8 +65,8 @@ def __init__( self, name="InstanceBasics", credpath=None, region=None, config_fi self.zone = zone self.reservation = None self.reservation_lock = threading.Lock() - self.run_instance_params = {'image': self.image, 'user_data': user_data, 'user_data_file': self.user_data_file, - 'username': instance_user, 'keypair': self.keypair.name, 'group': self.group.name, + self.run_instance_params = {'image': self.image, 'user_data': self.user_data, 'username': instance_user, + 'keypair': self.keypair.name, 'group': self.group.name, 'zone': self.zone, 'timeout': self.instance_timeout} self.managed_network = True @@ -245,27 +242,26 @@ def MetaData(self): return reservation def UserData(self): - """ - This case was developed to test the user-data service of an instance for consistency. - This case does a comparison of the user data passed in by the user-data argument to - the data supplied by the user-data service within the instance. Supported - user data formats can be found here: https://cloudinit.readthedocs.org/en/latest/topics/format.html - If this test fails, the test case will error out; logging the results. - The userdata tested is 16K string (maximum size of userdata string defined by AWS) - """ - if not self.reservation: - reservation = self.tester.run_instance(**self.run_instance_params) - else: - reservation = self.reservation - for instance in reservation.instances: - """ - For aesthetics, the user data value is a 16K file thats converted to string then compare, - """ - if self.user_data_file: - with open(self.user_data_file) as user_data_file: - user_data = user_data_file.read() - instance_user_data = StringIO.StringIO(instance.get_userdata()) - self.assertTrue(difflib.SequenceMatcher(None, instance_user_data.getvalue(), user_data), 'Incorrect User Data File') + """ + This case was developed to test the user-data service of an instance for consistency. + This case does a comparison of the user data passed in by the user-data argument to + the data supplied by the user-data service within the instance. Supported + user data formats can be found here: https://cloudinit.readthedocs.org/en/latest/topics/format.html + If this test fails, the test case will error out; logging the results. + The userdata tested is 16K string (maximum size of userdata string defined by AWS) + """ + if not self.reservation: + reservation = self.tester.run_instance(**self.run_instance_params) + else: + reservation = self.reservation + for instance in reservation.instances: + """ + For aesthetics, the user data value is a 16K file thats converted to string then compare, + """ + if self.user_data: + self.assertEqual(instance.get_userdata()[0], self.user_data, 'Incorrect User Data String') + self.set_reservation(reservation) + return reservation def DNSResolveCheck(self): """ From 9a1e9db97ec7497fbbf6ac75c6896f69180abed2 Mon Sep 17 00:00:00 2001 From: "Harold Spencer, Jr" Date: Sun, 5 Jan 2014 07:09:07 +0000 Subject: [PATCH 5/5] Removed userdata-max-size.txt --- .../cloud_user/instances/user-data-tests/userdata-max-size.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt diff --git a/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt b/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt deleted file mode 100644 index 51ad0f4f..00000000 --- a/testcases/cloud_user/instances/user-data-tests/userdata-max-size.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file