Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added testcase for Instance Profile Information Presented in Instance Metadata Service #228

Open
wants to merge 7 commits into
base: testing
Choose a base branch
from
14 changes: 14 additions & 0 deletions eucaops/ec2ops.py
Expand Up @@ -2387,6 +2387,8 @@ def run_instance(self,
max=1,
user_data=None,
private_addressing=False,
instance_profile_name=None,
instance_profile_arn=None,
username="root",
password=None,
is_reachable=True,
Expand All @@ -2404,6 +2406,8 @@ def run_instance(self,
:param max: Maxiumum instances to launch, default 1
:param user_data: User-data string to pass to instance
:param private_addressing: Runs an instance with only private IP address
:param instance_profile_name: name of the IAM (Euare) profile to associate with the instances
:param instance_profile_arn: ARN of the IAM (Euare) instance profile to associate with the instances
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just add 1 param which is the name, and then translate to the arn if necessary?

:param username: username to use when connecting via ssh
:param password: password to use when connecting via ssh
:param is_reachable: Instance can be reached on its public IP (Default=True)
Expand Down Expand Up @@ -2438,6 +2442,7 @@ def run_instance(self,
self.debug( "Attempting to run "+ str(image.root_device_type) +" image " + str(image) + " in group " + str(group))
reservation = image.run(key_name=keypair,security_groups=[group],instance_type=type, placement=zone,
min_count=min, max_count=max, user_data=user_data, addressing_type=addressing_type,
instance_profile_name=instance_profile_name, instance_profile_arn=instance_profile_arn,
monitoring_enabled=enabled)
self.test_resources["reservations"].append(reservation)

Expand Down Expand Up @@ -2510,6 +2515,8 @@ def run_image(self,
block_device_map=None,
user_data=None,
private_addressing=False,
instance_profile_name=None,
instance_profile_arn=None,
username="root",
password=None,
auto_connect=True,
Expand All @@ -2527,6 +2534,8 @@ def run_image(self,
:param max: max amount of instances to try to run
:param user_data: user_data to run instances with
:param private_addressing: boolean to run instances without public ips
:param instance_profile_name: name of the IAM (Euare) profile to associate with the instances
:param instance_profile_arn: ARN of the IAM (Euare) instance profile to associate with the instances
:param username: username for connecting ssh to instances
:param password: password for connnecting ssh to instances
:param auto_connect: boolean flag whether or not ssh connections should be automatically attempted
Expand Down Expand Up @@ -2568,6 +2577,7 @@ def run_image(self,
cmdstart=time.time()
reservation = image.run(key_name=keypair,security_groups=[group],instance_type=type, placement=zone,
min_count=min, max_count=max, user_data=user_data, addressing_type=addressing_type,
instance_profile_name=instance_profile_name, instance_profile_arn=instance_profile_arn,
block_device_map=block_device_map)
self.test_resources["reservations"].append(reservation)

Expand All @@ -2589,6 +2599,8 @@ def run_image(self,
password=password,
reservation=reservation,
private_addressing=private_addressing,
instance_profile_name=instance_profile_name,
instance_profile_arn=instance_profile_arn,
timeout=timeout,
cmdstart=cmdstart,
auto_connect=False
Expand All @@ -2601,6 +2613,8 @@ def run_image(self,
username = username,
password=password,
reservation = reservation,
instance_profile_name=instance_profile_name,
instance_profile_arn=instance_profile_arn,
private_addressing=private_addressing,
timeout=timeout,
cmdstart=cmdstart,
Expand Down
8 changes: 8 additions & 0 deletions eutester/eutestcase.py
Expand Up @@ -366,6 +366,8 @@ def setup_parser(self,
color=True,
testlist=True,
userdata=True,
instance_profile_name=True,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to leave this config option out of the defaults for eutestcase as few tests will leverage this for now. If we see a large uptick in testcases lets add it.

instance_profile_arn=True,
instance_user=True,
stdout_log_level=True,
logfile_level=True,
Expand Down Expand Up @@ -479,6 +481,12 @@ 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 instance_profile_name:
parser.add_argument('--instance-profile-name',
help="The name of the IAM Instance Profile (IIP) to associate with the instances.", default=None)
if instance_profile_arn:
parser.add_argument('--instance-profile-arn',
help="The Amazon Resource Name (ARN) of the IAM instance profile to associate with the instances", default=None)
if instance_user:
parser.add_argument('--instance-user',
help="Username used for ssh login. Default:'root'", default='root')
Expand Down
166 changes: 166 additions & 0 deletions testcases/cloud_user/instances/instanceprofiletest.py
@@ -0,0 +1,166 @@
#!/usr/bin/env python
#
#
# Description: This script encompasses test cases/modules concerning instance specific behavior and
# features regarding the metadata service with regards to IAM roles and instance profiles in Eucalyptus.
# 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 json

class InstanceBasics(EutesterTestCase):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename the class to something that hasnt been used yet.

def __init__( self, name="InstanceBasics", credpath=None, region=None, config_file=None, password=None, emi=None, zone=None,
user_data=None, instance_user=None, instance_profile_name=None, instance_profile_arn=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 instance_profile_name: Instance Profile name to pass to instance
:param instance_profile_arn: Instance Profile ARN 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")
self.address = None
self.volume = None
self.private_addressing = False
if instance_profile_name:
self.instance_profile_name = instance_profile_name
self.instance_profile_arn = None
elif instance_profile_arn:
self.instance_profile_arn = instance_profile_arn
self.instance_profile_name = None
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': user_data, 'username': instance_user,
'keypair': self.keypair.name, 'group': self.group.name,'zone': self.zone,
'instance_profile_name': self.instance_profile_name,
'instance_profile_arn': self.instance_profile_arn, '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 InstanceProfileChecks(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since most of this code is the same as instancetest, can we just add this method there?

"""
This case was developed to test the metadata service regarding instance profile of an instance for consistency.
The following meta-data attributes are tested:
- iam/security-credentials/<role-name>
- iam/info/instance-profile-arn
- iam/info/instance-profile-id
- iam/info/last-updated-date
If any of these tests fail, 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:
# Check to see if instance profile ARN/Name matches metadata instance-profile-arn
if self.instance_profile_name:
self.assertTrue(re.search(self.instance_profile_name,
instance.get_metadata("iam/info/instance-profile-arn")[0]), 'Incorrect Instance Profile Name')
else:
self.assertTrue(re.match(instance.get_metadata("iam/info/instance-profile-arn")[0],
self.instance_profile_arn), 'Incorrect Instance Profile ARN')
# Check to see if instance profile ARN is at least 20 characters and at a maximum 2048 characters
# based on AWS IAM InstanceProfile data type definition
self.assertTrue(len(instance.get_metadata("iam/info/instance-profile-arn")[0]) >= 20, 'Instance Profile ARN is less than 20 characters')
self.assertTrue(len(instance.get_metadata("iam/info/instance-profile-arn")[0]) <= 2048, 'Instance Profile ARN is greater than 2048 characters')
# Check to see if instance profile ID is at least 16 characters and at a maximum 32 characters
# based on AWS IAM InstanceProfile data type definition
self.assertTrue(len(instance.get_metadata("iam/info/instance-profile-id")[0]) >= 16, 'Instance Profile ID is less than 16 characters')
self.assertTrue(len(instance.get_metadata("iam/info/instance-profile-id")[0]) <= 32, 'Instance Profile ID is greater than 32 characters')
# Check date format of instance profile LastUpdated
self.assertTrue(re.match('(\d{4})[/.-](\d{2})[/.-](\d{2})T(\d{2})[/.:](\d{2})[/.:](\d{2})Z',instance.get_metadata("iam/info/last-updated-date")[0]),
'Incorrect date format for Instance Profile LastUpdated')
# Check to see if iam/security-credentials/<role-name> is available, then check contents
self.assertTrue(instance.get_metadata("iam/security-credentials/")[0], 'IAM Role Not Available in Metadata')
try:
role_name = instance.get_metadata("iam/security-credentials/")[0]
temp_creds = json.loads(''.join(instance.get_metadata("iam/security-credentials/%s"%role_name)))
except Exception, e:
self.tester.debug("Unable to access IAM Role temporary credentials:\n"+ str(e))
raise e
# Check date format of temporary credentials LastUpdated variable
self.assertTrue(re.match('(\d{4})[/.-](\d{2})[/.-](\d{2})T(\d{2})[/.:](\d{2})[/.:](\d{2})Z',temp_creds['LastUpdated'].encode('ascii')),
'Incorrect date format for temporary credentials LastUpdated variable')
# Check to see if AccessKeyId is at least 16 characters and at a maximum 32 characters
# based on AWS STS Credentials data type definition
self.assertTrue(len(temp_creds['AccessKeyId'].encode('ascii')) >= 16, 'AccessKeyId is less than 16 characters')
self.assertTrue(len(temp_creds['AccessKeyId'].encode('ascii')) <= 32, 'AccessKeyId is greater than 32 characters')
self.assertTrue(temp_creds['SecretAccessKey'].encode('ascii'), "SecretAccessKey does not exist in " + role_name + " temporary credentials")
self.assertTrue(temp_creds['Token'].encode('ascii'), "Token does not exist in " + role_name + " temporary credentials")
# Check date format of temporary credentials Expiration variable
self.assertTrue(re.match('(\d{4})[/.-](\d{2})[/.-](\d{2})T(\d{2})[/.:](\d{2})[/.:](\d{2})Z',temp_creds['Expiration'].encode('ascii')),
'Incorrect date format for temporary credentials Expiration variable')
self.set_reservation(reservation)
return reservation

if __name__ == "__main__":
testcase= EutesterTestCase(name='instanceprofiletest')
testcase.setup_parser(description="Test the Eucalyptus EC2 instance profile metadata 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 [ "InstanceProfileChecks"]
### 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)