diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index e14a60bf1..348c3f723 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -583,3 +583,19 @@ class InvalidParameterDependency(EC2ClientError):
param, param_needed
),
)
+
+
+class IncorrectStateIamProfileAssociationError(EC2ClientError):
+ def __init__(self, instance_id):
+ super(IncorrectStateIamProfileAssociationError, self).__init__(
+ "IncorrectState",
+ "There is an existing association for instance {0}".format(instance_id),
+ )
+
+
+class InvalidAssociationIDIamProfileAssociationError(EC2ClientError):
+ def __init__(self, association_id):
+ super(InvalidAssociationIDIamProfileAssociationError, self).__init__(
+ "InvalidAssociationID.NotFound",
+ "An invalid association-id of '{0}' was given".format(association_id),
+ )
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index 586f49dcf..9b5e692a7 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -99,6 +99,8 @@ from .exceptions import (
RulesPerSecurityGroupLimitExceededError,
TagLimitExceeded,
InvalidParameterDependency,
+ IncorrectStateIamProfileAssociationError,
+ InvalidAssociationIDIamProfileAssociationError,
)
from .utils import (
EC2_RESOURCE_TO_PREFIX,
@@ -136,6 +138,7 @@ from .utils import (
random_vpc_id,
random_vpc_cidr_association_id,
random_vpc_peering_connection_id,
+ random_iam_instance_profile_association_id,
generic_filter,
is_valid_resource_id,
get_prefix,
@@ -143,6 +146,8 @@ from .utils import (
is_valid_cidr,
filter_internet_gateways,
filter_reservations,
+ filter_iam_instance_profile_associations,
+ filter_iam_instance_profiles,
random_network_acl_id,
random_network_acl_subnet_association_id,
random_vpn_gateway_id,
@@ -674,6 +679,16 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
instance = reservation.instances[0]
for tag in properties.get("Tags", []):
instance.add_tag(tag["Key"], tag["Value"])
+
+ # Associating iam instance profile.
+ # TODO: Don't forget to implement replace_iam_instance_profile_association once update_from_cloudformation_json
+ # for ec2 instance will be implemented.
+ if properties.get("IamInstanceProfile"):
+ ec2_backend.associate_iam_instance_profile(
+ instance_id=instance.id,
+ iam_instance_profile_name=properties.get("IamInstanceProfile"),
+ )
+
return instance
@classmethod
@@ -759,6 +774,15 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
"Client.UserInitiatedShutdown",
)
+ # Disassociate iam instance profile if associated, otherwise iam_instance_profile_associations will
+ # be pointing to None.
+ if self.ec2_backend.iam_instance_profile_associations.get(self.id):
+ self.ec2_backend.disassociate_iam_instance_profile(
+ association_id=self.ec2_backend.iam_instance_profile_associations[
+ self.id
+ ].id
+ )
+
def reboot(self, *args, **kwargs):
self._state.name = "running"
self._state.code = 16
@@ -5868,6 +5892,121 @@ class LaunchTemplateBackend(object):
return generic_filter(filters, templates)
+class IamInstanceProfileAssociation(CloudFormationModel):
+ def __init__(self, ec2_backend, association_id, instance, iam_instance_profile):
+ self.ec2_backend = ec2_backend
+ self.id = association_id
+ self.instance = instance
+ self.iam_instance_profile = iam_instance_profile
+ self.state = "associated"
+
+
+class IamInstanceProfileAssociationBackend(object):
+ def __init__(self):
+ self.iam_instance_profile_associations = {}
+ super(IamInstanceProfileAssociationBackend, self).__init__()
+
+ def associate_iam_instance_profile(
+ self,
+ instance_id,
+ iam_instance_profile_name=None,
+ iam_instance_profile_arn=None,
+ ):
+ iam_association_id = random_iam_instance_profile_association_id()
+
+ instance_profile = filter_iam_instance_profiles(
+ iam_instance_profile_arn, iam_instance_profile_name
+ )
+
+ if instance_id in self.iam_instance_profile_associations.keys():
+ raise IncorrectStateIamProfileAssociationError(instance_id)
+
+ iam_instance_profile_associations = IamInstanceProfileAssociation(
+ self,
+ iam_association_id,
+ self.get_instance(instance_id) if instance_id else None,
+ instance_profile,
+ )
+ # Regarding to AWS there can be only one association with ec2.
+ self.iam_instance_profile_associations[
+ instance_id
+ ] = iam_instance_profile_associations
+ return iam_instance_profile_associations
+
+ def describe_iam_instance_profile_associations(
+ self, association_ids, filters=None, max_results=100, next_token=None
+ ):
+ associations_list = []
+ if association_ids:
+ for association in self.iam_instance_profile_associations.values():
+ if association.id in association_ids:
+ associations_list.append(association)
+ else:
+ # That's mean that no association id were given. Showing all.
+ associations_list.extend(self.iam_instance_profile_associations.values())
+
+ associations_list = filter_iam_instance_profile_associations(
+ associations_list, filters
+ )
+
+ starting_point = int(next_token or 0)
+ ending_point = starting_point + int(max_results or 100)
+ associations_page = associations_list[starting_point:ending_point]
+ new_next_token = (
+ str(ending_point) if ending_point < len(associations_list) else None
+ )
+
+ return associations_page, new_next_token
+
+ def disassociate_iam_instance_profile(self, association_id):
+ iam_instance_profile_associations = None
+ for association_key in self.iam_instance_profile_associations.keys():
+ if (
+ self.iam_instance_profile_associations[association_key].id
+ == association_id
+ ):
+ iam_instance_profile_associations = self.iam_instance_profile_associations[
+ association_key
+ ]
+ del self.iam_instance_profile_associations[association_key]
+ # Deleting once and avoiding `RuntimeError: dictionary changed size during iteration`
+ break
+
+ if not iam_instance_profile_associations:
+ raise InvalidAssociationIDIamProfileAssociationError(association_id)
+
+ return iam_instance_profile_associations
+
+ def replace_iam_instance_profile_association(
+ self,
+ association_id,
+ iam_instance_profile_name=None,
+ iam_instance_profile_arn=None,
+ ):
+ instance_profile = filter_iam_instance_profiles(
+ iam_instance_profile_arn, iam_instance_profile_name
+ )
+
+ iam_instance_profile_association = None
+ for association_key in self.iam_instance_profile_associations.keys():
+ if (
+ self.iam_instance_profile_associations[association_key].id
+ == association_id
+ ):
+ self.iam_instance_profile_associations[
+ association_key
+ ].iam_instance_profile = instance_profile
+ iam_instance_profile_association = self.iam_instance_profile_associations[
+ association_key
+ ]
+ break
+
+ if not iam_instance_profile_association:
+ raise InvalidAssociationIDIamProfileAssociationError(association_id)
+
+ return iam_instance_profile_association
+
+
class EC2Backend(
BaseBackend,
InstanceBackend,
@@ -5897,6 +6036,7 @@ class EC2Backend(
CustomerGatewayBackend,
NatGatewayBackend,
LaunchTemplateBackend,
+ IamInstanceProfileAssociationBackend,
):
def __init__(self, region_name):
self.region_name = region_name
@@ -5983,6 +6123,13 @@ class EC2Backend(
self.describe_vpn_connections(vpn_connection_ids=[resource_id])
elif resource_prefix == EC2_RESOURCE_TO_PREFIX["vpn-gateway"]:
self.get_vpn_gateway(vpn_gateway_id=resource_id)
+ elif (
+ resource_prefix
+ == EC2_RESOURCE_TO_PREFIX["iam-instance-profile-association"]
+ ):
+ self.describe_iam_instance_profile_associations(
+ association_ids=[resource_id]
+ )
return True
diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py
index 893a25e89..515ae1f31 100644
--- a/moto/ec2/responses/__init__.py
+++ b/moto/ec2/responses/__init__.py
@@ -34,6 +34,7 @@ from .vpc_peering_connections import VPCPeeringConnections
from .vpn_connections import VPNConnections
from .windows import Windows
from .nat_gateways import NatGateways
+from .iam_instance_profiles import IamInstanceProfiles
class EC2Response(
@@ -71,6 +72,7 @@ class EC2Response(
VPNConnections,
Windows,
NatGateways,
+ IamInstanceProfiles,
):
@property
def ec2_backend(self):
diff --git a/moto/ec2/responses/iam_instance_profiles.py b/moto/ec2/responses/iam_instance_profiles.py
new file mode 100644
index 000000000..3d2525ba7
--- /dev/null
+++ b/moto/ec2/responses/iam_instance_profiles.py
@@ -0,0 +1,89 @@
+from __future__ import unicode_literals
+from moto.core.responses import BaseResponse
+
+
+class IamInstanceProfiles(BaseResponse):
+ def associate_iam_instance_profile(self):
+ instance_id = self._get_param("InstanceId")
+ iam_instance_profile_name = self._get_param("IamInstanceProfile.Name")
+ iam_instance_profile_arn = self._get_param("IamInstanceProfile.Arn")
+ iam_association = self.ec2_backend.associate_iam_instance_profile(
+ instance_id, iam_instance_profile_name, iam_instance_profile_arn
+ )
+ template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE)
+ return template.render(iam_association=iam_association, state="associating")
+
+ def describe_iam_instance_profile_associations(self):
+ association_ids = self._get_multi_param("AssociationId")
+ filters = self._get_object_map("Filter")
+ max_items = self._get_param("MaxItems")
+ next_token = self._get_param("NextToken")
+ (
+ iam_associations,
+ next_token,
+ ) = self.ec2_backend.describe_iam_instance_profile_associations(
+ association_ids, filters, max_items, next_token
+ )
+ template = self.response_template(DESCRIBE_IAM_INSTANCE_PROFILE_RESPONSE)
+ return template.render(iam_associations=iam_associations, next_token=next_token)
+
+ def disassociate_iam_instance_profile(self):
+ association_id = self._get_param("AssociationId")
+ iam_association = self.ec2_backend.disassociate_iam_instance_profile(
+ association_id
+ )
+ template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE)
+ return template.render(iam_association=iam_association, state="disassociating")
+
+ def replace_iam_instance_profile_association(self):
+ association_id = self._get_param("AssociationId")
+ iam_instance_profile_name = self._get_param("IamInstanceProfile.Name")
+ iam_instance_profile_arn = self._get_param("IamInstanceProfile.Arn")
+ iam_association = self.ec2_backend.replace_iam_instance_profile_association(
+ association_id, iam_instance_profile_name, iam_instance_profile_arn
+ )
+ template = self.response_template(IAM_INSTANCE_PROFILE_RESPONSE)
+ return template.render(iam_association=iam_association, state="associating")
+
+
+# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_AssociateIamInstanceProfile.html
+IAM_INSTANCE_PROFILE_RESPONSE = """
+
+ e10deeaf-7cda-48e7-950b-example
+
+ {{ iam_association.id }}
+ {% if iam_association.iam_instance_profile %}
+
+ {{ iam_association.iam_instance_profile.arn }}
+ {{ iam_association.iam_instance_profile.id }}
+
+ {% endif %}
+ {{ iam_association.instance.id }}
+ {{ state }}
+
+
+"""
+
+
+# https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeIamInstanceProfileAssociations.html
+# Note: this API description page contains an error! Provided `iamInstanceProfileAssociations` doesn't work, you
+# should use `iamInstanceProfileAssociationSet` instead.
+DESCRIBE_IAM_INSTANCE_PROFILE_RESPONSE = """
+
+ 84c2d2a6-12dc-491f-a9ee-example
+ {% if next_token %}{{ next_token }}{% endif %}
+
+ {% for iam_association in iam_associations %}
+ -
+ {{ iam_association.id }}
+
+ {{ iam_association.iam_instance_profile.arn }}
+ {{ iam_association.iam_instance_profile.id }}
+
+ {{ iam_association.instance.id }}
+ {{ iam_association.state }}
+
+ {% endfor %}
+
+
+"""
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index e6763fec1..4a101f923 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from moto.core import ACCOUNT_ID
+from moto.iam import iam_backends
EC2_RESOURCE_TO_PREFIX = {
"customer-gateway": "cgw",
@@ -43,6 +44,7 @@ EC2_RESOURCE_TO_PREFIX = {
"vpc-peering-connection": "pcx",
"vpn-connection": "vpn",
"vpn-gateway": "vgw",
+ "iam-instance-profile-association": "iip-assoc",
}
@@ -171,6 +173,10 @@ def random_launch_template_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX["launch-template"], size=17)
+def random_iam_instance_profile_association_id():
+ return random_id(prefix=EC2_RESOURCE_TO_PREFIX["iam-instance-profile-association"])
+
+
def random_public_ip():
return "54.214.{0}.{1}".format(random.choice(range(255)), random.choice(range(255)))
@@ -597,3 +603,47 @@ def rsa_public_key_fingerprint(rsa_public_key):
fingerprint_hex = hashlib.md5(key_data).hexdigest()
fingerprint = re.sub(r"([a-f0-9]{2})(?!$)", r"\1:", fingerprint_hex)
return fingerprint
+
+
+def filter_iam_instance_profile_associations(iam_instance_associations, filter_dict):
+ if not filter_dict:
+ return iam_instance_associations
+ result = []
+ for iam_instance_association in iam_instance_associations:
+ filter_passed = True
+ if filter_dict.get("instance-id"):
+ if (
+ iam_instance_association.instance.id
+ not in filter_dict.get("instance-id").values()
+ ):
+ filter_passed = False
+ if filter_dict.get("state"):
+ if iam_instance_association.state not in filter_dict.get("state").values():
+ filter_passed = False
+ if filter_passed:
+ result.append(iam_instance_association)
+ return result
+
+
+def filter_iam_instance_profiles(iam_instance_profile_arn, iam_instance_profile_name):
+ instance_profile = None
+ instance_profile_by_name = None
+ instance_profile_by_arn = None
+ if iam_instance_profile_name:
+ instance_profile_by_name = iam_backends["global"].get_instance_profile(
+ iam_instance_profile_name
+ )
+ instance_profile = instance_profile_by_name
+ if iam_instance_profile_arn:
+ instance_profile_by_arn = iam_backends["global"].get_instance_profile_by_arn(
+ iam_instance_profile_arn
+ )
+ instance_profile = instance_profile_by_arn
+ # We would prefer instance profile that we found by arn
+ if iam_instance_profile_arn and iam_instance_profile_name:
+ if instance_profile_by_name == instance_profile_by_arn:
+ instance_profile = instance_profile_by_arn
+ else:
+ instance_profile = None
+
+ return instance_profile
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 76b824d60..ac8402e57 100755
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -1852,6 +1852,13 @@ class IAMBackend(BaseBackend):
"Instance profile {0} not found".format(profile_name)
)
+ def get_instance_profile_by_arn(self, profile_arn):
+ for profile in self.get_instance_profiles():
+ if profile.arn == profile_arn:
+ return profile
+
+ raise IAMNotFoundException("Instance profile {0} not found".format(profile_arn))
+
def get_instance_profiles(self):
return self.instance_profiles.values()
diff --git a/tests/test_ec2/test_iam_instance_profile_associations.py b/tests/test_ec2/test_iam_instance_profile_associations.py
new file mode 100644
index 000000000..6a7dcad30
--- /dev/null
+++ b/tests/test_ec2/test_iam_instance_profile_associations.py
@@ -0,0 +1,345 @@
+from __future__ import unicode_literals
+
+# Ensure 'pytest.raises' context manager support for Python 2.6
+import pytest
+
+import time
+import json
+import boto3
+from botocore.exceptions import ClientError
+import sure # noqa
+
+from moto import mock_ec2, mock_iam, mock_cloudformation
+
+
+def quick_instance_creation():
+ image_id = "ami-1234abcd"
+ conn_ec2 = boto3.resource("ec2", "us-east-1")
+ test_instance = conn_ec2.create_instances(ImageId=image_id, MinCount=1, MaxCount=1)
+ # We only need instance id for this tests
+ return test_instance[0].id
+
+
+def quick_instance_profile_creation(name):
+ conn_iam = boto3.resource("iam", "us-east-1")
+ test_instance_profile = conn_iam.create_instance_profile(
+ InstanceProfileName=name, Path="/"
+ )
+ return test_instance_profile.arn, test_instance_profile.name
+
+
+@mock_ec2
+@mock_iam
+def test_associate():
+ client = boto3.client("ec2", region_name="us-east-1")
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile"
+ )
+
+ association = client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+ association["IamInstanceProfileAssociation"]["InstanceId"].should.equal(instance_id)
+ association["IamInstanceProfileAssociation"]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(instance_profile_arn)
+ association["IamInstanceProfileAssociation"]["State"].should.equal("associating")
+
+
+@mock_ec2
+@mock_iam
+def test_invalid_associate():
+ client = boto3.client("ec2", region_name="us-east-1")
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile"
+ )
+
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+
+ # Duplicate
+ with pytest.raises(ClientError) as ex:
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+ ex.value.response["Error"]["Code"].should.equal("IncorrectState")
+ ex.value.response["Error"]["Message"].should.contain(
+ "There is an existing association for"
+ )
+
+ # Wrong instance profile
+ with pytest.raises(ClientError) as ex:
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={"Arn": "fake", "Name": "fake"}, InstanceId=instance_id,
+ )
+ ex.value.response["Error"]["Code"].should.equal("NoSuchEntity")
+ ex.value.response["Error"]["Message"].should.contain("not found")
+
+ # Wrong instance id
+ with pytest.raises(ClientError) as ex:
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId="fake",
+ )
+ ex.value.response["Error"]["Code"].should.equal("InvalidInstanceID.NotFound")
+ ex.value.response["Error"]["Message"].should.contain("does not exist")
+
+
+@mock_ec2
+@mock_iam
+def test_describe():
+ client = boto3.client("ec2", region_name="us-east-1")
+
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile"
+ )
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+ associations = client.describe_iam_instance_profile_associations()
+ associations["IamInstanceProfileAssociations"].should.have.length_of(1)
+ associations["IamInstanceProfileAssociations"][0]["InstanceId"].should.equal(
+ instance_id
+ )
+ associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(instance_profile_arn)
+ associations["IamInstanceProfileAssociations"][0]["State"].should.equal(
+ "associated"
+ )
+
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile1"
+ )
+ client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+
+ next_test_associations = client.describe_iam_instance_profile_associations()
+ next_test_associations["IamInstanceProfileAssociations"].should.have.length_of(2)
+
+ associations = client.describe_iam_instance_profile_associations(
+ AssociationIds=[
+ next_test_associations["IamInstanceProfileAssociations"][0][
+ "AssociationId"
+ ],
+ ]
+ )
+ associations["IamInstanceProfileAssociations"].should.have.length_of(1)
+ associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(
+ next_test_associations["IamInstanceProfileAssociations"][0][
+ "IamInstanceProfile"
+ ]["Arn"]
+ )
+
+ associations = client.describe_iam_instance_profile_associations(
+ Filters=[
+ {
+ "Name": "instance-id",
+ "Values": [
+ next_test_associations["IamInstanceProfileAssociations"][0][
+ "InstanceId"
+ ],
+ ],
+ },
+ {"Name": "state", "Values": ["associated"]},
+ ]
+ )
+ associations["IamInstanceProfileAssociations"].should.have.length_of(1)
+ associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(
+ next_test_associations["IamInstanceProfileAssociations"][0][
+ "IamInstanceProfile"
+ ]["Arn"]
+ )
+
+
+@mock_ec2
+@mock_iam
+def test_replace():
+ client = boto3.client("ec2", region_name="us-east-1")
+ instance_id1 = quick_instance_creation()
+ instance_profile_arn1, instance_profile_name1 = quick_instance_profile_creation(
+ "test_profile1"
+ )
+ instance_profile_arn2, instance_profile_name2 = quick_instance_profile_creation(
+ "test_profile2"
+ )
+
+ association = client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn1,
+ "Name": instance_profile_name1,
+ },
+ InstanceId=instance_id1,
+ )
+
+ association = client.replace_iam_instance_profile_association(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn2,
+ "Name": instance_profile_name2,
+ },
+ AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"],
+ )
+
+ association["IamInstanceProfileAssociation"]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(instance_profile_arn2)
+ association["IamInstanceProfileAssociation"]["State"].should.equal("associating")
+
+
+@mock_ec2
+@mock_iam
+def test_invalid_replace():
+ client = boto3.client("ec2", region_name="us-east-1")
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile"
+ )
+ instance_profile_arn2, instance_profile_name2 = quick_instance_profile_creation(
+ "test_profile2"
+ )
+
+ association = client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+
+ # Wrong id
+ with pytest.raises(ClientError) as ex:
+ client.replace_iam_instance_profile_association(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn2,
+ "Name": instance_profile_name2,
+ },
+ AssociationId="fake",
+ )
+ ex.value.response["Error"]["Code"].should.equal("InvalidAssociationID.NotFound")
+ ex.value.response["Error"]["Message"].should.contain("An invalid association-id of")
+
+ # Wrong instance profile
+ with pytest.raises(ClientError) as ex:
+ client.replace_iam_instance_profile_association(
+ IamInstanceProfile={"Arn": "fake", "Name": "fake",},
+ AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"],
+ )
+ ex.value.response["Error"]["Code"].should.equal("NoSuchEntity")
+ ex.value.response["Error"]["Message"].should.contain("not found")
+
+
+@mock_ec2
+@mock_iam
+def test_disassociate():
+ client = boto3.client("ec2", region_name="us-east-1")
+ instance_id = quick_instance_creation()
+ instance_profile_arn, instance_profile_name = quick_instance_profile_creation(
+ "test_profile"
+ )
+
+ association = client.associate_iam_instance_profile(
+ IamInstanceProfile={
+ "Arn": instance_profile_arn,
+ "Name": instance_profile_name,
+ },
+ InstanceId=instance_id,
+ )
+
+ associations = client.describe_iam_instance_profile_associations()
+ associations["IamInstanceProfileAssociations"].should.have.length_of(1)
+
+ disassociation = client.disassociate_iam_instance_profile(
+ AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"],
+ )
+
+ disassociation["IamInstanceProfileAssociation"]["IamInstanceProfile"][
+ "Arn"
+ ].should.equal(instance_profile_arn)
+ disassociation["IamInstanceProfileAssociation"]["State"].should.equal(
+ "disassociating"
+ )
+
+ associations = client.describe_iam_instance_profile_associations()
+ associations["IamInstanceProfileAssociations"].should.have.length_of(0)
+
+
+@mock_ec2
+@mock_iam
+def test_invalid_disassociate():
+ client = boto3.client("ec2", region_name="us-east-1")
+
+ # Wrong id
+ with pytest.raises(ClientError) as ex:
+ client.disassociate_iam_instance_profile(AssociationId="fake",)
+ ex.value.response["Error"]["Code"].should.equal("InvalidAssociationID.NotFound")
+ ex.value.response["Error"]["Message"].should.contain("An invalid association-id of")
+
+
+@mock_ec2
+@mock_cloudformation
+def test_cloudformation():
+ dummy_template_json = {
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Resources": {
+ "InstanceProfile": {
+ "Type": "AWS::IAM::InstanceProfile",
+ "Properties": {"Path": "/", "Roles": []},
+ },
+ "Ec2Instance": {
+ "Type": "AWS::EC2::Instance",
+ "Properties": {
+ "IamInstanceProfile": {"Ref": "InstanceProfile"},
+ "KeyName": "mykey1",
+ "ImageId": "ami-7a11e213",
+ },
+ },
+ },
+ }
+
+ client = boto3.client("ec2", region_name="us-east-1")
+ cf_conn = boto3.client("cloudformation", region_name="us-east-1")
+ cf_conn.create_stack(
+ StackName="test_stack", TemplateBody=json.dumps(dummy_template_json)
+ )
+ associations = client.describe_iam_instance_profile_associations()
+ associations["IamInstanceProfileAssociations"].should.have.length_of(1)
+ associations["IamInstanceProfileAssociations"][0]["IamInstanceProfile"][
+ "Arn"
+ ].should.contain("test_stack")
+
+ cf_conn.delete_stack(StackName="test_stack")
+ associations = client.describe_iam_instance_profile_associations()
+ associations["IamInstanceProfileAssociations"].should.have.length_of(0)