Skip to content

Commit

Permalink
Minor update on module_utils/ec2 + add mew module ec2_launch_template…
Browse files Browse the repository at this point in the history
…_info
  • Loading branch information
abikouo committed Oct 15, 2024
1 parent 3add45e commit 33da9e5
Show file tree
Hide file tree
Showing 9 changed files with 800 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
minor_changes:
- Move function ``determine_iam_role`` from module ``ec2_instance`` to module_utils/ec2 so that it can be used by ``community.aws.ec2_launch_template`` module (https://github.com/ansible-collections/amazon.aws/pull/2319).
- module_utils/ec2 - add some shared code for Launch template AWS API calls (https://github.com/ansible-collections/amazon.aws/pull/2319).
1 change: 1 addition & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ action_groups:
- ec2_instance_info
- ec2_key
- ec2_key_info
- ec2_launch_template_info
- ec2_security_group
- ec2_security_group_info
- ec2_snapshot
Expand Down
14 changes: 7 additions & 7 deletions plugins/doc_fragments/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ class ModuleDocFragment:
tags:
description:
- A dictionary representing the tags to be applied to the resource.
- If the I(tags) parameter is not set then tags will not be modified.
- If the O(tags) parameter is not set then tags will not be modified.
type: dict
required: false
aliases: ['resource_tags']
purge_tags:
description:
- If I(purge_tags=true) and I(tags) is set, existing tags will be purged
from the resource to match exactly what is defined by I(tags) parameter.
- If the I(tags) parameter is not set then tags will not be modified, even
if I(purge_tags=True).
- Tag keys beginning with C(aws:) are reserved by Amazon and can not be
- If O(purge_tags=true) and O(tags) is set, existing tags will be purged
from the resource to match exactly what is defined by O(tags) parameter.
- If the O(tags) parameter is not set then tags will not be modified, even
if O(purge_tags=True).
- Tag keys beginning with V(aws:) are reserved by Amazon and can not be
modified. As such they will be ignored for the purposes of the
I(purge_tags) parameter. See the Amazon documentation for more information
O(purge_tags) parameter. See the Amazon documentation for more information
U(https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html#tag-conventions).
type: bool
default: true
Expand Down
116 changes: 115 additions & 1 deletion plugins/module_utils/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@

# Used to live here, moved into ansible_collections.amazon.aws.plugins.module_utils.arn
from .arn import is_outpost_arn as is_outposts_arn # pylint: disable=unused-import
from .arn import validate_aws_arn

# Used to live here, moved into ansible_collections.amazon.aws.plugins.module_utils.botocore
from .botocore import HAS_BOTO3 # pylint: disable=unused-import
Expand All @@ -72,6 +73,7 @@

# Used to live here, moved into ansible_collections.amazon.aws.plugins.module_utils.exceptions
from .exceptions import AnsibleAWSError # pylint: disable=unused-import
from .iam import list_iam_instance_profiles

# Used to live here, moved into ansible_collections.amazon.aws.plugins.module_utils.modules
# The names have been changed in .modules to better reflect their applicability.
Expand Down Expand Up @@ -1239,7 +1241,7 @@ class EC2NetworkAclErrorHandler(AWSErrorHandler):

@classmethod
def _is_missing(cls):
return is_boto3_error_code("")
return is_boto3_error_code("InvalidNetworkAclID.NotFound")


@EC2NetworkAclErrorHandler.list_error_handler("describe network acls", [])
Expand Down Expand Up @@ -1359,6 +1361,108 @@ def create_ec2_placement_group(client, **params: Dict[str, Union[str, EC2TagSpec
return client.create_placement_group(**params)["PlacementGroup"]


# EC2 Launch template
class EC2LaunchTemplateErrorHandler(AWSErrorHandler):
_CUSTOM_EXCEPTION = AnsibleEC2Error

@classmethod
def _is_missing(cls):
return is_boto3_error_code(["InvalidLaunchTemplateName.NotFoundException", "InvalidLaunchTemplateId.NotFound"])


@EC2LaunchTemplateErrorHandler.list_error_handler("describe launch templates", [])
@AWSRetry.jittered_backoff()
def describe_launch_templates(
client,
launch_template_ids: Optional[List[str]] = None,
launch_template_names: Optional[List[str]] = None,
filters: Optional[List[Dict[str, List[str]]]] = None,
) -> List[Dict[str, Any]]:
params = {}
if launch_template_ids:
params["LaunchTemplateIds"] = launch_template_ids
if launch_template_names:
params["LaunchTemplateNames"] = launch_template_names
if filters:
params["Filters"] = filters
paginator = client.get_paginator("describe_launch_templates")
return paginator.paginate(**params).build_full_result()["LaunchTemplates"]


@EC2LaunchTemplateErrorHandler.common_error_handler("describe launch template versions")
@AWSRetry.jittered_backoff()
def describe_launch_template_versions(client, **params: Dict[str, Any]) -> List[Dict[str, Any]]:
paginator = client.get_paginator("describe_launch_template_versions")
return paginator.paginate(**params).build_full_result()["LaunchTemplateVersions"]


@EC2LaunchTemplateErrorHandler.common_error_handler("delete launch template versions")
@AWSRetry.jittered_backoff()
def delete_launch_template_versions(
client, versions: List[str], launch_template_id: Optional[str] = None, launch_template_name: Optional[str] = None
) -> Dict[str, Any]:
params = {}
if launch_template_id:
params["LaunchTemplateId"] = launch_template_id
if launch_template_name:
params["LaunchTemplateName"] = launch_template_name
response = {
"UnsuccessfullyDeletedLaunchTemplateVersions": [],
"SuccessfullyDeletedLaunchTemplateVersions": [],
}
# Using this API, You can specify up to 200 launch template version numbers.
for i in range(0, len(versions), 200):
result = client.delete_launch_template_versions(Versions=list(versions[i : i + 200]), **params)
for x in ("SuccessfullyDeletedLaunchTemplateVersions", "UnsuccessfullyDeletedLaunchTemplateVersions"):
response[x] += result.get(x, [])
return response


@EC2LaunchTemplateErrorHandler.common_error_handler("delete launch template")
@AWSRetry.jittered_backoff()
def delete_launch_template(
client, launch_template_id: Optional[str] = None, launch_template_name: Optional[str] = None
) -> Dict[str, Any]:
params = {}
if launch_template_id:
params["LaunchTemplateId"] = launch_template_id
if launch_template_name:
params["LaunchTemplateName"] = launch_template_name
return client.delete_launch_template(**params)["LaunchTemplate"]


@EC2LaunchTemplateErrorHandler.common_error_handler("create launch template")
@AWSRetry.jittered_backoff()
def create_launch_template(
client,
launch_template_name: str,
launch_template_data: Dict[str, Any],
tags: Optional[EC2TagSpecifications] = None,
**kwargs: Dict[str, Any],
) -> Dict[str, Any]:
params = {"LaunchTemplateName": launch_template_name, "LaunchTemplateData": launch_template_data}
if tags:
params["TagSpecifications"] = boto3_tag_specifications(tags, types="launch-template")
params.update(kwargs)
return client.create_launch_template(**params)["LaunchTemplate"]


@EC2LaunchTemplateErrorHandler.common_error_handler("create launch template version")
@AWSRetry.jittered_backoff()
def create_launch_template_version(
client, launch_template_data: Dict[str, Any], **params: Dict[str, Any]
) -> Dict[str, Any]:
return client.create_launch_template_version(LaunchTemplateData=launch_template_data, **params)[
"LaunchTemplateVersion"
]


@EC2LaunchTemplateErrorHandler.common_error_handler("modify launch template")
@AWSRetry.jittered_backoff()
def modify_launch_template(client, **params: Dict[str, Any]) -> Dict[str, Any]:
return client.modify_launch_template(**params)["LaunchTemplate"]


def get_ec2_security_group_ids_from_names(sec_group_list, ec2_connection, vpc_id=None, boto3=None):
"""Return list of security group IDs from security group names. Note that security group names are not unique
across VPCs. If a name exists across multiple VPCs and no VPC ID is supplied, all matching IDs will be returned. This
Expand Down Expand Up @@ -1580,3 +1684,13 @@ def normalize_ec2_vpc_dhcp_config(option_config: List[Dict[str, Any]]) -> Dict[s
config_data[option] = [val["Value"] for val in config_item["Values"]]

return config_data


def determine_iam_arn_from_name(iam_client, name_or_arn: str) -> str:
if validate_aws_arn(name_or_arn, service="iam", resource_type="instance-profile"):
return name_or_arn

iam_instance_profiles = list_iam_instance_profiles(iam_client, name=name_or_arn)
if not iam_instance_profiles:
raise AnsibleEC2Error(message=f"Could not find IAM instance profile {name_or_arn}")
return iam_instance_profiles[0]["Arn"]
36 changes: 11 additions & 25 deletions plugins/modules/ec2_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -1338,8 +1338,6 @@
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict
from ansible.module_utils.six import string_types

from ansible_collections.amazon.aws.plugins.module_utils.arn import validate_aws_arn
from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import associate_iam_instance_profile
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import attach_network_interface
Expand All @@ -1349,6 +1347,7 @@
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_instances
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_subnets
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_vpcs
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import determine_iam_arn_from_name
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_ec2_security_group_ids_from_names
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import modify_instance_attribute
Expand Down Expand Up @@ -1391,12 +1390,14 @@ def add_or_update_instance_profile(
client, module: AnsibleAWSModule, instance: Dict[str, Any], desired_profile_name: str
) -> bool:
instance_profile_setting = instance.get("IamInstanceProfile")
iam_client = None
if instance_profile_setting and desired_profile_name:
if desired_profile_name in (instance_profile_setting.get("Name"), instance_profile_setting.get("Arn")):
# great, the profile we asked for is what's there
return False
else:
desired_arn = determine_iam_role(module, desired_profile_name)
iam_client = module.client("iam")
desired_arn = determine_iam_arn_from_name(iam_client, desired_profile_name)
if instance_profile_setting.get("Arn") == desired_arn:
return False

Expand All @@ -1409,10 +1410,11 @@ def add_or_update_instance_profile(
# check for InvalidAssociationID.NotFound
module.fail_json_aws(e, "Could not find instance profile association")
try:
iam_client = iam_client or module.client("iam")
replace_iam_instance_profile_association(
client,
association_id=association[0]["AssociationId"],
iam_instance_profile={"Arn": determine_iam_role(module, desired_profile_name)},
iam_instance_profile={"Arn": determine_iam_arn_from_name(iam_client, desired_profile_name)},
)
return True
except AnsibleEC2Error as e:
Expand All @@ -1421,9 +1423,10 @@ def add_or_update_instance_profile(
if not instance_profile_setting and desired_profile_name:
# create association
try:
iam_client = iam_client or module.client("iam")
associate_iam_instance_profile(
client,
iam_instance_profile={"Arn": determine_iam_role(module, desired_profile_name)},
iam_instance_profile={"Arn": determine_iam_arn_from_name(iam_client, desired_profile_name)},
instance_id=instance["InstanceId"],
)
return True
Expand Down Expand Up @@ -1806,7 +1809,9 @@ def build_run_instance_spec(client, module: AnsibleAWSModule, current_count: int

# IAM profile
if params.get("iam_instance_profile"):
spec["IamInstanceProfile"] = dict(Arn=determine_iam_role(module, params.get("iam_instance_profile")))
spec["IamInstanceProfile"] = dict(
Arn=determine_iam_arn_from_name(module.client("iam"), params.get("iam_instance_profile"))
)

if params.get("instance_type"):
spec["InstanceType"] = params["instance_type"]
Expand Down Expand Up @@ -2310,25 +2315,6 @@ def pretty_instance(i):
return instance


def determine_iam_role(module: AnsibleAWSModule, name_or_arn: Optional[str]) -> str:
if validate_aws_arn(name_or_arn, service="iam", resource_type="instance-profile"):
return name_or_arn
iam = module.client("iam", retry_decorator=AWSRetry.jittered_backoff())
try:
role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True)
return role["InstanceProfile"]["Arn"]
except is_boto3_error_code("NoSuchEntity") as e:
module.fail_json_aws(e, msg=f"Could not find iam_instance_profile {name_or_arn}")
except (
botocore.exceptions.ClientError,
botocore.exceptions.BotoCoreError,
) as e: # pylint: disable=duplicate-except
module.fail_json_aws(
e,
msg=f"An error occurred while searching for iam_instance_profile {name_or_arn}. Please try supplying the full ARN.",
)


def modify_instance_type(
client,
module: AnsibleAWSModule,
Expand Down
Loading

0 comments on commit 33da9e5

Please sign in to comment.