import datetime
from dataclasses import field
from dataclasses import make_dataclass
from typing import Any
from typing import Dict
from typing import List
from dict_tools.typing import Computed
__contracts__ = ["resource", "allow_sync_sls_name_and_name_tag"]
__reconcile_wait__ = {"static": {"wait_in_seconds": 5}}
TREQ = {
"present": {
"require": [
"aws.ec2.network_interface.present",
"aws.ec2.vpc.present",
"aws.ec2.subnet.present",
"aws.iam.role.present",
],
},
}
[docs]async def present(
hub,
ctx,
name: str,
image_id: str,
*,
resource_id: str = None,
# From DescribeInstances
instance_type: str = None,
ebs_optimized: bool = None,
kernel_id: str = None,
subnet_id: str = None,
security_group_ids: List[str] = None,
network_interfaces: List[
make_dataclass(
"NetworkInterfaceAttachment",
[
("device_index", int),
("network_interface_id", str, field(default=None)),
("network_card_index", int, field(default=None)),
("subnet_id", Computed[str], field(default=None)),
("public_ip_address", Computed[str], field(default=None)),
("private_ip_address", Computed[str], field(default=None)),
("mac_address", Computed[str], field(default=None)),
],
)
] = None,
monitoring_enabled: bool = None,
source_dest_check: bool = None,
running: bool = True,
private_ip_address: str = None,
owner_id: str = None,
# Placement Options
availability_zone: str = None,
affinity: str = None,
group_name: str = None,
partition_number: int = None,
host_id: str = None,
tenancy: str = None,
spread_domain: str = None,
host_resource_group_arn: str = None,
# From launchTemplate
user_data: str = None,
disable_api_termination: bool = None,
ram_disk_id: str = None,
tags: Dict[str, str] = None,
iam_instance_profile_arn: str = None,
instance_initiated_shutdown_behavior: str = None,
elastic_inference_accelerators: List[
make_dataclass(
"ElasticInferenceAccelerator",
[("Type", str), ("Count", int, field(default=None))],
)
] = None,
elastic_gpu_specifications: List[
make_dataclass("ElasticGpuSpecification", [("Type", str)])
] = None,
auto_recovery_enabled: bool = None,
sriov_net_support: str = None,
key_name: str = None,
# Enclave options
nitro_enclave_enabled: bool = None,
# Can only be changed on initial creation of an instance as far as we know so far
client_token: str = None,
product_codes: List[Dict[str, str]] = None,
reservation_id: str = None,
block_device_mappings: List[
make_dataclass(
"BlockDeviceMapping",
[
("device_name", str),
("volume_id", str, field(default=None)),
("delete_on_termination", bool, field(default=False)),
# computed volume properties
("size", Computed[int], field(default=None)),
("type", Computed[str], field(default=None)),
("encrypted", Computed[bool], field(default=None)),
],
)
] = None,
license_arns: List[str] = None,
hibernation_enabled: bool = None,
# Instance Market Options
market_type: str = None,
# Spot Options
max_price: str = None,
spot_instance_type: str = None,
block_duration_minutes: int = None,
valid_until: str = None,
instance_interruption_behavior: str = None,
# Credit Specification
cpu_credits: str = None,
cpu_core_count: int = None,
cpu_threads_per_core: int = None,
# Metadata options
http_tokens: str = None,
http_put_response_hop_limit: int = None,
http_endpoint_enabled: bool = None,
http_protocol_ipv6_enabled: bool = None,
metadata_tags_enabled: bool = None,
# Private DNS Name options
hostname_type: str = None,
enable_resource_name_dns_a_record: bool = None,
enable_resource_name_dns_aaaa_record: bool = None,
# Capacity Reservation Options
capacity_reservation_preference: str = None,
instance_requirements: Dict[str, Any] = None,
# computed properties
instance_state: Computed[str] = None,
public_ip_address: Computed[str] = None,
root_device_name: Computed[str] = None,
# idem-heist options
bootstrap: List[Dict[str, str]] = None,
**kwargs,
) -> Dict[str, Any]:
"""Launches an instance using an AMI for which you have permissions.
You can specify a number of options, or leave the default options.
The following rules apply:
- [EC2-VPC] If you don't specify a subnet ID, we choose a default subnet from your default VPC for you.
If you don't have a default VPC, you must specify a subnet ID in the request.
- [EC2-Classic] If don't specify an Availability Zone, we choose one for you.
Some instance types must be launched into a VPC.
If you do not have a default VPC, or if you do not specify a subnet ID, the request fails.
For more information, see Instance types available only in a VPC.
- [EC2-VPC] All instances have a network interface with a primary private IPv4 address.
If you don't specify this address, we choose one from the IPv4 range of your subnet.
Not all instance types support IPv6 addresses.
For more information, see Instance types.
- If you don't specify a security group ID, we use the default security group.
For more information, see Security groups.
- If any of the AMIs have a product code attached for which the user has not subscribed, the request fails.
- You can create a launch template, which is a resource that contains the parameters to launch an instance.
You can specify the launch template instead of specifying the launch parameters.
An instance is ready for you to use when it's in the running state.
- You can tag instances and EBS volumes during launch, after launch, or both.
- Linux instances have access to the public key of the key pair at boot.
You can use this key to provide secure access to the instance.
Amazon EC2 public images use this feature to provide secure access without passwords.
For more information, see Key pairs.
Args:
name(str):
An Idem name of the resource.
resource_id(str):
AWS Ec2 Instance ID.
image_id(str):
The ID of an AMI.
instance_type(str, Optional):
The instance type to use for this instance on creation.
ebs_optimized(bool, Optional):
Indicates whether the instance is optimized ofr Amazon EBS I/O.
kernel_id(str, Optional):
The kernel associated with this instance, if applicable.
subnet_id(str, Optional):
The ID of the subnet in which the instance is running.
security_group_ids(List[str], Optional):
The IDs of the security groups. If you specify a network interface, you must specify any security
groups as part of the network interface. Default: Amazon EC2 uses the default security group.
network_interfaces(list[dict[str, Any]], Optional):
The network interfaces to associate with the instance. This manages the network interface attachment
to the instance, network interfaces themselves are managed in a separate state. Defaults to None.
* network_interface_id (str, Optional):
The ID of the network interface. If you are creating a Spot Fleet, omit this parameter because
you can’t specify a network interface ID in a launch specification.
* device_index (int, Optional):
The position of the network interface in the attachment order. A primary network interface has a
device index of 0. If you specify a network interface when launching an instance, you must
specify the device index.
* network_card_index (int, Optional):
The index of the network card. Some instance types support multiple network cards. The primary
network interface must be assigned to network card index 0. The default is network card index 0.
If you are using RequestSpotInstances to create Spot Instances, omit this parameter because you
can’t specify the network card index when using this API. To specify the network card index, use
RunInstances.
* subnet_id (Computed[str]):
The ID of the subnet.
* public_ip_address (Computed[str]):
The public IP address or Elastic IP address bound to the network interface.
* private_ip_address (Computed[str]):
The IPv4 address of the network interface within the subnet.
* mac_address (Computed[str]):
The MAC address.
monitoring_enabled(bool, Optional):
Indicates whether detailed monitoring is enabled.
source_dest_check(bool, Optional):
Indicates whether source/destination checking is enabled.
running(bool, Optional):
Indicates whether the instance should be in the "running" state.
private_ip_address(str, Optional):
The Ipv4 address of the network interface within the subnet.
owner_id(str, Optional):
The ID of the AWS account that owns the reservation.
availability_zone(str, Optional):
The Availability Zone of the instance.
affinity(str, Optional):
The affinity setting for the instance on the Dedicated Host.
group_name(str, Optional):
The affinity setting for the instance on the Dedicated Host.
partition_number(int, Optional):
The number of the partition that the instance is in.
host_id(str, Optional):
The ID of the Dedicated Host on which the instance resides.
tenancy(str, Optional):
The tenancy of the instance (if the instance is running in a VPC). An instance with a tenancy of dedicated runs on single-tenant hardware.
spread_domain(str, Optional):
Not yet documented by AWS.
host_resource_group_arn(str, Optional):
The ARN of the host resource group in which to launch the instances.
user_data(str, Optional):
The user data for the instance.
disable_api_termination(bool, Optional):
Indicates that an instance cannot be terminated using the Amazon Ec2 console, command line tool, or API.
ram_disk_id(str, Optional):
The ID of the RAM disk, if applicable.
tags(dict, Optional, Optional):
The tags to apply to the resource. Defaults to None.
iam_instance_profile_arn(str, Optional):
The IAM instance profile ARN.
instance_initiated_shutdown_behavior(str, Optional): Indicates whether an instance stops or terminates when you initiate
shutdown from the instance (using the operating system command for system shutdown).
elastic_inference_accelerators(list[dict[str, Any]], Optional):
An elastic inference accelerator to associate with the instance. Elastic inference accelerators
are a resource you can attach to your Amazon EC2 instances to accelerate your Deep Learning (DL)
inference workloads. You cannot specify accelerators from different generations in the same
request. Defaults to None.
* Type (str):
The type of elastic inference accelerator. The possible values are eia1.medium, eia1.large,
eia1.xlarge, eia2.medium, eia2.large, and eia2.xlarge.
* Count (int, Optional):
The number of elastic inference accelerators to attach to the instance. Default: 1
elastic_gpu_specifications(list[dict[str, Any]], Optional):
An elastic GPU to associate with the instance. An Elastic GPU is a GPU resource that you can
attach to your Windows instance to accelerate the graphics performance of your applications. For
more information, see Amazon EC2 Elastic GPUs in the Amazon EC2 User Guide. Defaults to None.
* Type (str): The type of Elastic Graphics accelerator. For more information about the values to specify for
Type, see Elastic Graphics Basics, specifically the Elastic Graphics accelerator column, in the
Amazon Elastic Compute Cloud User Guide for Windows Instances.
auto_recovery_enabled(bool, Optional):
Disables the automatic recovery behavior of your instance or sets it to default.
sriov_net_support(str, Optional):
Specifies whether enhanced networking with the Intel 82599 Virtual Function interface is enabled.
key_name(str, Optional):
The name of the keypair.
nitro_enclave_enabled(bool, Optional):
Indicates whether the instance is enabled for AWS Nitro Enclaves.
client_token(str, Optional):
The idempotency token for the instance.
root_device_name(str, Optional):
The device name of the root device (for example, /dev/sda1).
product_codes(list[dict[str, str]]:
The product codes attached to the instance, if applicable.
reservation_id(str, Optional):
The ID of the reservation
block_device_mappings(list[dict[str, Any]], Optional):
The block device mapping, which defines the EBS volumes and instance store volumes to attach to
the instance at launch. For more information, see Block device mappings in the Amazon EC2 User
Guide. Defaults to None.
* device_name (str, Optional):
The device name (for example, /dev/sdh or xvdh).
* volume_id: (str):
The resource_id of the volume to attach to the instance.
* virtual_name (str, Optional):
The virtual device name (ephemeralN). Instance store volumes are numbered starting from 0. An
instance type with 2 available instance store volumes can specify mappings for ephemeral0 and
ephemeral1. The number of available instance store volumes depends on the instance type. After
you connect to the instance, you must mount the volume. NVMe instance store volumes are
automatically enumerated and assigned a device name. Including them in your block device mapping
has no effect. Constraints: For M3 instances, you must specify instance store volumes in the
block device mapping for the instance. When you launch an M3 instance, we ignore any instance
store volumes specified in the block device mapping for the AMI.
* delete_on_termination (bool, Optional):
Indicates whether the EBS volume is deleted on instance termination. For more information, see
Preserving Amazon EBS volumes on instance termination in the Amazon EC2 User Guide.
license_arns(list[str], Optional):
The license configuration arns.
hibernation_enabled(bool, Optional):
Indicates whether the instance is configured for hibernation.
market_type(str, Optional):
The market (purchasing) option for the instance.
max_price(str, Optional):
The maximum hourly price you're willing to pay for the Spot Instances.
spot_instance_type(str, Optional):
The Spot Instance request type. Persistent Spot Instance requests are only supported when the instance
interruption behavior is either hibernate or stop.
block_duration_minutes(int, Optional):
Deprecated.
valid_until(str, Optional):
The end date of the request, in UTC format (YYYY -MM -DD T*HH* :MM :SS Z). Supported only for persistent requests.
instance_interruption_behavior(str, Optional):
The behavior when a Spot Instance is interrupted. The default is terminate.
cpu_credits(str, Optional):
The credit option for CPU usage of a T2, T3, or T3a instance. Valid values are standard and unlimited.
cpu_core_count(int, Optional):
The number of CPU cores for the instance.
cpu_threads_per_core(int, Optional):
The number of threads per CPU core. To disable multithreading for the instance, specify a value of1 .
Otherwise, specify the default value of 2.
http_tokens(str, Optional):
The state of token usage for your instance metadata requests. If the state is optional, you can
choose to retrieve instance metadata with or without a signed token header on your request. If
you retrieve the IAM role credentials without a token, the version 1.0 role credentials are
returned. If you retrieve the IAM role credentials using a valid signed token, the version 2.0
role credentials are returned. If the state is required, you must send a signed token header
with any instance metadata retrieval requests. In this state, retrieving the IAM role
credentials always returns the version 2.0 credentials; the version 1.0 credentials are not
available. Default: Optional.
http_put_response_hop_limit(int, Optional):
The desired HTTP PUT response hop limit for instance metadata requests. The larger the number,
the further instance metadata requests can travel. Default: 1 Possible values: Integers from 1
to 64.
http_endpoint_enabled(bool, Optional):
Enables or disables the HTTP metadata endpoint on your instances. If you specify a value of
disabled, you cannot access your instance metadata. Default: enabled.
http_protocol_ipv6_enabled(str, Optional):
Enables or disables the IPv6 endpoint for the instance metadata service.
metadata_tags_enabled(bool, Optional):
Set to enabled to allow access to instance tags from the instance metadata. Set to disabled to
turn off access to instance tags from the instance metadata. For more information, see Work with
instance tags using the instance metadata. Default: disabled.
hostname_type(str, Optional):
The type of hostname for EC2 instances.
For IPv4 only subnets, an instance DNS name must be based on the instance IPv4 address.
For IPv6 only subnets, an instance DNS name must be based on the instance ID.
For dual-stack subnets, you can specify whether DNS names use the instance IPv4 address or the instance ID.
enable_resource_name_dns_a_record(bool, Optional):
Indicates whether to respond to DNS queries for instance hostnames with DNS A records.
enable_resource_name_dns_aaaa_record(bool, Optional):
Indicates whether to respond to DNS queries for instance hostnames with DNS A records.
capacity_reservation_preference(str, Optional):
Indicates the instance's Capacity Reservation preferences.
instance_requirements(dict[str, Any], Optional):
The attributes for the instance type. When you specify instance attributes, Amazon EC2 will identify
instance types with these attributes.
instance_state(Computed[str]):
The reported state of the instance, possible values are:
pending, running, shutting-down, terminated, stopping, stopped
public_ip_address(Computed[str]):
The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable.
A Carrier IP address only applies to an instance launched in a subnet associated with a Wavelength Zone.
bootstrap(list[dict[str, Any]], Optional):
Bootstr options for provisioning an instance with "heist".
Request Syntax:
.. code-block:: sls
[instance-resource-id]:
aws.ec2.instance.present:
- name: "string"
- image_id: "string"
- tags:
- Key: "string"
Value: "string"
- bootstrap:
- heist_manager: "string"
artifact_version: "string"
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
default_vpc:
exec.run:
- path: aws.ec2.vpc.get
- kwargs:
default: "true"
default_subnet:
exec.run:
- path: aws.ec2.subnet.get
- kwargs:
filters:
- name: vpc-id
values:
- ${exec:default_vpc:resource_id}
small_image:
exec.run:
- path: aws.ec2.ami.get
- kwargs:
owners:
- amazon
most_recent: True
filters:
- name: image-type
values:
- machine
- name: state
values:
- available
- name: hypervisor
values:
- xen
- name: architecture
values:
- x86_64
- name: root-device-type
values:
- ebs
- name: virtualization-type
values:
- hvm
small_instance_type:
exec.run:
- path: aws.ec2.instance_type.get
- kwargs:
filters:
- name: instance-type
values:
- '*.nano'
- name: hypervisor
values:
- xen
- name: processor-info.supported-architecture
values:
- x86_64
my_network_interface:
aws.ec2.network_interface.present:
- subnet_id: ${exec:default_subnet:resource_id}
create_instance:
aws.ec2.instance.present:
- image_id: ${exec:small_image:resource_id}
- instance_type: ${exec:small_instance_type:resource_id}
- client_token: optional-idempotence-token
- network_interfaces:
- network_interface_id: ${aws.ec2.network_interface:my_network_interface:resource_id}
device_index: 0
network_card_index: 0
- tags:
Name: test-idem-cloud-demo
- bootstrap:
- heist_manager: salt.minion
"""
# Convert a list of dictionaries to a plain dictionary before calculating desired_state
if isinstance(tags, list):
tags = hub.tool.aws.tag_utils.convert_tag_list_to_dict(tags)
# Due to a bug, the default value populated to ESM is {}, for idem-aws versions <= 3.0.0
if iam_instance_profile_arn == {}:
iam_instance_profile_arn = None
# Get all the parameters passed to this function as a single dictionary
desired_state = {
k: v
for k, v in locals().items()
if k not in ("hub", "ctx", "kwargs") and v is not None
}
result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
current_state = None
if resource_id:
get = await hub.exec.aws.ec2.instance.get(
ctx, name=name, resource_id=resource_id
)
current_state = result["old_state"] = get.ret
if not result["old_state"]:
result["comment"] += [
f"Could not find instance for '{name}' with existing id '{resource_id}'"
]
return result
result["comment"] += [f"Instance '{name}' already exists"]
elif client_token:
get = await hub.exec.aws.ec2.instance.get(
ctx, name=name, filters=[{"Name": "client-token", "Values": [client_token]}]
)
if get.result is True:
if get.ret is None:
result["comment"] += [
f"Could not find Instance for '{name}' with existing idempotence token '{client_token}'"
]
else:
current_state = result["old_state"] = get.ret
resource_id = current_state.resource_id
result["comment"] += [f"Instance '{name}' already exists"]
if not resource_id:
if ctx.test:
result["new_state"] = hub.tool.aws.ec2.instance.state.test(**desired_state)
result["comment"] += [f"Would create aws.ec2.instance '{name}'"]
return result
# Prepare options that are grouped in the request
# Prepare instance market options
spot_options = {}
if max_price:
spot_options["MaxPrice"] = max_price
if spot_instance_type:
spot_options["SpotInstanceType"] = spot_instance_type
if block_duration_minutes:
spot_options["BlockDurationMinutes"] = block_duration_minutes
if valid_until:
spot_options["ValidUntil"] = datetime.datetime(valid_until)
if instance_initiated_shutdown_behavior:
spot_options[
"InstanceInterruptionBehavior"
] = instance_interruption_behavior
if market_type:
instance_market_options = {
"MarketType": market_type,
"SpotOptions": spot_options,
}
elif spot_options:
instance_market_options = {"SpotOptions": spot_options}
else:
instance_market_options = None
# Prepare Metadata options
metadata_options = {}
if http_tokens:
metadata_options["HttpTokens"] = http_tokens
if http_put_response_hop_limit:
metadata_options["HttpPutResponseHopLimit"] = http_put_response_hop_limit
if http_endpoint_enabled is not None:
metadata_options["HttpEndpoint"] = (
"enabled" if http_endpoint_enabled else "disabled"
)
if http_protocol_ipv6_enabled is not None:
metadata_options["HttpProtocolIpv6"] = (
"enabled" if http_protocol_ipv6_enabled else "disabled"
)
if metadata_tags_enabled is not None:
metadata_options["InstanceMetadataTags"] = (
"enabled" if metadata_tags_enabled else "disabled"
)
# Prepare Private DNS Name Options
private_dns_name_options = {}
if hostname_type:
private_dns_name_options["HostnameType"] = hostname_type
if enable_resource_name_dns_a_record is not None:
private_dns_name_options[
"EnableResourceNameDnsARecord"
] = enable_resource_name_dns_a_record
if enable_resource_name_dns_aaaa_record is not None:
private_dns_name_options[
"EnableResourceNameDnsAAAARecord"
] = enable_resource_name_dns_aaaa_record
# Prepare Cpu Options
cpu_options = {}
if cpu_core_count:
cpu_options["CoreCount"] = cpu_core_count
if cpu_threads_per_core:
cpu_options["ThreadsPerCore"] = cpu_threads_per_core
# Prepare Placement Options
placement = {}
if availability_zone:
placement["AvailabilityZone"] = availability_zone
if affinity:
placement["Affinity"] = affinity
if group_name:
placement["GroupName"] = group_name
if partition_number:
placement["PartitionNumber"] = partition_number
if host_id:
placement["HostId"] = host_id
if tenancy:
placement["Tenancy"] = tenancy
if spread_domain:
placement["SpreadDomain"] = spread_domain
if host_resource_group_arn:
placement["HostResourceGroupArn"] = host_resource_group_arn
# Prepare TagSpecifications
if tags:
tag_specifications = [
{
"ResourceType": "instance",
"Tags": [{"Key": k, "Value": v} for k, v in tags.items()],
}
]
else:
tag_specifications = None
# Prepare Maintenance Options
maintenance_options = {}
if auto_recovery_enabled is not None:
maintenance_options["AutoRecovery"] = (
"disabled" if auto_recovery_enabled is False else "default"
)
# Prepare Hibernation Options
hibernation_options = {}
if hibernation_enabled is not None:
hibernation_options["Configured"] = hibernation_enabled
# Prepare Credit Specifications
credit_specifications = {}
if cpu_credits is not None:
credit_specifications["CpuCredits"] = cpu_credits
# Prepare Enclave Options
enclave_options = {}
if nitro_enclave_enabled:
enclave_options["Enabled"] = nitro_enclave_enabled
# Prepare Monitoring Options
monitoring_options = {}
if monitoring_enabled:
monitoring_options["Enabled"] = monitoring_enabled
# Prepare Capacity Reservation Specification
capacity_reservation_specification = {}
if capacity_reservation_preference:
capacity_reservation_specification[
"CapacityReservationPreference"
] = capacity_reservation_preference
network_interface_attachment_specification = []
if subnet_id:
# Subnet id and network interfaces cannot be specified together
# The network interfaces will be handled in the update process
network_interface_attachment_specification = None
elif network_interfaces:
for network_interface_attachment in network_interfaces:
network_interface_attachment_specification.append(
{
"NetworkInterfaceId": network_interface_attachment.get(
"network_interface_id"
),
"DeviceIndex": network_interface_attachment.get("device_index"),
"NetworkCardIndex": network_interface_attachment.get(
"network_card_index"
),
}
)
new_block_device_mappings = None
if block_device_mappings:
new_block_device_mappings = list()
for bd in block_device_mappings:
device_name = bd.get("device_name")
if not device_name:
continue
# Create an empty mapping while the instance is starting -- it will be connected on update
new_bd = dict(DeviceName=device_name, NoDevice="")
if bd.get("virtual_name"):
new_bd["VirtualName"] = bd["virtual_name"]
new_block_device_mappings.append(new_bd)
# Create a brand new instance with minimal arguments, it will be updated after creation
create_ret = await hub.exec.boto3.client.ec2.run_instances(
ctx,
ClientToken=client_token,
MaxCount=1,
MinCount=1,
ImageId=image_id,
InstanceType=instance_type,
EbsOptimized=ebs_optimized,
BlockDeviceMappings=new_block_device_mappings,
KernelId=kernel_id,
Placement=placement or None,
SubnetId=subnet_id,
NetworkInterfaces=network_interface_attachment_specification,
Monitoring=monitoring_options or None,
PrivateIpAddress=private_ip_address,
UserData=user_data,
DisableApiTermination=disable_api_termination,
InstanceInitiatedShutdownBehavior=instance_initiated_shutdown_behavior,
EnclaveOptions=enclave_options or None,
RamDiskId=ram_disk_id,
TagSpecifications=tag_specifications,
ElasticGpuSpecification=elastic_gpu_specifications,
ElasticInferenceAccelerators=elastic_inference_accelerators,
IamInstanceProfile={"Arn": iam_instance_profile_arn}
if iam_instance_profile_arn
else None,
KeyName=key_name,
InstanceMarketOptions=instance_market_options,
CreditSpecification=credit_specifications or None,
CpuOptions=cpu_options or None,
CapacityReservationSpecification=capacity_reservation_specification or None,
LicenseSpecifications=[
{"LicenseConfigurationArn": license_arn}
for license_arn in license_arns or []
],
HibernationOptions=hibernation_options or None,
MetadataOptions=metadata_options or None,
PrivateDnsNameOptions=private_dns_name_options or None,
MaintenanceOptions=maintenance_options or None,
SecurityGroupIds=security_group_ids,
)
result["result"] &= create_ret.result
if not create_ret:
result["comment"] += [create_ret.comment]
return result
result["comment"] += [f"Created '{name}'"]
resource_id = create_ret.ret["Instances"][0]["InstanceId"]
# This makes sure the created Instance is saved to esm regardless if the subsequent update call fails or not.
result["new_state"] = dict(name=name, resource_id=resource_id)
result["force_save"] = True
present_ret = (
await hub.tool.aws.ec2.instance.state.convert_instance_to_present_async(
ctx, {"Reservations": [create_ret.ret]}, name=name
)
)
current_state = present_ret[resource_id]
if not current_state:
result["comment"] += [
f"Unable to get the current_state, instance may still be undergoing creation: '{name}'"
]
result["result"] = False
return result
# Modify all the attributes that need modification
if ctx.test:
result["new_state"] = hub.tool.aws.ec2.instance.state.test(**desired_state)
return result
resource = await hub.tool.boto3.resource.create(
ctx, "ec2", "Instance", id=resource_id
)
# TODO: Remove waiters across the board in this flow
if resource.state["Name"] == "pending":
await hub.tool.boto3.resource.exec(resource, "wait_until_running")
elif resource.state["Name"] == "stopping":
await hub.tool.boto3.resource.exec(resource, "wait_until_stopped")
try:
result["result"] &= await hub.tool.aws.ec2.instance.update.init.apply(
ctx,
resource,
old_value=current_state,
new_value=desired_state,
comments=result["comment"],
)
except Exception as apply_config_error:
err_msg = f"Failed to apply configuration on the resource {resource_id}: {apply_config_error.__class__.__name__}"
hub.log.error(err_msg)
result["comment"].append(err_msg)
result["result"] = False
get = await hub.exec.aws.ec2.instance.get(ctx, resource_id=resource_id, name=name)
if get.result:
current_state = get.ret
result["new_state"] = current_state
if running:
# If running is set to True then it goes to reconciler and makes sure current state is running
hub.log.debug(f"Waiting for instance '{resource_id}' to be running")
result["rerun_data"] = dict(target_instance_state="running")
else:
hub.log.debug(f"Waiting for instance '{resource_id}' to be stopped")
result["rerun_data"] = dict(target_instance_state="stopped")
return result
[docs]async def absent(
hub, ctx, name: str, resource_id: str = None, client_token: str = None, **kwargs
) -> Dict[str, Any]:
"""Shuts down the specified instance.
Terminated instances remain visible after termination (for approximately one hour).
Args:
name(str): The name of the state.
resource_id(str): An instance id.
client_token(str): An idempotence token that was used with the instance creation
Request Syntax:
.. code-block:: sls
[instance-resource-id]:
aws.ec2.instance.absent:
- name: "string"
- resource_id: "string"
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
resource_is_absent:
aws.ec2.instance.absent:
- name: value
"""
result = dict(
comment=[],
old_state=ctx.old_state,
new_state=None,
name=name,
result=True,
rerun_data=dict(target_instance_state="terminated"),
)
# Get the resource_id from ESM
if not resource_id:
resource_id = (ctx.old_state or {}).get("resource_id")
if not resource_id and client_token:
get = await hub.exec.aws.ec2.instance.get(
ctx, name=name, filters=[{"Name": "client-token", "Values": [client_token]}]
)
resource_id = get.ret.resource_id
# If there still is no resource_id, the instance is gone
if not resource_id:
result["comment"] += [f"'{name}' already terminated"]
return result
if ctx.test:
result["comment"] += [f"Would terminate aws.ec2.instance '{name}'"]
return result
get = await hub.exec.aws.ec2.instance.get(ctx, name=name, resource_id=resource_id)
if get.get("ret") is not None:
result["old_state"] = get["ret"]
current_status = result["old_state"].get("instance_state")
if current_status == "terminated":
result["comment"] += [
f"Terminated instance '{name}', it will still be visible for about 60 minutes"
]
return result
ret = await hub.exec.boto3.client.ec2.terminate_instances(
ctx, InstanceIds=[resource_id]
)
if ret.comment:
result["comment"].append(ret.comment)
if not ret.result and any("does not exist" in c for c in ret.comment):
# The command failed because the instance no longer exists
return result
elif not ret.result:
# The failure is some other reason we need to examine
result["result"] &= ret.result
else:
get = await hub.exec.aws.ec2.instance.get(
ctx, resource_id=resource_id, name=name
)
result["new_state"] = get.ret
result["comment"] += [
f"Terminated instance '{name}', it will still be visible for about 60 minutes"
]
return result
[docs]async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]:
"""Describe the resource in a way that can be recreated/managed with the corresponding "present" function.
Returns:
Dict[str, Any]
Examples:
.. code-block:: bash
$ idem describe aws.ec2.instance
"""
result = {}
ret = await hub.exec.boto3.client.ec2.describe_instances(ctx)
if not ret:
hub.log.warning(f"Could not describe Instances: {ret.comment}")
return {}
instances = await hub.tool.aws.ec2.instance.state.convert_instance_to_present_async(
ctx, ret.ret
)
for instance_id, present_state in instances.items():
result[instance_id] = {
"aws.ec2.instance.present": [{k: v} for k, v in present_state.items()]
}
return result
[docs]def is_pending(hub, ret):
new_state = ret.get("new_state", None)
rerun_data = ret.get("rerun_data", None)
if new_state and rerun_data:
current_status = new_state.get("instance_state")
resource_id = new_state.get("resource_id")
if current_status and isinstance(current_status, str):
if current_status.casefold() == rerun_data.get("target_instance_state"):
hub.log.debug(
f"No need to reconcile new state for instance '{resource_id}' with current status {current_status}"
)
# Cleanup new_state to make sure instance is removed from ESM
if (
rerun_data.get("target_instance_state") == "terminated"
and ret["new_state"]
):
return True
return False
else:
hub.log.debug(
f"Reconcile new state for instance '{resource_id}' with current status {current_status}"
)
return True
if (
not new_state
and rerun_data
and rerun_data.get("target_instance_state") == "terminated"
):
# No need to reconcile terminated state
return False
return (not ret["result"]) or bool(ret["changes"])