my ansible modules which are stock in pull request
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1716 lines
74KB

  1. #!/usr/bin/python
  2. # This file is part of Ansible
  3. #
  4. # Ansible is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # Ansible is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  16. ANSIBLE_METADATA = {'metadata_version': '1.1',
  17. 'status': ['stableinterface'],
  18. 'supported_by': 'community'}
  19. DOCUMENTATION = """
  20. ---
  21. module: ec2_asg
  22. short_description: Create or delete AWS Autoscaling Groups
  23. description:
  24. - Can create or delete AWS Autoscaling Groups
  25. - Can be used with the ec2_lc module to manage Launch Configurations
  26. version_added: "1.6"
  27. author: "Gareth Rushgrove (@garethr)"
  28. requirements: [ "boto3", "botocore" ]
  29. options:
  30. state:
  31. description:
  32. - register or deregister the instance
  33. choices: ['present', 'absent']
  34. default: present
  35. name:
  36. description:
  37. - Unique name for group to be created or deleted
  38. required: true
  39. load_balancers:
  40. description:
  41. - List of ELB names to use for the group. Use for classic load balancers.
  42. target_group_arns:
  43. description:
  44. - List of target group ARNs to use for the group. Use for application load balancers.
  45. version_added: "2.4"
  46. availability_zones:
  47. description:
  48. - List of availability zone names in which to create the group. Defaults to all the availability zones in the region if vpc_zone_identifier is not set.
  49. launch_config_name:
  50. description:
  51. - Name of the Launch configuration to use for the group. See the ec2_lc module for managing these.
  52. If unspecified then the current group value will be used. One of launch_config_name or launch_template must be provided.
  53. launch_template:
  54. description:
  55. - Dictionary describing the Launch Template to use
  56. suboptions:
  57. version:
  58. description:
  59. - The version number of the launch template to use. Defaults to latest version if not provided.
  60. default: "latest"
  61. launch_template_name:
  62. description:
  63. - The name of the launch template. Only one of launch_template_name or launch_template_id is required.
  64. launch_template_id:
  65. description:
  66. - The id of the launch template. Only one of launch_template_name or launch_template_id is required.
  67. version_added: "2.8"
  68. min_size:
  69. description:
  70. - Minimum number of instances in group, if unspecified then the current group value will be used.
  71. max_size:
  72. description:
  73. - Maximum number of instances in group, if unspecified then the current group value will be used.
  74. mixed_instances_policy:
  75. description:
  76. - Using mixed intances policy while ASG present
  77. required: false
  78. version_added: "2.9"
  79. suboptions:
  80. instance_types:
  81. description:
  82. - A list of instance_types.
  83. placement_group:
  84. description:
  85. - Physical location of your cluster placement group created in Amazon EC2.
  86. version_added: "2.3"
  87. desired_capacity:
  88. description:
  89. - Desired number of instances in group, if unspecified then the current group value will be used.
  90. replace_all_instances:
  91. description:
  92. - In a rolling fashion, replace all instances that used the old launch configuration with one from the new launch configuration.
  93. It increases the ASG size by C(replace_batch_size), waits for the new instances to be up and running.
  94. After that, it terminates a batch of old instances, waits for the replacements, and repeats, until all old instances are replaced.
  95. Once that's done the ASG size is reduced back to the expected size.
  96. version_added: "1.8"
  97. default: 'no'
  98. type: bool
  99. replace_batch_size:
  100. description:
  101. - Number of instances you'd like to replace at a time. Used with replace_all_instances.
  102. required: false
  103. version_added: "1.8"
  104. default: 1
  105. replace_instances:
  106. description:
  107. - List of instance_ids belonging to the named ASG that you would like to terminate and be replaced with instances matching the current launch
  108. configuration.
  109. version_added: "1.8"
  110. lc_check:
  111. description:
  112. - Check to make sure instances that are being replaced with replace_instances do not already have the current launch_config.
  113. version_added: "1.8"
  114. default: 'yes'
  115. type: bool
  116. lt_check:
  117. description:
  118. - Check to make sure instances that are being replaced with replace_instances do not already have the current launch_template or launch_template version.
  119. version_added: "2.8"
  120. default: 'yes'
  121. type: bool
  122. vpc_zone_identifier:
  123. description:
  124. - List of VPC subnets to use
  125. tags:
  126. description:
  127. - A list of tags to add to the Auto Scale Group. Optional key is 'propagate_at_launch', which defaults to true.
  128. version_added: "1.7"
  129. health_check_period:
  130. description:
  131. - Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
  132. required: false
  133. default: 300 seconds
  134. version_added: "1.7"
  135. health_check_type:
  136. description:
  137. - The service you want the health status from, Amazon EC2 or Elastic Load Balancer.
  138. required: false
  139. default: EC2
  140. version_added: "1.7"
  141. choices: ['EC2', 'ELB']
  142. default_cooldown:
  143. description:
  144. - The number of seconds after a scaling activity completes before another can begin.
  145. default: 300 seconds
  146. version_added: "2.0"
  147. wait_timeout:
  148. description:
  149. - How long to wait for instances to become viable when replaced. If you experience the error "Waited too long for ELB instances to be healthy",
  150. try increasing this value.
  151. default: 300
  152. version_added: "1.8"
  153. wait_for_instances:
  154. description:
  155. - Wait for the ASG instances to be in a ready state before exiting. If instances are behind an ELB, it will wait until the ELB determines all
  156. instances have a lifecycle_state of "InService" and a health_status of "Healthy".
  157. version_added: "1.9"
  158. default: 'yes'
  159. type: bool
  160. termination_policies:
  161. description:
  162. - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity.
  163. - For 'Default', when used to create a new autoscaling group, the "Default"i value is used. When used to change an existent autoscaling group, the
  164. current termination policies are maintained.
  165. default: Default
  166. choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default']
  167. version_added: "2.0"
  168. notification_topic:
  169. description:
  170. - A SNS topic ARN to send auto scaling notifications to.
  171. version_added: "2.2"
  172. notification_types:
  173. description:
  174. - A list of auto scaling events to trigger notifications on.
  175. default:
  176. - 'autoscaling:EC2_INSTANCE_LAUNCH'
  177. - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR'
  178. - 'autoscaling:EC2_INSTANCE_TERMINATE'
  179. - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'
  180. required: false
  181. version_added: "2.2"
  182. suspend_processes:
  183. description:
  184. - A list of scaling processes to suspend.
  185. default: []
  186. choices: ['Launch', 'Terminate', 'HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions', 'AddToLoadBalancer']
  187. version_added: "2.3"
  188. metrics_collection:
  189. description:
  190. - Enable ASG metrics collection
  191. type: bool
  192. default: 'no'
  193. version_added: "2.6"
  194. metrics_granularity:
  195. description:
  196. - When metrics_collection is enabled this will determine granularity of metrics collected by CloudWatch
  197. default: "1minute"
  198. version_added: "2.6"
  199. metrics_list:
  200. description:
  201. - List of autoscaling metrics to collect when enabling metrics_collection
  202. default:
  203. - 'GroupMinSize'
  204. - 'GroupMaxSize'
  205. - 'GroupDesiredCapacity'
  206. - 'GroupInServiceInstances'
  207. - 'GroupPendingInstances'
  208. - 'GroupStandbyInstances'
  209. - 'GroupTerminatingInstances'
  210. - 'GroupTotalInstances'
  211. version_added: "2.6"
  212. extends_documentation_fragment:
  213. - aws
  214. - ec2
  215. """
  216. EXAMPLES = '''
  217. # Basic configuration with Launch Configuration
  218. - ec2_asg:
  219. name: special
  220. load_balancers: [ 'lb1', 'lb2' ]
  221. availability_zones: [ 'eu-west-1a', 'eu-west-1b' ]
  222. launch_config_name: 'lc-1'
  223. min_size: 1
  224. max_size: 10
  225. desired_capacity: 5
  226. vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ]
  227. tags:
  228. - environment: production
  229. propagate_at_launch: no
  230. # Rolling ASG Updates
  231. # Below is an example of how to assign a new launch config to an ASG and terminate old instances.
  232. #
  233. # All instances in "myasg" that do not have the launch configuration named "my_new_lc" will be terminated in
  234. # a rolling fashion with instances using the current launch configuration, "my_new_lc".
  235. #
  236. # This could also be considered a rolling deploy of a pre-baked AMI.
  237. #
  238. # If this is a newly created group, the instances will not be replaced since all instances
  239. # will have the current launch configuration.
  240. - name: create launch config
  241. ec2_lc:
  242. name: my_new_lc
  243. image_id: ami-lkajsf
  244. key_name: mykey
  245. region: us-east-1
  246. security_groups: sg-23423
  247. instance_type: m1.small
  248. assign_public_ip: yes
  249. - ec2_asg:
  250. name: myasg
  251. launch_config_name: my_new_lc
  252. health_check_period: 60
  253. health_check_type: ELB
  254. replace_all_instances: yes
  255. min_size: 5
  256. max_size: 5
  257. desired_capacity: 5
  258. region: us-east-1
  259. # To only replace a couple of instances instead of all of them, supply a list
  260. # to "replace_instances":
  261. - ec2_asg:
  262. name: myasg
  263. launch_config_name: my_new_lc
  264. health_check_period: 60
  265. health_check_type: ELB
  266. replace_instances:
  267. - i-b345231
  268. - i-24c2931
  269. min_size: 5
  270. max_size: 5
  271. desired_capacity: 5
  272. region: us-east-1
  273. # Basic Configuration with Launch Template
  274. - ec2_asg:
  275. name: special
  276. load_balancers: [ 'lb1', 'lb2' ]
  277. availability_zones: [ 'eu-west-1a', 'eu-west-1b' ]
  278. launch_template:
  279. version: '1'
  280. launch_template_name: 'lt-example'
  281. launch_template_id: 'lt-123456'
  282. min_size: 1
  283. max_size: 10
  284. desired_capacity: 5
  285. vpc_zone_identifier: [ 'subnet-abcd1234', 'subnet-1a2b3c4d' ]
  286. tags:
  287. - environment: production
  288. propagate_at_launch: no
  289. '''
  290. RETURN = '''
  291. ---
  292. auto_scaling_group_name:
  293. description: The unique name of the auto scaling group
  294. returned: success
  295. type: str
  296. sample: "myasg"
  297. auto_scaling_group_arn:
  298. description: The unique ARN of the autoscaling group
  299. returned: success
  300. type: str
  301. sample: "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:6a09ad6d-eeee-1234-b987-ee123ced01ad:autoScalingGroupName/myasg"
  302. availability_zones:
  303. description: The availability zones for the auto scaling group
  304. returned: success
  305. type: list
  306. sample: [
  307. "us-east-1d"
  308. ]
  309. created_time:
  310. description: Timestamp of create time of the auto scaling group
  311. returned: success
  312. type: str
  313. sample: "2017-11-08T14:41:48.272000+00:00"
  314. default_cooldown:
  315. description: The default cooldown time in seconds.
  316. returned: success
  317. type: int
  318. sample: 300
  319. desired_capacity:
  320. description: The number of EC2 instances that should be running in this group.
  321. returned: success
  322. type: int
  323. sample: 3
  324. healthcheck_period:
  325. description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health.
  326. returned: success
  327. type: int
  328. sample: 30
  329. healthcheck_type:
  330. description: The service you want the health status from, one of "EC2" or "ELB".
  331. returned: success
  332. type: str
  333. sample: "ELB"
  334. healthy_instances:
  335. description: Number of instances in a healthy state
  336. returned: success
  337. type: int
  338. sample: 5
  339. in_service_instances:
  340. description: Number of instances in service
  341. returned: success
  342. type: int
  343. sample: 3
  344. instance_facts:
  345. description: Dictionary of EC2 instances and their status as it relates to the ASG.
  346. returned: success
  347. type: dict
  348. sample: {
  349. "i-0123456789012": {
  350. "health_status": "Healthy",
  351. "launch_config_name": "public-webapp-production-1",
  352. "lifecycle_state": "InService"
  353. }
  354. }
  355. instances:
  356. description: list of instance IDs in the ASG
  357. returned: success
  358. type: list
  359. sample: [
  360. "i-0123456789012"
  361. ]
  362. launch_config_name:
  363. description: >
  364. Name of launch configuration associated with the ASG. Same as launch_configuration_name,
  365. provided for compatibility with ec2_asg module.
  366. returned: success
  367. type: str
  368. sample: "public-webapp-production-1"
  369. load_balancers:
  370. description: List of load balancers names attached to the ASG.
  371. returned: success
  372. type: list
  373. sample: ["elb-webapp-prod"]
  374. max_size:
  375. description: Maximum size of group
  376. returned: success
  377. type: int
  378. sample: 3
  379. min_size:
  380. description: Minimum size of group
  381. returned: success
  382. type: int
  383. sample: 1
  384. pending_instances:
  385. description: Number of instances in pending state
  386. returned: success
  387. type: int
  388. sample: 1
  389. tags:
  390. description: List of tags for the ASG, and whether or not each tag propagates to instances at launch.
  391. returned: success
  392. type: list
  393. sample: [
  394. {
  395. "key": "Name",
  396. "value": "public-webapp-production-1",
  397. "resource_id": "public-webapp-production-1",
  398. "resource_type": "auto-scaling-group",
  399. "propagate_at_launch": "true"
  400. },
  401. {
  402. "key": "env",
  403. "value": "production",
  404. "resource_id": "public-webapp-production-1",
  405. "resource_type": "auto-scaling-group",
  406. "propagate_at_launch": "true"
  407. }
  408. ]
  409. target_group_arns:
  410. description: List of ARNs of the target groups that the ASG populates
  411. returned: success
  412. type: list
  413. sample: [
  414. "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-host-hello/1a2b3c4d5e6f1a2b",
  415. "arn:aws:elasticloadbalancing:ap-southeast-2:123456789012:targetgroup/target-group-path-world/abcd1234abcd1234"
  416. ]
  417. target_group_names:
  418. description: List of names of the target groups that the ASG populates
  419. returned: success
  420. type: list
  421. sample: [
  422. "target-group-host-hello",
  423. "target-group-path-world"
  424. ]
  425. termination_policies:
  426. description: A list of termination policies for the group.
  427. returned: success
  428. type: str
  429. sample: ["Default"]
  430. unhealthy_instances:
  431. description: Number of instances in an unhealthy state
  432. returned: success
  433. type: int
  434. sample: 0
  435. viable_instances:
  436. description: Number of instances in a viable state
  437. returned: success
  438. type: int
  439. sample: 1
  440. vpc_zone_identifier:
  441. description: VPC zone ID / subnet id for the auto scaling group
  442. returned: success
  443. type: str
  444. sample: "subnet-a31ef45f"
  445. metrics_collection:
  446. description: List of enabled AutosSalingGroup metrics
  447. returned: success
  448. type: list
  449. sample: [
  450. {
  451. "Granularity": "1Minute",
  452. "Metric": "GroupInServiceInstances"
  453. }
  454. ]
  455. '''
  456. import time
  457. import traceback
  458. from ansible.module_utils._text import to_native
  459. from ansible.module_utils.basic import AnsibleModule
  460. from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, HAS_BOTO3, camel_dict_to_snake_dict, get_aws_connection_info, AWSRetry
  461. try:
  462. import botocore
  463. except ImportError:
  464. pass # will be detected by imported HAS_BOTO3
  465. ASG_ATTRIBUTES = ('AvailabilityZones', 'DefaultCooldown', 'DesiredCapacity',
  466. 'HealthCheckGracePeriod', 'HealthCheckType', 'LaunchConfigurationName',
  467. 'LoadBalancerNames', 'MaxSize', 'MinSize', 'AutoScalingGroupName', 'PlacementGroup',
  468. 'TerminationPolicies', 'VPCZoneIdentifier')
  469. INSTANCE_ATTRIBUTES = ('instance_id', 'health_status', 'lifecycle_state', 'launch_config_name')
  470. backoff_params = dict(tries=10, delay=3, backoff=1.5)
  471. @AWSRetry.backoff(**backoff_params)
  472. def describe_autoscaling_groups(connection, group_name):
  473. pg = connection.get_paginator('describe_auto_scaling_groups')
  474. return pg.paginate(AutoScalingGroupNames=[group_name]).build_full_result().get('AutoScalingGroups', [])
  475. @AWSRetry.backoff(**backoff_params)
  476. def deregister_lb_instances(connection, lb_name, instance_id):
  477. connection.deregister_instances_from_load_balancer(LoadBalancerName=lb_name, Instances=[dict(InstanceId=instance_id)])
  478. @AWSRetry.backoff(**backoff_params)
  479. def describe_instance_health(connection, lb_name, instances):
  480. params = dict(LoadBalancerName=lb_name)
  481. if instances:
  482. params.update(Instances=instances)
  483. return connection.describe_instance_health(**params)
  484. @AWSRetry.backoff(**backoff_params)
  485. def describe_target_health(connection, target_group_arn, instances):
  486. return connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=instances)
  487. @AWSRetry.backoff(**backoff_params)
  488. def suspend_asg_processes(connection, asg_name, processes):
  489. connection.suspend_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes)
  490. @AWSRetry.backoff(**backoff_params)
  491. def resume_asg_processes(connection, asg_name, processes):
  492. connection.resume_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes)
  493. @AWSRetry.backoff(**backoff_params)
  494. def describe_launch_configurations(connection, launch_config_name):
  495. pg = connection.get_paginator('describe_launch_configurations')
  496. return pg.paginate(LaunchConfigurationNames=[launch_config_name]).build_full_result()
  497. @AWSRetry.backoff(**backoff_params)
  498. def describe_launch_templates(connection, launch_template):
  499. if launch_template['launch_template_id'] is not None:
  500. try:
  501. lt = connection.describe_launch_templates(LaunchTemplateIds=[launch_template['launch_template_id']])
  502. return lt
  503. except (botocore.exceptions.ClientError) as e:
  504. module.fail_json(msg="No launch template found matching: %s" % launch_template)
  505. else:
  506. try:
  507. lt = connection.describe_launch_templates(LaunchTemplateNames=[launch_template['launch_template_name']])
  508. return lt
  509. except (botocore.exceptions.ClientError) as e:
  510. module.fail_json(msg="No launch template found matching: %s" % launch_template)
  511. @AWSRetry.backoff(**backoff_params)
  512. def create_asg(connection, **params):
  513. connection.create_auto_scaling_group(**params)
  514. @AWSRetry.backoff(**backoff_params)
  515. def put_notification_config(connection, asg_name, topic_arn, notification_types):
  516. connection.put_notification_configuration(
  517. AutoScalingGroupName=asg_name,
  518. TopicARN=topic_arn,
  519. NotificationTypes=notification_types
  520. )
  521. @AWSRetry.backoff(**backoff_params)
  522. def del_notification_config(connection, asg_name, topic_arn):
  523. connection.delete_notification_configuration(
  524. AutoScalingGroupName=asg_name,
  525. TopicARN=topic_arn
  526. )
  527. @AWSRetry.backoff(**backoff_params)
  528. def attach_load_balancers(connection, asg_name, load_balancers):
  529. connection.attach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers)
  530. @AWSRetry.backoff(**backoff_params)
  531. def detach_load_balancers(connection, asg_name, load_balancers):
  532. connection.detach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers)
  533. @AWSRetry.backoff(**backoff_params)
  534. def attach_lb_target_groups(connection, asg_name, target_group_arns):
  535. connection.attach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns)
  536. @AWSRetry.backoff(**backoff_params)
  537. def detach_lb_target_groups(connection, asg_name, target_group_arns):
  538. connection.detach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns)
  539. @AWSRetry.backoff(**backoff_params)
  540. def update_asg(connection, **params):
  541. connection.update_auto_scaling_group(**params)
  542. @AWSRetry.backoff(catch_extra_error_codes=['ScalingActivityInProgress'], **backoff_params)
  543. def delete_asg(connection, asg_name, force_delete):
  544. connection.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=force_delete)
  545. @AWSRetry.backoff(**backoff_params)
  546. def terminate_asg_instance(connection, instance_id, decrement_capacity):
  547. connection.terminate_instance_in_auto_scaling_group(InstanceId=instance_id,
  548. ShouldDecrementDesiredCapacity=decrement_capacity)
  549. def enforce_required_arguments_for_create():
  550. ''' As many arguments are not required for autoscale group deletion
  551. they cannot be mandatory arguments for the module, so we enforce
  552. them here '''
  553. missing_args = []
  554. if module.params.get('launch_config_name') is None and module.params.get('launch_template') is None:
  555. module.fail_json(msg="Missing either launch_config_name or launch_template for autoscaling group create")
  556. for arg in ('min_size', 'max_size'):
  557. if module.params[arg] is None:
  558. missing_args.append(arg)
  559. if missing_args:
  560. module.fail_json(msg="Missing required arguments for autoscaling group create: %s" % ",".join(missing_args))
  561. def get_properties(autoscaling_group):
  562. properties = dict()
  563. properties['healthy_instances'] = 0
  564. properties['in_service_instances'] = 0
  565. properties['unhealthy_instances'] = 0
  566. properties['pending_instances'] = 0
  567. properties['viable_instances'] = 0
  568. properties['terminating_instances'] = 0
  569. instance_facts = dict()
  570. autoscaling_group_instances = autoscaling_group.get('Instances')
  571. if autoscaling_group_instances:
  572. properties['instances'] = [i['InstanceId'] for i in autoscaling_group_instances]
  573. for i in autoscaling_group_instances:
  574. if i.get('LaunchConfigurationName'):
  575. instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
  576. 'lifecycle_state': i['LifecycleState'],
  577. 'launch_config_name': i['LaunchConfigurationName']}
  578. elif i.get('LaunchTemplate'):
  579. instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
  580. 'lifecycle_state': i['LifecycleState'],
  581. 'launch_template': i['LaunchTemplate']}
  582. else:
  583. instance_facts[i['InstanceId']] = {'health_status': i['HealthStatus'],
  584. 'lifecycle_state': i['LifecycleState']}
  585. if i['HealthStatus'] == 'Healthy' and i['LifecycleState'] == 'InService':
  586. properties['viable_instances'] += 1
  587. if i['HealthStatus'] == 'Healthy':
  588. properties['healthy_instances'] += 1
  589. else:
  590. properties['unhealthy_instances'] += 1
  591. if i['LifecycleState'] == 'InService':
  592. properties['in_service_instances'] += 1
  593. if i['LifecycleState'] == 'Terminating':
  594. properties['terminating_instances'] += 1
  595. if i['LifecycleState'] == 'Pending':
  596. properties['pending_instances'] += 1
  597. else:
  598. properties['instances'] = []
  599. properties['auto_scaling_group_name'] = autoscaling_group.get('AutoScalingGroupName')
  600. properties['auto_scaling_group_arn'] = autoscaling_group.get('AutoScalingGroupARN')
  601. properties['availability_zones'] = autoscaling_group.get('AvailabilityZones')
  602. properties['created_time'] = autoscaling_group.get('CreatedTime')
  603. properties['instance_facts'] = instance_facts
  604. properties['load_balancers'] = autoscaling_group.get('LoadBalancerNames')
  605. if autoscaling_group.get('LaunchConfigurationName'):
  606. properties['launch_config_name'] = autoscaling_group.get('LaunchConfigurationName')
  607. else:
  608. properties['launch_template'] = autoscaling_group.get('LaunchTemplate')
  609. properties['tags'] = autoscaling_group.get('Tags')
  610. properties['min_size'] = autoscaling_group.get('MinSize')
  611. properties['max_size'] = autoscaling_group.get('MaxSize')
  612. properties['desired_capacity'] = autoscaling_group.get('DesiredCapacity')
  613. properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown')
  614. properties['healthcheck_grace_period'] = autoscaling_group.get('HealthCheckGracePeriod')
  615. properties['healthcheck_type'] = autoscaling_group.get('HealthCheckType')
  616. properties['default_cooldown'] = autoscaling_group.get('DefaultCooldown')
  617. properties['termination_policies'] = autoscaling_group.get('TerminationPolicies')
  618. properties['target_group_arns'] = autoscaling_group.get('TargetGroupARNs')
  619. properties['vpc_zone_identifier'] = autoscaling_group.get('VPCZoneIdentifier')
  620. properties['metrics_collection'] = autoscaling_group.get('EnabledMetrics')
  621. if properties['target_group_arns']:
  622. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  623. elbv2_connection = boto3_conn(module,
  624. conn_type='client',
  625. resource='elbv2',
  626. region=region,
  627. endpoint=ec2_url,
  628. **aws_connect_params)
  629. tg_paginator = elbv2_connection.get_paginator('describe_target_groups')
  630. tg_result = tg_paginator.paginate(TargetGroupArns=properties['target_group_arns']).build_full_result()
  631. target_groups = tg_result['TargetGroups']
  632. else:
  633. target_groups = []
  634. properties['target_group_names'] = [tg['TargetGroupName'] for tg in target_groups]
  635. return properties
  636. def get_launch_object(connection, ec2_connection):
  637. launch_object = dict()
  638. launch_config_name = module.params.get('launch_config_name')
  639. launch_template = module.params.get('launch_template')
  640. mixed_instances_policy = module.params.get('mixed_instances_policy')
  641. if launch_config_name is None and launch_template is None:
  642. return launch_object
  643. elif launch_config_name:
  644. try:
  645. launch_configs = describe_launch_configurations(connection, launch_config_name)
  646. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  647. module.fail_json(msg="Failed to describe launch configurations",
  648. exception=traceback.format_exc())
  649. if len(launch_configs['LaunchConfigurations']) == 0:
  650. module.fail_json(msg="No launch config found with name %s" % launch_config_name)
  651. launch_object = {"LaunchConfigurationName": launch_configs['LaunchConfigurations'][0]['LaunchConfigurationName']}
  652. return launch_object
  653. elif launch_template:
  654. lt = describe_launch_templates(ec2_connection, launch_template)['LaunchTemplates'][0]
  655. if launch_template['version'] is not None:
  656. launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": launch_template['version']}}
  657. else:
  658. launch_object = {"LaunchTemplate": {"LaunchTemplateId": lt['LaunchTemplateId'], "Version": str(lt['LatestVersionNumber'])}}
  659. if mixed_instances_policy:
  660. instance_types = mixed_instances_policy.get('instance_types', [])
  661. policy = {
  662. 'LaunchTemplate': {
  663. 'LaunchTemplateSpecification': launch_object["LaunchTemplate"]
  664. }
  665. }
  666. if instance_types:
  667. policy['LaunchTemplate']['Overrides'] = []
  668. for instance_type in instance_types:
  669. instance_type_dict = {'InstanceType': instance_type}
  670. policy['LaunchTemplate']['Overrides'].append(instance_type_dict)
  671. launch_object['MixedInstancesPolicy'] = policy
  672. return launch_object
  673. def elb_dreg(asg_connection, group_name, instance_id):
  674. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  675. as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
  676. wait_timeout = module.params.get('wait_timeout')
  677. count = 1
  678. if as_group['LoadBalancerNames'] and as_group['HealthCheckType'] == 'ELB':
  679. elb_connection = boto3_conn(module,
  680. conn_type='client',
  681. resource='elb',
  682. region=region,
  683. endpoint=ec2_url,
  684. **aws_connect_params)
  685. else:
  686. return
  687. for lb in as_group['LoadBalancerNames']:
  688. deregister_lb_instances(elb_connection, lb, instance_id)
  689. module.debug("De-registering %s from ELB %s" % (instance_id, lb))
  690. wait_timeout = time.time() + wait_timeout
  691. while wait_timeout > time.time() and count > 0:
  692. count = 0
  693. for lb in as_group['LoadBalancerNames']:
  694. lb_instances = describe_instance_health(elb_connection, lb, [])
  695. for i in lb_instances['InstanceStates']:
  696. if i['InstanceId'] == instance_id and i['State'] == "InService":
  697. count += 1
  698. module.debug("%s: %s, %s" % (i['InstanceId'], i['State'], i['Description']))
  699. time.sleep(10)
  700. if wait_timeout <= time.time():
  701. # waiting took too long
  702. module.fail_json(msg="Waited too long for instance to deregister. {0}".format(time.asctime()))
  703. def elb_healthy(asg_connection, elb_connection, group_name):
  704. healthy_instances = set()
  705. as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
  706. props = get_properties(as_group)
  707. # get healthy, inservice instances from ASG
  708. instances = []
  709. for instance, settings in props['instance_facts'].items():
  710. if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy':
  711. instances.append(dict(InstanceId=instance))
  712. module.debug("ASG considers the following instances InService and Healthy: %s" % instances)
  713. module.debug("ELB instance status:")
  714. lb_instances = list()
  715. for lb in as_group.get('LoadBalancerNames'):
  716. # we catch a race condition that sometimes happens if the instance exists in the ASG
  717. # but has not yet show up in the ELB
  718. try:
  719. lb_instances = describe_instance_health(elb_connection, lb, instances)
  720. except botocore.exceptions.ClientError as e:
  721. if e.response['Error']['Code'] == 'InvalidInstance':
  722. return None
  723. module.fail_json(msg="Failed to get load balancer.",
  724. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  725. except botocore.exceptions.BotoCoreError as e:
  726. module.fail_json(msg="Failed to get load balancer.",
  727. exception=traceback.format_exc())
  728. for i in lb_instances.get('InstanceStates'):
  729. if i['State'] == "InService":
  730. healthy_instances.add(i['InstanceId'])
  731. module.debug("ELB Health State %s: %s" % (i['InstanceId'], i['State']))
  732. return len(healthy_instances)
  733. def tg_healthy(asg_connection, elbv2_connection, group_name):
  734. healthy_instances = set()
  735. as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
  736. props = get_properties(as_group)
  737. # get healthy, inservice instances from ASG
  738. instances = []
  739. for instance, settings in props['instance_facts'].items():
  740. if settings['lifecycle_state'] == 'InService' and settings['health_status'] == 'Healthy':
  741. instances.append(dict(Id=instance))
  742. module.debug("ASG considers the following instances InService and Healthy: %s" % instances)
  743. module.debug("Target Group instance status:")
  744. tg_instances = list()
  745. for tg in as_group.get('TargetGroupARNs'):
  746. # we catch a race condition that sometimes happens if the instance exists in the ASG
  747. # but has not yet show up in the ELB
  748. try:
  749. tg_instances = describe_target_health(elbv2_connection, tg, instances)
  750. except botocore.exceptions.ClientError as e:
  751. if e.response['Error']['Code'] == 'InvalidInstance':
  752. return None
  753. module.fail_json(msg="Failed to get target group.",
  754. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  755. except botocore.exceptions.BotoCoreError as e:
  756. module.fail_json(msg="Failed to get target group.",
  757. exception=traceback.format_exc())
  758. for i in tg_instances.get('TargetHealthDescriptions'):
  759. if i['TargetHealth']['State'] == "healthy":
  760. healthy_instances.add(i['Target']['Id'])
  761. module.debug("Target Group Health State %s: %s" % (i['Target']['Id'], i['TargetHealth']['State']))
  762. return len(healthy_instances)
  763. def wait_for_elb(asg_connection, group_name):
  764. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  765. wait_timeout = module.params.get('wait_timeout')
  766. # if the health_check_type is ELB, we want to query the ELBs directly for instance
  767. # status as to avoid health_check_grace period that is awarded to ASG instances
  768. as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
  769. if as_group.get('LoadBalancerNames') and as_group.get('HealthCheckType') == 'ELB':
  770. module.debug("Waiting for ELB to consider instances healthy.")
  771. elb_connection = boto3_conn(module,
  772. conn_type='client',
  773. resource='elb',
  774. region=region,
  775. endpoint=ec2_url,
  776. **aws_connect_params)
  777. wait_timeout = time.time() + wait_timeout
  778. healthy_instances = elb_healthy(asg_connection, elb_connection, group_name)
  779. while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time():
  780. healthy_instances = elb_healthy(asg_connection, elb_connection, group_name)
  781. module.debug("ELB thinks %s instances are healthy." % healthy_instances)
  782. time.sleep(10)
  783. if wait_timeout <= time.time():
  784. # waiting took too long
  785. module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime())
  786. module.debug("Waiting complete. ELB thinks %s instances are healthy." % healthy_instances)
  787. def wait_for_target_group(asg_connection, group_name):
  788. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  789. wait_timeout = module.params.get('wait_timeout')
  790. # if the health_check_type is ELB, we want to query the ELBs directly for instance
  791. # status as to avoid health_check_grace period that is awarded to ASG instances
  792. as_group = describe_autoscaling_groups(asg_connection, group_name)[0]
  793. if as_group.get('TargetGroupARNs') and as_group.get('HealthCheckType') == 'ELB':
  794. module.debug("Waiting for Target Group to consider instances healthy.")
  795. elbv2_connection = boto3_conn(module,
  796. conn_type='client',
  797. resource='elbv2',
  798. region=region,
  799. endpoint=ec2_url,
  800. **aws_connect_params)
  801. wait_timeout = time.time() + wait_timeout
  802. healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name)
  803. while healthy_instances < as_group.get('MinSize') and wait_timeout > time.time():
  804. healthy_instances = tg_healthy(asg_connection, elbv2_connection, group_name)
  805. module.debug("Target Group thinks %s instances are healthy." % healthy_instances)
  806. time.sleep(10)
  807. if wait_timeout <= time.time():
  808. # waiting took too long
  809. module.fail_json(msg="Waited too long for ELB instances to be healthy. %s" % time.asctime())
  810. module.debug("Waiting complete. Target Group thinks %s instances are healthy." % healthy_instances)
  811. def suspend_processes(ec2_connection, as_group):
  812. suspend_processes = set(module.params.get('suspend_processes'))
  813. try:
  814. suspended_processes = set([p['ProcessName'] for p in as_group['SuspendedProcesses']])
  815. except AttributeError:
  816. # New ASG being created, no suspended_processes defined yet
  817. suspended_processes = set()
  818. if suspend_processes == suspended_processes:
  819. return False
  820. resume_processes = list(suspended_processes - suspend_processes)
  821. if resume_processes:
  822. resume_asg_processes(ec2_connection, module.params.get('name'), resume_processes)
  823. if suspend_processes:
  824. suspend_asg_processes(ec2_connection, module.params.get('name'), list(suspend_processes))
  825. return True
  826. def create_autoscaling_group(connection):
  827. group_name = module.params.get('name')
  828. load_balancers = module.params['load_balancers']
  829. target_group_arns = module.params['target_group_arns']
  830. availability_zones = module.params['availability_zones']
  831. launch_config_name = module.params.get('launch_config_name')
  832. launch_template = module.params.get('launch_template')
  833. mixed_instances_policy = module.params.get('mixed_instances_policy')
  834. min_size = module.params['min_size']
  835. max_size = module.params['max_size']
  836. placement_group = module.params.get('placement_group')
  837. desired_capacity = module.params.get('desired_capacity')
  838. vpc_zone_identifier = module.params.get('vpc_zone_identifier')
  839. set_tags = module.params.get('tags')
  840. health_check_period = module.params.get('health_check_period')
  841. health_check_type = module.params.get('health_check_type')
  842. default_cooldown = module.params.get('default_cooldown')
  843. wait_for_instances = module.params.get('wait_for_instances')
  844. wait_timeout = module.params.get('wait_timeout')
  845. termination_policies = module.params.get('termination_policies')
  846. notification_topic = module.params.get('notification_topic')
  847. notification_types = module.params.get('notification_types')
  848. metrics_collection = module.params.get('metrics_collection')
  849. metrics_granularity = module.params.get('metrics_granularity')
  850. metrics_list = module.params.get('metrics_list')
  851. try:
  852. as_groups = describe_autoscaling_groups(connection, group_name)
  853. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  854. module.fail_json(msg="Failed to describe auto scaling groups.",
  855. exception=traceback.format_exc())
  856. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  857. ec2_connection = boto3_conn(module,
  858. conn_type='client',
  859. resource='ec2',
  860. region=region,
  861. endpoint=ec2_url,
  862. **aws_connect_params)
  863. if vpc_zone_identifier:
  864. vpc_zone_identifier = ','.join(vpc_zone_identifier)
  865. asg_tags = []
  866. for tag in set_tags:
  867. for k, v in tag.items():
  868. if k != 'propagate_at_launch':
  869. asg_tags.append(dict(Key=k,
  870. Value=to_native(v),
  871. PropagateAtLaunch=bool(tag.get('propagate_at_launch', True)),
  872. ResourceType='auto-scaling-group',
  873. ResourceId=group_name))
  874. if not as_groups:
  875. if not vpc_zone_identifier and not availability_zones:
  876. availability_zones = module.params['availability_zones'] = [zone['ZoneName'] for
  877. zone in ec2_connection.describe_availability_zones()['AvailabilityZones']]
  878. enforce_required_arguments_for_create()
  879. if desired_capacity is None:
  880. desired_capacity = min_size
  881. ag = dict(
  882. AutoScalingGroupName=group_name,
  883. MinSize=min_size,
  884. MaxSize=max_size,
  885. DesiredCapacity=desired_capacity,
  886. Tags=asg_tags,
  887. HealthCheckGracePeriod=health_check_period,
  888. HealthCheckType=health_check_type,
  889. DefaultCooldown=default_cooldown,
  890. TerminationPolicies=termination_policies)
  891. if vpc_zone_identifier:
  892. ag['VPCZoneIdentifier'] = vpc_zone_identifier
  893. if availability_zones:
  894. ag['AvailabilityZones'] = availability_zones
  895. if placement_group:
  896. ag['PlacementGroup'] = placement_group
  897. if load_balancers:
  898. ag['LoadBalancerNames'] = load_balancers
  899. if target_group_arns:
  900. ag['TargetGroupARNs'] = target_group_arns
  901. launch_object = get_launch_object(connection, ec2_connection)
  902. if 'LaunchConfigurationName' in launch_object:
  903. ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName']
  904. elif 'LaunchTemplate' in launch_object:
  905. if 'MixedInstancesPolicy' in launch_object:
  906. ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy']
  907. else:
  908. ag['LaunchTemplate'] = launch_object['LaunchTemplate']
  909. else:
  910. module.fail_json(msg="Missing LaunchConfigurationName or LaunchTemplate",
  911. exception=traceback.format_exc())
  912. try:
  913. create_asg(connection, **ag)
  914. if metrics_collection:
  915. connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list)
  916. all_ag = describe_autoscaling_groups(connection, group_name)
  917. if len(all_ag) == 0:
  918. module.fail_json(msg="No auto scaling group found with the name %s" % group_name)
  919. as_group = all_ag[0]
  920. suspend_processes(connection, as_group)
  921. if wait_for_instances:
  922. wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances')
  923. if load_balancers:
  924. wait_for_elb(connection, group_name)
  925. # Wait for target group health if target group(s)defined
  926. if target_group_arns:
  927. wait_for_target_group(connection, group_name)
  928. if notification_topic:
  929. put_notification_config(connection, group_name, notification_topic, notification_types)
  930. as_group = describe_autoscaling_groups(connection, group_name)[0]
  931. asg_properties = get_properties(as_group)
  932. changed = True
  933. return changed, asg_properties
  934. except botocore.exceptions.ClientError as e:
  935. module.fail_json(msg="Failed to create Autoscaling Group.",
  936. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  937. except botocore.exceptions.BotoCoreError as e:
  938. module.fail_json(msg="Failed to create Autoscaling Group.",
  939. exception=traceback.format_exc())
  940. else:
  941. as_group = as_groups[0]
  942. initial_asg_properties = get_properties(as_group)
  943. changed = False
  944. if suspend_processes(connection, as_group):
  945. changed = True
  946. # process tag changes
  947. if len(set_tags) > 0:
  948. have_tags = as_group.get('Tags')
  949. want_tags = asg_tags
  950. dead_tags = []
  951. have_tag_keyvals = [x['Key'] for x in have_tags]
  952. want_tag_keyvals = [x['Key'] for x in want_tags]
  953. for dead_tag in set(have_tag_keyvals).difference(want_tag_keyvals):
  954. changed = True
  955. dead_tags.append(dict(ResourceId=as_group['AutoScalingGroupName'],
  956. ResourceType='auto-scaling-group', Key=dead_tag))
  957. have_tags = [have_tag for have_tag in have_tags if have_tag['Key'] != dead_tag]
  958. if dead_tags:
  959. connection.delete_tags(Tags=dead_tags)
  960. zipped = zip(have_tags, want_tags)
  961. if len(have_tags) != len(want_tags) or not all(x == y for x, y in zipped):
  962. changed = True
  963. connection.create_or_update_tags(Tags=asg_tags)
  964. # Handle load balancer attachments/detachments
  965. # Attach load balancers if they are specified but none currently exist
  966. if load_balancers and not as_group['LoadBalancerNames']:
  967. changed = True
  968. try:
  969. attach_load_balancers(connection, group_name, load_balancers)
  970. except botocore.exceptions.ClientError as e:
  971. module.fail_json(msg="Failed to update Autoscaling Group.",
  972. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  973. except botocore.exceptions.BotoCoreError as e:
  974. module.fail_json(msg="Failed to update Autoscaling Group.",
  975. exception=traceback.format_exc())
  976. # Update load balancers if they are specified and one or more already exists
  977. elif as_group['LoadBalancerNames']:
  978. change_load_balancers = load_balancers is not None
  979. # Get differences
  980. if not load_balancers:
  981. load_balancers = list()
  982. wanted_elbs = set(load_balancers)
  983. has_elbs = set(as_group['LoadBalancerNames'])
  984. # check if all requested are already existing
  985. if has_elbs - wanted_elbs and change_load_balancers:
  986. # if wanted contains less than existing, then we need to delete some
  987. elbs_to_detach = has_elbs.difference(wanted_elbs)
  988. if elbs_to_detach:
  989. changed = True
  990. try:
  991. detach_load_balancers(connection, group_name, list(elbs_to_detach))
  992. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  993. module.fail_json(msg="Failed to detach load balancers %s: %s." % (elbs_to_detach, to_native(e)),
  994. exception=traceback.format_exc())
  995. if wanted_elbs - has_elbs:
  996. # if has contains less than wanted, then we need to add some
  997. elbs_to_attach = wanted_elbs.difference(has_elbs)
  998. if elbs_to_attach:
  999. changed = True
  1000. try:
  1001. attach_load_balancers(connection, group_name, list(elbs_to_attach))
  1002. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  1003. module.fail_json(msg="Failed to attach load balancers %s: %s." % (elbs_to_attach, to_native(e)),
  1004. exception=traceback.format_exc())
  1005. # Handle target group attachments/detachments
  1006. # Attach target groups if they are specified but none currently exist
  1007. if target_group_arns and not as_group['TargetGroupARNs']:
  1008. changed = True
  1009. try:
  1010. attach_lb_target_groups(connection, group_name, target_group_arns)
  1011. except botocore.exceptions.ClientError as e:
  1012. module.fail_json(msg="Failed to update Autoscaling Group.",
  1013. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  1014. except botocore.exceptions.BotoCoreError as e:
  1015. module.fail_json(msg="Failed to update Autoscaling Group.",
  1016. exception=traceback.format_exc())
  1017. # Update target groups if they are specified and one or more already exists
  1018. elif target_group_arns is not None and as_group['TargetGroupARNs']:
  1019. # Get differences
  1020. wanted_tgs = set(target_group_arns)
  1021. has_tgs = set(as_group['TargetGroupARNs'])
  1022. # check if all requested are already existing
  1023. if has_tgs.issuperset(wanted_tgs):
  1024. # if wanted contains less than existing, then we need to delete some
  1025. tgs_to_detach = has_tgs.difference(wanted_tgs)
  1026. if tgs_to_detach:
  1027. changed = True
  1028. try:
  1029. detach_lb_target_groups(connection, group_name, list(tgs_to_detach))
  1030. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  1031. module.fail_json(msg="Failed to detach load balancer target groups %s: %s" % (tgs_to_detach, to_native(e)),
  1032. exception=traceback.format_exc())
  1033. if wanted_tgs.issuperset(has_tgs):
  1034. # if has contains less than wanted, then we need to add some
  1035. tgs_to_attach = wanted_tgs.difference(has_tgs)
  1036. if tgs_to_attach:
  1037. changed = True
  1038. try:
  1039. attach_lb_target_groups(connection, group_name, list(tgs_to_attach))
  1040. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  1041. module.fail_json(msg="Failed to attach load balancer target groups %s: %s" % (tgs_to_attach, to_native(e)),
  1042. exception=traceback.format_exc())
  1043. # check for attributes that aren't required for updating an existing ASG
  1044. # check if min_size/max_size/desired capacity have been specified and if not use ASG values
  1045. if min_size is None:
  1046. min_size = as_group['MinSize']
  1047. if max_size is None:
  1048. max_size = as_group['MaxSize']
  1049. if desired_capacity is None:
  1050. desired_capacity = as_group['DesiredCapacity']
  1051. ag = dict(
  1052. AutoScalingGroupName=group_name,
  1053. MinSize=min_size,
  1054. MaxSize=max_size,
  1055. DesiredCapacity=desired_capacity,
  1056. HealthCheckGracePeriod=health_check_period,
  1057. HealthCheckType=health_check_type,
  1058. DefaultCooldown=default_cooldown,
  1059. TerminationPolicies=termination_policies)
  1060. # Get the launch object (config or template) if one is provided in args or use the existing one attached to ASG if not.
  1061. launch_object = get_launch_object(connection, ec2_connection)
  1062. if 'LaunchConfigurationName' in launch_object:
  1063. ag['LaunchConfigurationName'] = launch_object['LaunchConfigurationName']
  1064. elif 'LaunchTemplate' in launch_object:
  1065. import q
  1066. import json
  1067. q(launch_object)
  1068. if 'MixedInstancesPolicy' in launch_object:
  1069. ag['MixedInstancesPolicy'] = launch_object['MixedInstancesPolicy']
  1070. else:
  1071. ag['LaunchTemplate'] = launch_object['LaunchTemplate']
  1072. else:
  1073. try:
  1074. ag['LaunchConfigurationName'] = as_group['LaunchConfigurationName']
  1075. except Exception:
  1076. launch_template = as_group['LaunchTemplate']
  1077. # Prefer LaunchTemplateId over Name as it's more specific. Only one can be used for update_asg.
  1078. ag['LaunchTemplate'] = {"LaunchTemplateId": launch_template['LaunchTemplateId'], "Version": launch_template['Version']}
  1079. if availability_zones:
  1080. ag['AvailabilityZones'] = availability_zones
  1081. if vpc_zone_identifier:
  1082. ag['VPCZoneIdentifier'] = vpc_zone_identifier
  1083. try:
  1084. update_asg(connection, **ag)
  1085. if metrics_collection:
  1086. connection.enable_metrics_collection(AutoScalingGroupName=group_name, Granularity=metrics_granularity, Metrics=metrics_list)
  1087. else:
  1088. connection.disable_metrics_collection(AutoScalingGroupName=group_name, Metrics=metrics_list)
  1089. except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
  1090. module.fail_json(msg="Failed to update autoscaling group: %s" % to_native(e),
  1091. exception=traceback.format_exc())
  1092. if notification_topic:
  1093. try:
  1094. put_notification_config(connection, group_name, notification_topic, notification_types)
  1095. except botocore.exceptions.ClientError as e:
  1096. module.fail_json(msg="Failed to update Autoscaling Group notifications.",
  1097. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  1098. except botocore.exceptions.BotoCoreError as e:
  1099. module.fail_json(msg="Failed to update Autoscaling Group notifications.",
  1100. exception=traceback.format_exc())
  1101. if wait_for_instances:
  1102. wait_for_new_inst(connection, group_name, wait_timeout, desired_capacity, 'viable_instances')
  1103. # Wait for ELB health if ELB(s)defined
  1104. if load_balancers:
  1105. module.debug('\tWAITING FOR ELB HEALTH')
  1106. wait_for_elb(connection, group_name)
  1107. # Wait for target group health if target group(s)defined
  1108. if target_group_arns:
  1109. module.debug('\tWAITING FOR TG HEALTH')
  1110. wait_for_target_group(connection, group_name)
  1111. try:
  1112. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1113. asg_properties = get_properties(as_group)
  1114. if asg_properties != initial_asg_properties:
  1115. changed = True
  1116. except botocore.exceptions.ClientError as e:
  1117. module.fail_json(msg="Failed to read existing Autoscaling Groups.",
  1118. exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
  1119. except botocore.exceptions.BotoCoreError as e:
  1120. module.fail_json(msg="Failed to read existing Autoscaling Groups.",
  1121. exception=traceback.format_exc())
  1122. return changed, asg_properties
  1123. def delete_autoscaling_group(connection):
  1124. group_name = module.params.get('name')
  1125. notification_topic = module.params.get('notification_topic')
  1126. wait_for_instances = module.params.get('wait_for_instances')
  1127. wait_timeout = module.params.get('wait_timeout')
  1128. if notification_topic:
  1129. del_notification_config(connection, group_name, notification_topic)
  1130. groups = describe_autoscaling_groups(connection, group_name)
  1131. if groups:
  1132. wait_timeout = time.time() + wait_timeout
  1133. if not wait_for_instances:
  1134. delete_asg(connection, group_name, force_delete=True)
  1135. else:
  1136. updated_params = dict(AutoScalingGroupName=group_name, MinSize=0, MaxSize=0, DesiredCapacity=0)
  1137. update_asg(connection, **updated_params)
  1138. instances = True
  1139. while instances and wait_for_instances and wait_timeout >= time.time():
  1140. tmp_groups = describe_autoscaling_groups(connection, group_name)
  1141. if tmp_groups:
  1142. tmp_group = tmp_groups[0]
  1143. if not tmp_group.get('Instances'):
  1144. instances = False
  1145. time.sleep(10)
  1146. if wait_timeout <= time.time():
  1147. # waiting took too long
  1148. module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime())
  1149. delete_asg(connection, group_name, force_delete=False)
  1150. while describe_autoscaling_groups(connection, group_name) and wait_timeout >= time.time():
  1151. time.sleep(5)
  1152. if wait_timeout <= time.time():
  1153. # waiting took too long
  1154. module.fail_json(msg="Waited too long for ASG to delete. %s" % time.asctime())
  1155. return True
  1156. return False
  1157. def get_chunks(l, n):
  1158. for i in range(0, len(l), n):
  1159. yield l[i:i + n]
  1160. def update_size(connection, group, max_size, min_size, dc):
  1161. module.debug("setting ASG sizes")
  1162. module.debug("minimum size: %s, desired_capacity: %s, max size: %s" % (min_size, dc, max_size))
  1163. updated_group = dict()
  1164. updated_group['AutoScalingGroupName'] = group['AutoScalingGroupName']
  1165. updated_group['MinSize'] = min_size
  1166. updated_group['MaxSize'] = max_size
  1167. updated_group['DesiredCapacity'] = dc
  1168. update_asg(connection, **updated_group)
  1169. def replace(connection):
  1170. batch_size = module.params.get('replace_batch_size')
  1171. wait_timeout = module.params.get('wait_timeout')
  1172. group_name = module.params.get('name')
  1173. max_size = module.params.get('max_size')
  1174. min_size = module.params.get('min_size')
  1175. desired_capacity = module.params.get('desired_capacity')
  1176. launch_config_name = module.params.get('launch_config_name')
  1177. # Required to maintain the default value being set to 'true'
  1178. if launch_config_name:
  1179. lc_check = module.params.get('lc_check')
  1180. else:
  1181. lc_check = False
  1182. # Mirror above behaviour for Launch Templates
  1183. launch_template = module.params.get('launch_template')
  1184. if launch_template:
  1185. lt_check = module.params.get('lt_check')
  1186. else:
  1187. lt_check = False
  1188. replace_instances = module.params.get('replace_instances')
  1189. replace_all_instances = module.params.get('replace_all_instances')
  1190. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1191. if desired_capacity is None:
  1192. desired_capacity = as_group['DesiredCapacity']
  1193. wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'], 'viable_instances')
  1194. props = get_properties(as_group)
  1195. instances = props['instances']
  1196. if replace_all_instances:
  1197. # If replacing all instances, then set replace_instances to current set
  1198. # This allows replace_instances and replace_all_instances to behave same
  1199. replace_instances = instances
  1200. if replace_instances:
  1201. instances = replace_instances
  1202. # check to see if instances are replaceable if checking launch configs
  1203. if launch_config_name:
  1204. new_instances, old_instances = get_instances_by_launch_config(props, lc_check, instances)
  1205. elif launch_template:
  1206. new_instances, old_instances = get_instances_by_launch_template(props, lt_check, instances)
  1207. num_new_inst_needed = desired_capacity - len(new_instances)
  1208. if lc_check or lt_check:
  1209. if num_new_inst_needed == 0 and old_instances:
  1210. module.debug("No new instances needed, but old instances are present. Removing old instances")
  1211. terminate_batch(connection, old_instances, instances, True)
  1212. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1213. props = get_properties(as_group)
  1214. changed = True
  1215. return(changed, props)
  1216. # we don't want to spin up extra instances if not necessary
  1217. if num_new_inst_needed < batch_size:
  1218. module.debug("Overriding batch size to %s" % num_new_inst_needed)
  1219. batch_size = num_new_inst_needed
  1220. if not old_instances:
  1221. changed = False
  1222. return(changed, props)
  1223. # check if min_size/max_size/desired capacity have been specified and if not use ASG values
  1224. if min_size is None:
  1225. min_size = as_group['MinSize']
  1226. if max_size is None:
  1227. max_size = as_group['MaxSize']
  1228. # set temporary settings and wait for them to be reached
  1229. # This should get overwritten if the number of instances left is less than the batch size.
  1230. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1231. update_size(connection, as_group, max_size + batch_size, min_size + batch_size, desired_capacity + batch_size)
  1232. wait_for_new_inst(connection, group_name, wait_timeout, as_group['MinSize'] + batch_size, 'viable_instances')
  1233. wait_for_elb(connection, group_name)
  1234. wait_for_target_group(connection, group_name)
  1235. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1236. props = get_properties(as_group)
  1237. instances = props['instances']
  1238. if replace_instances:
  1239. instances = replace_instances
  1240. module.debug("beginning main loop")
  1241. for i in get_chunks(instances, batch_size):
  1242. # break out of this loop if we have enough new instances
  1243. break_early, desired_size, term_instances = terminate_batch(connection, i, instances, False)
  1244. wait_for_term_inst(connection, term_instances)
  1245. wait_for_new_inst(connection, group_name, wait_timeout, desired_size, 'viable_instances')
  1246. wait_for_elb(connection, group_name)
  1247. wait_for_target_group(connection, group_name)
  1248. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1249. if break_early:
  1250. module.debug("breaking loop")
  1251. break
  1252. update_size(connection, as_group, max_size, min_size, desired_capacity)
  1253. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1254. asg_properties = get_properties(as_group)
  1255. module.debug("Rolling update complete.")
  1256. changed = True
  1257. return(changed, asg_properties)
  1258. def get_instances_by_launch_config(props, lc_check, initial_instances):
  1259. new_instances = []
  1260. old_instances = []
  1261. # old instances are those that have the old launch config
  1262. if lc_check:
  1263. for i in props['instances']:
  1264. # Check if migrating from launch_template to launch_config first
  1265. if 'launch_template' in props['instance_facts'][i]:
  1266. old_instances.append(i)
  1267. elif props['instance_facts'][i]['launch_config_name'] == props['launch_config_name']:
  1268. new_instances.append(i)
  1269. else:
  1270. old_instances.append(i)
  1271. else:
  1272. module.debug("Comparing initial instances with current: %s" % initial_instances)
  1273. for i in props['instances']:
  1274. if i not in initial_instances:
  1275. new_instances.append(i)
  1276. else:
  1277. old_instances.append(i)
  1278. module.debug("New instances: %s, %s" % (len(new_instances), new_instances))
  1279. module.debug("Old instances: %s, %s" % (len(old_instances), old_instances))
  1280. return new_instances, old_instances
  1281. def get_instances_by_launch_template(props, lt_check, initial_instances):
  1282. new_instances = []
  1283. old_instances = []
  1284. # old instances are those that have the old launch template or version of the same launch templatec
  1285. if lt_check:
  1286. for i in props['instances']:
  1287. # Check if migrating from launch_config_name to launch_template_name first
  1288. if 'launch_config_name' in props['instance_facts'][i]:
  1289. old_instances.append(i)
  1290. elif props['instance_facts'][i]['launch_template'] == props['launch_template']:
  1291. new_instances.append(i)
  1292. else:
  1293. old_instances.append(i)
  1294. else:
  1295. module.debug("Comparing initial instances with current: %s" % initial_instances)
  1296. for i in props['instances']:
  1297. if i not in initial_instances:
  1298. new_instances.append(i)
  1299. else:
  1300. old_instances.append(i)
  1301. module.debug("New instances: %s, %s" % (len(new_instances), new_instances))
  1302. module.debug("Old instances: %s, %s" % (len(old_instances), old_instances))
  1303. return new_instances, old_instances
  1304. def list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances):
  1305. instances_to_terminate = []
  1306. instances = (inst_id for inst_id in replace_instances if inst_id in props['instances'])
  1307. # check to make sure instances given are actually in the given ASG
  1308. # and they have a non-current launch config
  1309. if module.params.get('launch_config_name'):
  1310. if lc_check:
  1311. for i in instances:
  1312. if 'launch_template' in props['instance_facts'][i]:
  1313. instances_to_terminate.append(i)
  1314. elif props['instance_facts'][i]['launch_config_name'] != props['launch_config_name']:
  1315. instances_to_terminate.append(i)
  1316. else:
  1317. for i in instances:
  1318. if i in initial_instances:
  1319. instances_to_terminate.append(i)
  1320. elif module.params.get('launch_template'):
  1321. if lt_check:
  1322. for i in instances:
  1323. if 'launch_config_name' in props['instance_facts'][i]:
  1324. instances_to_terminate.append(i)
  1325. elif props['instance_facts'][i]['launch_template'] != props['launch_template']:
  1326. instances_to_terminate.append(i)
  1327. else:
  1328. for i in instances:
  1329. if i in initial_instances:
  1330. instances_to_terminate.append(i)
  1331. return instances_to_terminate
  1332. def terminate_batch(connection, replace_instances, initial_instances, leftovers=False):
  1333. batch_size = module.params.get('replace_batch_size')
  1334. min_size = module.params.get('min_size')
  1335. desired_capacity = module.params.get('desired_capacity')
  1336. group_name = module.params.get('name')
  1337. lc_check = module.params.get('lc_check')
  1338. lt_check = module.params.get('lt_check')
  1339. decrement_capacity = False
  1340. break_loop = False
  1341. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1342. if desired_capacity is None:
  1343. desired_capacity = as_group['DesiredCapacity']
  1344. props = get_properties(as_group)
  1345. desired_size = as_group['MinSize']
  1346. if module.params.get('launch_config_name'):
  1347. new_instances, old_instances = get_instances_by_launch_config(props, lc_check, initial_instances)
  1348. else:
  1349. new_instances, old_instances = get_instances_by_launch_template(props, lt_check, initial_instances)
  1350. num_new_inst_needed = desired_capacity - len(new_instances)
  1351. # check to make sure instances given are actually in the given ASG
  1352. # and they have a non-current launch config
  1353. instances_to_terminate = list_purgeable_instances(props, lc_check, lt_check, replace_instances, initial_instances)
  1354. module.debug("new instances needed: %s" % num_new_inst_needed)
  1355. module.debug("new instances: %s" % new_instances)
  1356. module.debug("old instances: %s" % old_instances)
  1357. module.debug("batch instances: %s" % ",".join(instances_to_terminate))
  1358. if num_new_inst_needed == 0:
  1359. decrement_capacity = True
  1360. if as_group['MinSize'] != min_size:
  1361. if min_size is None:
  1362. min_size = as_group['MinSize']
  1363. updated_params = dict(AutoScalingGroupName=as_group['AutoScalingGroupName'], MinSize=min_size)
  1364. update_asg(connection, **updated_params)
  1365. module.debug("Updating minimum size back to original of %s" % min_size)
  1366. # if are some leftover old instances, but we are already at capacity with new ones
  1367. # we don't want to decrement capacity
  1368. if leftovers:
  1369. decrement_capacity = False
  1370. break_loop = True
  1371. instances_to_terminate = old_instances
  1372. desired_size = min_size
  1373. module.debug("No new instances needed")
  1374. if num_new_inst_needed < batch_size and num_new_inst_needed != 0:
  1375. instances_to_terminate = instances_to_terminate[:num_new_inst_needed]
  1376. decrement_capacity = False
  1377. break_loop = False
  1378. module.debug("%s new instances needed" % num_new_inst_needed)
  1379. module.debug("decrementing capacity: %s" % decrement_capacity)
  1380. for instance_id in instances_to_terminate:
  1381. elb_dreg(connection, group_name, instance_id)
  1382. module.debug("terminating instance: %s" % instance_id)
  1383. terminate_asg_instance(connection, instance_id, decrement_capacity)
  1384. # we wait to make sure the machines we marked as Unhealthy are
  1385. # no longer in the list
  1386. return break_loop, desired_size, instances_to_terminate
  1387. def wait_for_term_inst(connection, term_instances):
  1388. wait_timeout = module.params.get('wait_timeout')
  1389. group_name = module.params.get('name')
  1390. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1391. count = 1
  1392. wait_timeout = time.time() + wait_timeout
  1393. while wait_timeout > time.time() and count > 0:
  1394. module.debug("waiting for instances to terminate")
  1395. count = 0
  1396. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1397. props = get_properties(as_group)
  1398. instance_facts = props['instance_facts']
  1399. instances = (i for i in instance_facts if i in term_instances)
  1400. for i in instances:
  1401. lifecycle = instance_facts[i]['lifecycle_state']
  1402. health = instance_facts[i]['health_status']
  1403. module.debug("Instance %s has state of %s,%s" % (i, lifecycle, health))
  1404. if lifecycle.startswith('Terminating') or health == 'Unhealthy':
  1405. count += 1
  1406. time.sleep(10)
  1407. if wait_timeout <= time.time():
  1408. # waiting took too long
  1409. module.fail_json(msg="Waited too long for old instances to terminate. %s" % time.asctime())
  1410. def wait_for_new_inst(connection, group_name, wait_timeout, desired_size, prop):
  1411. # make sure we have the latest stats after that last loop.
  1412. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1413. props = get_properties(as_group)
  1414. module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop]))
  1415. # now we make sure that we have enough instances in a viable state
  1416. wait_timeout = time.time() + wait_timeout
  1417. while wait_timeout > time.time() and desired_size > props[prop]:
  1418. module.debug("Waiting for %s = %s, currently %s" % (prop, desired_size, props[prop]))
  1419. time.sleep(10)
  1420. as_group = describe_autoscaling_groups(connection, group_name)[0]
  1421. props = get_properties(as_group)
  1422. if wait_timeout <= time.time():
  1423. # waiting took too long
  1424. module.fail_json(msg="Waited too long for new instances to become viable. %s" % time.asctime())
  1425. module.debug("Reached %s: %s" % (prop, desired_size))
  1426. return props
  1427. def asg_exists(connection):
  1428. group_name = module.params.get('name')
  1429. as_group = describe_autoscaling_groups(connection, group_name)
  1430. return bool(len(as_group))
  1431. def main():
  1432. argument_spec = ec2_argument_spec()
  1433. argument_spec.update(
  1434. dict(
  1435. name=dict(required=True, type='str'),
  1436. load_balancers=dict(type='list'),
  1437. target_group_arns=dict(type='list'),
  1438. availability_zones=dict(type='list'),
  1439. launch_config_name=dict(type='str'),
  1440. launch_template=dict(type='dict',
  1441. default=None,
  1442. options=dict(
  1443. version=dict(type='str'),
  1444. launch_template_name=dict(type='str'),
  1445. launch_template_id=dict(type='str'),
  1446. ),
  1447. ),
  1448. mixed_instances_policy=dict(type='dict',
  1449. default=None,
  1450. options=dict(
  1451. instance_types=dict(type='list'),
  1452. )),
  1453. min_size=dict(type='int'),
  1454. max_size=dict(type='int'),
  1455. placement_group=dict(type='str'),
  1456. desired_capacity=dict(type='int'),
  1457. vpc_zone_identifier=dict(type='list'),
  1458. replace_batch_size=dict(type='int', default=1),
  1459. replace_all_instances=dict(type='bool', default=False),
  1460. replace_instances=dict(type='list', default=[]),
  1461. lc_check=dict(type='bool', default=True),
  1462. lt_check=dict(type='bool', default=True),
  1463. wait_timeout=dict(type='int', default=300),
  1464. state=dict(default='present', choices=['present', 'absent']),
  1465. tags=dict(type='list', default=[]),
  1466. health_check_period=dict(type='int', default=300),
  1467. health_check_type=dict(default='EC2', choices=['EC2', 'ELB']),
  1468. default_cooldown=dict(type='int', default=300),
  1469. wait_for_instances=dict(type='bool', default=True),
  1470. termination_policies=dict(type='list', default='Default'),
  1471. notification_topic=dict(type='str', default=None),
  1472. notification_types=dict(type='list', default=[
  1473. 'autoscaling:EC2_INSTANCE_LAUNCH',
  1474. 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR',
  1475. 'autoscaling:EC2_INSTANCE_TERMINATE',
  1476. 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'
  1477. ]),
  1478. suspend_processes=dict(type='list', default=[]),
  1479. metrics_collection=dict(type='bool', default=False),
  1480. metrics_granularity=dict(type='str', default='1Minute'),
  1481. metrics_list=dict(type='list', default=[
  1482. 'GroupMinSize',
  1483. 'GroupMaxSize',
  1484. 'GroupDesiredCapacity',
  1485. 'GroupInServiceInstances',
  1486. 'GroupPendingInstances',
  1487. 'GroupStandbyInstances',
  1488. 'GroupTerminatingInstances',
  1489. 'GroupTotalInstances'
  1490. ])
  1491. ),
  1492. )
  1493. global module
  1494. module = AnsibleModule(
  1495. argument_spec=argument_spec,
  1496. mutually_exclusive=[
  1497. ['replace_all_instances', 'replace_instances'],
  1498. ['launch_config_name', 'launch_template']]
  1499. )
  1500. if not HAS_BOTO3:
  1501. module.fail_json(msg='boto3 required for this module')
  1502. state = module.params.get('state')
  1503. replace_instances = module.params.get('replace_instances')
  1504. replace_all_instances = module.params.get('replace_all_instances')
  1505. region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
  1506. connection = boto3_conn(module,
  1507. conn_type='client',
  1508. resource='autoscaling',
  1509. region=region,
  1510. endpoint=ec2_url,
  1511. **aws_connect_params)
  1512. changed = create_changed = replace_changed = False
  1513. exists = asg_exists(connection)
  1514. if state == 'present':
  1515. create_changed, asg_properties = create_autoscaling_group(connection)
  1516. elif state == 'absent':
  1517. changed = delete_autoscaling_group(connection)
  1518. module.exit_json(changed=changed)
  1519. # Only replace instances if asg existed at start of call
  1520. if exists and (replace_all_instances or replace_instances) and (module.params.get('launch_config_name') or module.params.get('launch_template')):
  1521. replace_changed, asg_properties = replace(connection)
  1522. if create_changed or replace_changed:
  1523. changed = True
  1524. module.exit_json(changed=changed, **asg_properties)
  1525. if __name__ == '__main__':
  1526. main()