diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 8a958b38f..20ee1a2de 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -131,6 +131,10 @@ class FakeLaunchConfiguration(CloudFormationModel): ebs_optimized, associate_public_ip_address, block_device_mapping_dict, + region_name, + metadata_options, + classic_link_vpc_id, + classic_link_vpc_security_groups, ): self.name = name self.image_id = image_id @@ -146,6 +150,10 @@ class FakeLaunchConfiguration(CloudFormationModel): self.ebs_optimized = ebs_optimized self.associate_public_ip_address = associate_public_ip_address self.block_device_mapping_dict = block_device_mapping_dict + self.metadata_options = metadata_options + self.classic_link_vpc_id = classic_link_vpc_id + self.classic_link_vpc_security_groups = classic_link_vpc_security_groups + self.arn = f"arn:aws:autoscaling:{region_name}:{ACCOUNT_ID}:launchConfiguration:9dbbbf87-6141-428a-a409-0752edbe6cad:launchConfigurationName/{self.name}" @classmethod def create_from_instance(cls, name, instance, backend): @@ -253,6 +261,8 @@ class FakeLaunchConfiguration(CloudFormationModel): mount_point = mapping.get("DeviceName") if mapping.get("VirtualName") and "ephemeral" in mapping.get("VirtualName"): block_type.ephemeral_name = mapping.get("VirtualName") + elif mapping.get("NoDevice", "false") == "true": + block_type.no_device = "true" else: ebs = mapping.get("Ebs", {}) block_type.volume_type = ebs.get("VolumeType") @@ -260,6 +270,8 @@ class FakeLaunchConfiguration(CloudFormationModel): block_type.delete_on_termination = ebs.get("DeleteOnTermination") block_type.size = ebs.get("VolumeSize") block_type.iops = ebs.get("Iops") + block_type.throughput = ebs.get("Throughput") + block_type.encrypted = ebs.get("Encrypted") block_device_map[mount_point] = block_type return block_device_map @@ -678,6 +690,9 @@ class AutoScalingBackend(BaseBackend): associate_public_ip_address, block_device_mappings, instance_id=None, + metadata_options=None, + classic_link_vpc_id=None, + classic_link_vpc_security_groups=None, ): valid_requests = [ instance_id is not None, @@ -705,6 +720,10 @@ class AutoScalingBackend(BaseBackend): ebs_optimized=ebs_optimized, associate_public_ip_address=associate_public_ip_address, block_device_mapping_dict=block_device_mappings, + region_name=self.region, + metadata_options=metadata_options, + classic_link_vpc_id=classic_link_vpc_id, + classic_link_vpc_security_groups=classic_link_vpc_security_groups, ) self.launch_configurations[name] = launch_configuration return launch_configuration diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py index 39678a140..cdfcc3444 100644 --- a/moto/autoscaling/responses.py +++ b/moto/autoscaling/responses.py @@ -37,6 +37,9 @@ class AutoScalingResponse(BaseResponse): associate_public_ip_address=params.get("AssociatePublicIpAddress"), block_device_mappings=params.get("BlockDeviceMappings"), instance_id=params.get("InstanceId"), + metadata_options=params.get("MetadataOptions"), + classic_link_vpc_id=params.get("ClassicLinkVPCId"), + classic_link_vpc_security_groups=params.get("ClassicLinkVPCSecurityGroups"), ) template = self.response_template(CREATE_LAUNCH_CONFIGURATION_TEMPLATE) return template.render() @@ -449,13 +452,27 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """ {{ 'true' if launch_configuration.associate_public_ip_address else 'false' }} + {% if launch_configuration.classic_link_vpc_id %} + {{ launch_configuration.classic_link_vpc_id }} + {% endif %} + {% if launch_configuration.classic_link_vpc_security_groups %} + + {% for sg in launch_configuration.classic_link_vpc_security_groups %} + {{ sg }} + {% endfor %} + + {% endif %} {% for security_group in launch_configuration.security_groups %} {{ security_group }} {% endfor %} 2013-01-21T23:04:42.200Z + {% if launch_configuration.kernel_id %} {{ launch_configuration.kernel_id }} + {% else %} + + {% endif %} {% if launch_configuration.instance_profile_name %} {{ launch_configuration.instance_profile_name }} {% endif %} @@ -466,7 +483,7 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """ {% endif %} {{ launch_configuration.instance_type }} - arn:aws:autoscaling:us-east-1:803981987763:launchConfiguration:9dbbbf87-6141-428a-a409-0752edbe6cad:launchConfigurationName/{{ launch_configuration.name }} + {{ launch_configuration.arn }} {% if launch_configuration.block_device_mappings %} {% for mount_point, mapping in launch_configuration.block_device_mappings.items() %} @@ -474,6 +491,8 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """{{ mount_point }} {% if mapping.ephemeral_name %} {{ mapping.ephemeral_name }} + {% elif mapping.no_device %} + true {% else %} {% if mapping.snapshot_id %} @@ -485,8 +504,18 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """{{ mapping.iops }} {% endif %} + {% if mapping.throughput %} + {{ mapping.throughput }} + {% endif %} + {% if mapping.delete_on_termination is not none %} {{ mapping.delete_on_termination }} + {% endif %} + {% if mapping.volume_type %} {{ mapping.volume_type }} + {% endif %} + {% if mapping.encrypted %} + {{ mapping.encrypted }} + {% endif %} {% endif %} @@ -501,7 +530,11 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """ {% endif %} + {% if launch_configuration.ramdisk_id %} {{ launch_configuration.ramdisk_id }} + {% else %} + + {% endif %} {{ launch_configuration.ebs_optimized }} {{ launch_configuration.instance_monitoring_enabled }} @@ -509,6 +542,13 @@ DESCRIBE_LAUNCH_CONFIGURATIONS_TEMPLATE = """{{ launch_configuration.spot_price }} {% endif %} + {% if launch_configuration.metadata_options %} + + {{ launch_configuration.metadata_options.get("HttpTokens") }} + {{ launch_configuration.metadata_options.get("HttpPutResponseHopLimit") }} + {{ launch_configuration.metadata_options.get("HttpEndpoint") }} + + {% endif %} {% endfor %} diff --git a/moto/ec2/_models/amis.py b/moto/ec2/_models/amis.py index ad1e253f5..6399c7f5b 100644 --- a/moto/ec2/_models/amis.py +++ b/moto/ec2/_models/amis.py @@ -8,6 +8,7 @@ from ..exceptions import ( InvalidAMIAttributeItemValueError, MalformedAMIIdError, InvalidTaggableResourceType, + UnvailableAMIIdError, ) from .core import TaggedEC2Resource from ..utils import ( @@ -146,6 +147,7 @@ class AmiBackend(object): def __init__(self): self.amis = {} + self.deleted_amis = list() self._load_amis() super().__init__() @@ -216,6 +218,7 @@ class AmiBackend(object): if len(ami_ids): # boto3 seems to default to just searching based on ami ids if that parameter is passed # and if no images are found, it raises an errors + # Note that we can search for images that have been previously deleted, without raising any errors malformed_ami_ids = [ ami_id for ami_id in ami_ids if not ami_id.startswith("ami-") ] @@ -223,7 +226,10 @@ class AmiBackend(object): raise MalformedAMIIdError(malformed_ami_ids) images = [ami for ami in images if ami.id in ami_ids] - if len(images) == 0: + deleted_images = [ + ami_id for ami_id in ami_ids if ami_id in self.deleted_amis + ] + if len(images) + len(deleted_images) == 0: raise InvalidAMIIdError(ami_ids) else: # Limit images by launch permissions @@ -257,7 +263,10 @@ class AmiBackend(object): def deregister_image(self, ami_id): if ami_id in self.amis: self.amis.pop(ami_id) + self.deleted_amis.append(ami_id) return True + elif ami_id in self.deleted_amis: + raise UnvailableAMIIdError(ami_id) raise InvalidAMIIdError(ami_id) def get_launch_permission_groups(self, ami_id): diff --git a/moto/ec2/_models/instances.py b/moto/ec2/_models/instances.py index cadfd42bc..406bf3b14 100644 --- a/moto/ec2/_models/instances.py +++ b/moto/ec2/_models/instances.py @@ -557,6 +557,8 @@ class InstanceBackend(object): new_reservation.instances.append(new_instance) new_instance.add_tags(instance_tags) block_device_mappings = None + if "block_device_mappings" not in kwargs: + new_instance.setup_defaults() if "block_device_mappings" in kwargs: block_device_mappings = kwargs["block_device_mappings"] elif kwargs.get("launch_template"): @@ -590,8 +592,6 @@ class InstanceBackend(object): kms_key_id, volume_type=volume_type, ) - else: - new_instance.setup_defaults() if kwargs.get("instance_market_options"): new_instance.lifecycle = "spot" # Tag all created volumes. diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 42980a9d8..65f00f132 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -258,6 +258,14 @@ class InvalidAMIIdError(EC2ClientError): ) +class UnvailableAMIIdError(EC2ClientError): + def __init__(self, ami_id): + super().__init__( + "InvalidAMIID.Unavailable", + "The image id '[{0}]' is no longer available".format(ami_id), + ) + + class InvalidAMIAttributeItemValueError(EC2ClientError): def __init__(self, attribute, value): super().__init__( diff --git a/moto/ec2/resources/amis.json b/moto/ec2/resources/amis.json index 40ccff92b..1fad1865a 100644 --- a/moto/ec2/resources/amis.json +++ b/moto/ec2/resources/amis.json @@ -661,5 +661,22 @@ "root_device_type": "ebs", "sriov": "simple", "virtualization_type": "hvm" + }, + { + "architecture": "x86_64", + "ami_id": "ami-04681a1dbd79675b6", + "image_location": "amazon/amzn2-ami-minimal-pv-2.0.20180810-x86_64-gp2", + "image_type": "machine", + "public": true, + "owner_id": "137112412989", + "platform": "Linux/UNIX", + "state": "available", + "description": "Example InstanceStore AMI used by AutoScaling tests", + "hypervisor": "xen", + "name": "amzn-ami-minimal-pv-2.0.20180810-x86_64-gp2", + "root_device_name": "/dev/xvda", + "root_device_type": "instance-store", + "sriov": "simple", + "virtualization_type": "hvm" } ] diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt index 97baa542f..abc483fdc 100644 --- a/tests/terraformtests/terraform-tests.success.txt +++ b/tests/terraformtests/terraform-tests.success.txt @@ -8,8 +8,11 @@ apigatewayv2: - TestAccAPIGatewayV2RouteResponse - TestAccAPIGatewayV2VPCLink autoscaling: - - TestAccAutoScalingGroupDataSource - TestAccAutoScalingAttachment + - TestAccAutoScalingGroupDataSource + - TestAccAutoScalingGroupTag + - TestAccAutoScalingLaunchConfigurationDataSource + - TestAccAutoScalingLaunchConfiguration_ batch: - TestAccBatchJobDefinition cloudtrail: diff --git a/tests/test_autoscaling/test_autoscaling.py b/tests/test_autoscaling/test_autoscaling.py index fa5aef0e0..e90e896dd 100644 --- a/tests/test_autoscaling/test_autoscaling.py +++ b/tests/test_autoscaling/test_autoscaling.py @@ -2810,5 +2810,9 @@ def test_create_template_with_block_device(): ) ec2_client = boto3.client("ec2", region_name="ap-southeast-2") volumes = ec2_client.describe_volumes()["Volumes"] - volumes[0]["VolumeType"].should.equal("gp3") - volumes[0]["Size"].should.equal(20) + # The standard root volume + volumes[0]["VolumeType"].should.equal("gp2") + volumes[0]["Size"].should.equal(8) + # Our Ebs-volume + volumes[1]["VolumeType"].should.equal("gp3") + volumes[1]["Size"].should.equal(20) diff --git a/tests/test_autoscaling/test_launch_configurations.py b/tests/test_autoscaling/test_launch_configurations.py index 4a84efc00..21dd5c4cd 100644 --- a/tests/test_autoscaling/test_launch_configurations.py +++ b/tests/test_autoscaling/test_launch_configurations.py @@ -99,7 +99,6 @@ def test_create_launch_configuration_with_block_device_mappings(): xvdp.should.have.key("Ebs") xvdp["Ebs"]["SnapshotId"].should.equal("snap-1234abcd") xvdp["Ebs"]["VolumeType"].should.equal("standard") - xvdp["Ebs"]["DeleteOnTermination"].should.equal(False) xvdb["VirtualName"].should.equal("ephemeral0") xvdb.shouldnt.have.key("Ebs") @@ -109,16 +108,32 @@ def test_create_launch_configuration_with_block_device_mappings(): def test_create_launch_configuration_additional_parameters(): client = boto3.client("autoscaling", region_name="us-east-1") client.create_launch_configuration( + ClassicLinkVPCId="vpc_id", + ClassicLinkVPCSecurityGroups=["classic_sg1"], LaunchConfigurationName="tester", ImageId=EXAMPLE_AMI_ID, InstanceType="t1.micro", EbsOptimized=True, AssociatePublicIpAddress=True, + MetadataOptions={ + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 123, + "HttpEndpoint": "disabled", + }, ) launch_config = client.describe_launch_configurations()["LaunchConfigurations"][0] + launch_config["ClassicLinkVPCId"].should.equal("vpc_id") + launch_config["ClassicLinkVPCSecurityGroups"].should.equal(["classic_sg1"]) launch_config["EbsOptimized"].should.equal(True) launch_config["AssociatePublicIpAddress"].should.equal(True) + launch_config["MetadataOptions"].should.equal( + { + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 123, + "HttpEndpoint": "disabled", + } + ) @mock_autoscaling @@ -287,7 +302,10 @@ def test_launch_config_with_block_device_mappings__volumes_are_created(): volumes = ec2_client.describe_volumes( Filters=[{"Name": "attachment.instance-id", "Values": [instance_id]}] )["Volumes"] - volumes.should.have.length_of(1) - volumes[0].should.have.key("Size").equals(10) + volumes.should.have.length_of(2) + volumes[0].should.have.key("Size").equals(8) volumes[0].should.have.key("Encrypted").equals(False) - volumes[0].should.have.key("VolumeType").equals("standard") + volumes[0].should.have.key("VolumeType").equals("gp2") + volumes[1].should.have.key("Size").equals(10) + volumes[1].should.have.key("Encrypted").equals(False) + volumes[1].should.have.key("VolumeType").equals("standard") diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 6108a7d16..422ac7115 100644 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -117,10 +117,41 @@ def test_ami_create_and_delete(): ec2.deregister_image(ImageId=image_id) ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) err = ex.value.response["Error"] + err["Code"].should.equal("InvalidAMIID.Unavailable") + ex.value.response["ResponseMetadata"]["RequestId"].should_not.equal(None) + + +@mock_ec2 +def test_deregister_image__unknown(): + ec2 = boto3.client("ec2", region_name="us-east-1") + + with pytest.raises(ClientError) as ex: + ec2.deregister_image(ImageId="ami-unknown-ami") + ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + err = ex.value.response["Error"] err["Code"].should.equal("InvalidAMIID.NotFound") ex.value.response["ResponseMetadata"]["RequestId"].should_not.equal(None) +@mock_ec2 +def test_deregister_image__and_describe(): + ec2 = boto3.client("ec2", region_name="us-east-1") + + reservation = ec2.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1) + instance = reservation["Instances"][0] + instance_id = instance["InstanceId"] + + image_id = ec2.create_image( + InstanceId=instance_id, Name="test-ami", Description="this is a test ami" + )["ImageId"] + + ec2.deregister_image(ImageId=image_id) + + # Searching for a deleted image ID should not throw an error + # It should simply not return this image + ec2.describe_images(ImageIds=[image_id])["Images"].should.have.length_of(0) + + @mock_ec2 def test_ami_copy_dryrun(): ec2 = boto3.client("ec2", region_name="us-west-1")