Skip to content

Commit

Permalink
autoscaling_instance_refresh - prepare modules for promotion (ansible…
Browse files Browse the repository at this point in the history
…-collections#2150)

SUMMARY
Closes ansible-collections#2120
Closes ansible-collections#2019
Closes ansible-collections#2016
Prepare modules autoscaling_instance_refresh and autoscaling_instance_refresh_info for promotion:

Refactor modules to use common code from ansible_collections.amazon.aws.plugins.module_utils.autoscaling
Add type hinting
Update integration tests

ISSUE TYPE

Feature Pull Request

Reviewed-by: GomathiselviS
Reviewed-by: Bikouo Aubin
Reviewed-by: Alina Buzachis

This commit was initially merged in https://github.com/ansible-collections/community.aws
See: ansible-collections/community.aws@d59fa93
  • Loading branch information
abikouo committed Oct 15, 2024
1 parent 814d336 commit e699fa1
Show file tree
Hide file tree
Showing 7 changed files with 618 additions and 553 deletions.
238 changes: 135 additions & 103 deletions plugins/modules/autoscaling_instance_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
description:
- Start or cancel an EC2 Auto Scaling Group instance refresh in AWS.
- Can be used with M(community.aws.autoscaling_instance_refresh_info) to track the subsequent progress.
- Prior to release 5.0.0 this module was called C(community.aws.ec2_asg_instance_refresh).
- Prior to release 5.0.0 this module was called M(community.aws.ec2_asg_instance_refresh).
The usage did not change.
author:
- "Dan Khersonsky (@danquixote)"
Expand All @@ -30,7 +30,7 @@
required: true
strategy:
description:
- The strategy to use for the instance refresh. The only valid value is C(Rolling).
- The strategy to use for the instance refresh. The only valid value is V(Rolling).
- A rolling update is an update that is applied to all instances in an Auto Scaling group until all instances have been updated.
- A rolling update can fail due to failed health checks or if instances are on standby or are protected from scale in.
- If the rolling update process fails, any instances that were already replaced are not rolled back to their previous configuration.
Expand All @@ -40,15 +40,16 @@
description:
- Set of preferences associated with the instance refresh request.
- If not provided, the default values are used.
- For I(min_healthy_percentage), the default value is C(90).
- For I(instance_warmup), the default is to use the value specified for the health check grace period for the Auto Scaling group.
- Can not be specified when I(state) is set to 'cancelled'.
- For O(preferences.min_healthy_percentage), the default value is V(90).
- For O(preferences.instance_warmup), the default is to use the value specified for the health check grace period for the Auto Scaling group.
- Can not be specified when O(state=cancelled).
required: false
suboptions:
min_healthy_percentage:
description:
- Total percent of capacity in ASG that must remain healthy during instance refresh to allow operation to continue.
- It is rounded up to the nearest integer.
- Value range is V(0) to V(100).
type: int
default: 90
instance_warmup:
Expand All @@ -57,6 +58,21 @@
- During this time, Amazon EC2 Auto Scaling does not immediately move on to the next replacement.
- The default is to use the value for the health check grace period defined for the group.
type: int
skip_matching:
description:
- Indicates whether skip matching is enabled.
- If enabled V(true), then Amazon EC2 Auto Scaling skips replacing instances that match the desired configuration.
type: bool
version_added: 9.0.0
max_healthy_percentage:
description:
- Specifies the maximum percentage of the group that can be in service and healthy, or pending,
to support your workload when replacing instances.
- The value is expressed as a percentage of the desired capacity of the Auto Scaling group.
- Value range is V(100) to V(200).
- When specified, you must also specify O(preferences.min_healthy_percentage), and the difference between them cannot be greater than V(100).
type: int
version_added: 9.0.0
type: dict
extends_documentation_fragment:
- amazon.aws.common.modules
Expand Down Expand Up @@ -84,123 +100,143 @@
preferences:
min_healthy_percentage: 91
instance_warmup: 60
skip_matching: true
"""

RETURN = r"""
---
instance_refresh_id:
description: instance refresh id
returned: success
type: str
sample: "08b91cf7-8fa6-48af-b6a6-d227f40f1b9b"
auto_scaling_group_name:
description: Name of autoscaling group
returned: success
type: str
sample: "public-webapp-production-1"
status:
description:
- The current state of the group when DeleteAutoScalingGroup is in progress.
- The following are the possible statuses
- Pending -- The request was created, but the operation has not started.
- InProgress -- The operation is in progress.
- Successful -- The operation completed successfully.
- Failed -- The operation failed to complete. You can troubleshoot using the status reason and the scaling activities.
- Cancelling --
- An ongoing operation is being cancelled.
- Cancellation does not roll back any replacements that have already been completed,
- but it prevents new replacements from being started.
- Cancelled -- The operation is cancelled.
returned: success
type: str
sample: "Pending"
start_time:
description: The date and time this ASG was created, in ISO 8601 format.
returned: success
type: str
sample: "2015-11-25T00:05:36.309Z"
end_time:
description: The date and time this ASG was created, in ISO 8601 format.
returned: success
type: str
sample: "2015-11-25T00:05:36.309Z"
percentage_complete:
description: the % of completeness
returned: success
type: int
sample: 100
instances_to_update:
description: num. of instance to update
returned: success
type: int
sample: 5
instance_refreshes:
description: Details of the instance refreshes for the Auto Scaling group.
returned: always
type: complex
contains:
instance_refresh_id:
description: Instance refresh id.
returned: success
type: str
sample: "08b91cf7-8fa6-48af-b6a6-d227f40f1b9b"
auto_scaling_group_name:
description: Name of autoscaling group.
returned: success
type: str
sample: "public-webapp-production-1"
status:
description:
- The current state of the group when DeleteAutoScalingGroup is in progress.
- The following are the possible statuses
- Pending - The request was created, but the operation has not started.
- InProgress - The operation is in progress.
- Successful - The operation completed successfully.
- Failed - The operation failed to complete.
You can troubleshoot using the status reason and the scaling activities.
- Cancelling - An ongoing operation is being cancelled.
Cancellation does not roll back any replacements that have already been
completed, but it prevents new replacements from being started.
- Cancelled - The operation is cancelled.
returned: success
type: str
sample: "Pending"
preferences:
description: The preferences for an instance refresh.
returned: always
type: dict
sample: {
'AlarmSpecification': {
'Alarms': [
'my-alarm',
],
},
'AutoRollback': True,
'InstanceWarmup': 200,
'MinHealthyPercentage': 90,
'ScaleInProtectedInstances': 'Ignore',
'SkipMatching': False,
'StandbyInstances': 'Ignore',
}
start_time:
description: The date and time this ASG was created, in ISO 8601 format.
returned: success
type: str
sample: "2015-11-25T00:05:36.309Z"
end_time:
description: The date and time this ASG was created, in ISO 8601 format.
returned: success
type: str
sample: "2015-11-25T00:05:36.309Z"
percentage_complete:
description: the % of completeness.
returned: success
type: int
sample: 100
instances_to_update:
description: number of instances to update.
returned: success
type: int
sample: 5
"""

try:
from botocore.exceptions import BotoCoreError
from botocore.exceptions import ClientError
except ImportError:
pass # caught by AnsibleAWSModule
from typing import Dict
from typing import Optional
from typing import Union

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict

from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.autoscaling import AnsibleAutoScalingError
from ansible_collections.amazon.aws.plugins.module_utils.autoscaling import cancel_instance_refresh
from ansible_collections.amazon.aws.plugins.module_utils.autoscaling import describe_instance_refreshes
from ansible_collections.amazon.aws.plugins.module_utils.autoscaling import start_instance_refresh
from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters

from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule


def start_or_cancel_instance_refresh(conn, module):
def validate_healthy_percentage(preferences: Dict[str, Union[bool, int]]) -> Optional[str]:
min_healthy_percentage = preferences.get("min_healthy_percentage")
max_healthy_percentage = preferences.get("max_healthy_percentage")

if min_healthy_percentage is not None and (min_healthy_percentage < 0 or min_healthy_percentage > 100):
return "The value range for the min_healthy_percentage is 0 to 100."
if max_healthy_percentage is not None:
if max_healthy_percentage < 100 or max_healthy_percentage > 200:
return "The value range for the max_healthy_percentage is 100 to 200."
if min_healthy_percentage is None:
return "You must also specify min_healthy_percentage when max_healthy_percentage is specified."
if (max_healthy_percentage - min_healthy_percentage) > 100:
return "The difference between the max_healthy_percentage and min_healthy_percentage cannot be greater than 100."
return None


def start_or_cancel_instance_refresh(conn, module: AnsibleAWSModule) -> None:
"""
Args:
conn (boto3.AutoScaling.Client): Valid Boto3 ASG client.
module: AnsibleAWSModule object
Returns:
{
"instance_refreshes": [
{
'auto_scaling_group_name': 'ansible-test-hermes-63642726-asg',
'instance_refresh_id': '6507a3e5-4950-4503-8978-e9f2636efc09',
'instances_to_update': 1,
'percentage_complete': 0,
"preferences": {
"instance_warmup": 60,
"min_healthy_percentage": 90,
"skip_matching": false
},
'start_time': '2021-02-04T03:39:40+00:00',
'status': 'Cancelling',
'status_reason': 'Replacing instances before cancelling.',
}
]
}
"""

asg_state = module.params.get("state")
asg_name = module.params.get("name")
preferences = module.params.get("preferences")

args = {}
args["AutoScalingGroupName"] = asg_name
if asg_state == "started":
args["Strategy"] = module.params.get("strategy")
if preferences:
if asg_state == "cancelled":
module.fail_json(msg="can not pass preferences dict when canceling a refresh")
_prefs = scrub_none_parameters(preferences)
args["Preferences"] = snake_dict_to_camel_dict(_prefs, capitalize_first=True)
error = validate_healthy_percentage(preferences)
if error:
module.fail_json(msg=error)
args["Preferences"] = snake_dict_to_camel_dict(scrub_none_parameters(preferences), capitalize_first=True)
cmd_invocations = {
"cancelled": conn.cancel_instance_refresh,
"started": conn.start_instance_refresh,
"cancelled": cancel_instance_refresh,
"started": start_instance_refresh,
}
try:
if module.check_mode:
ongoing_refresh = describe_instance_refreshes(conn, auto_scaling_group_name=asg_name).get(
"InstanceRefreshes", []
)
if asg_state == "started":
ongoing_refresh = conn.describe_instance_refreshes(AutoScalingGroupName=asg_name).get(
"InstanceRefreshes", "[]"
)
if ongoing_refresh:
module.exit_json(
changed=False,
Expand All @@ -209,26 +245,23 @@ def start_or_cancel_instance_refresh(conn, module):
else:
module.exit_json(changed=True, msg="Would have started instance refresh if not in check mode.")
elif asg_state == "cancelled":
ongoing_refresh = conn.describe_instance_refreshes(AutoScalingGroupName=asg_name).get(
"InstanceRefreshes", "[]"
)[0]
if ongoing_refresh.get("Status", "") in ["Cancelling", "Cancelled"]:
if ongoing_refresh and ongoing_refresh[0].get("Status", "") in ["Cancelling", "Cancelled"]:
module.exit_json(
changed=False,
msg="In check_mode - Instance Refresh already cancelled or is pending cancellation.",
)
elif not ongoing_refresh:
module.exit_json(chaned=False, msg="In check_mode - No active referesh found, nothing to cancel.")
module.exit_json(changed=False, msg="In check_mode - No active referesh found, nothing to cancel.")
else:
module.exit_json(changed=True, msg="Would have cancelled instance refresh if not in check mode.")
result = cmd_invocations[asg_state](aws_retry=True, **args)
instance_refreshes = conn.describe_instance_refreshes(
AutoScalingGroupName=asg_name, InstanceRefreshIds=[result["InstanceRefreshId"]]
instance_refresh_id = cmd_invocations[asg_state](conn, auto_scaling_group_name=asg_name, **args)
response = describe_instance_refreshes(
conn, auto_scaling_group_name=asg_name, instance_refresh_ids=[instance_refresh_id]
)
result = dict(instance_refreshes=camel_dict_to_snake_dict(instance_refreshes["InstanceRefreshes"][0]))
return module.exit_json(**result)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Failed to {asg_state.replace('ed', '')} InstanceRefresh")
result = dict(instance_refreshes=camel_dict_to_snake_dict(response["InstanceRefreshes"][0]))
module.exit_json(**result)
except AnsibleAutoScalingError as e:
module.fail_json_aws(e, msg=f"Failed to {asg_state.replace('ed', '')} InstanceRefresh: {e}")


def main():
Expand All @@ -246,6 +279,8 @@ def main():
options=dict(
min_healthy_percentage=dict(type="int", default=90),
instance_warmup=dict(type="int"),
skip_matching=dict(type="bool"),
max_healthy_percentage=dict(type="int"),
),
),
)
Expand All @@ -254,10 +289,7 @@ def main():
argument_spec=argument_spec,
supports_check_mode=True,
)
autoscaling = module.client(
"autoscaling",
retry_decorator=AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=["InstanceRefreshInProgress"]),
)
autoscaling = module.client("autoscaling")

start_or_cancel_instance_refresh(autoscaling, module)

Expand Down
Loading

0 comments on commit e699fa1

Please sign in to comment.