|
1 | 1 | """ |
2 | 2 | Functions to scale and respawn Auto Scale Groups |
3 | 3 | """ |
| 4 | +import collections |
4 | 5 | import time |
5 | 6 |
|
6 | 7 | from clickclick import Action, info |
|
14 | 15 | ELASTIGROUP_TERMINATED_DEPLOY_STATUS = ["stopped", "failed"] |
15 | 16 |
|
16 | 17 | DEFAULT_BATCH_SIZE = 20 |
| 18 | +WAIT_FOR_ELASTIGROUP_SEC = 10 |
17 | 19 |
|
18 | 20 |
|
19 | 21 | def get_auto_scaling_group(asg, asg_name: str): |
@@ -177,19 +179,117 @@ def respawn_auto_scaling_group( |
177 | 179 |
|
178 | 180 |
|
179 | 181 | def respawn_elastigroup( |
180 | | - elastigroup_id: str, stack_name: str, region: str, batch_size: int |
| 182 | + elastigroup_id: str, stack_name: str, region: str, batch_size: int, batch_per_subnet: bool |
181 | 183 | ): |
182 | 184 | """ |
183 | 185 | Respawn all instances in the ElastiGroup. |
184 | 186 | """ |
185 | 187 |
|
| 188 | + spotinst_account = elastigroup_api.get_spotinst_account_data(region, stack_name) |
| 189 | + |
| 190 | + stateful_instances = elastigroup_api.get_stateful_instances(elastigroup_id, spotinst_account) |
| 191 | + if stateful_instances: |
| 192 | + if batch_size is not None: |
| 193 | + raise Exception("Batch size is not supported when respawning stateful ElastiGroups") |
| 194 | + |
| 195 | + respawn_stateful_elastigroup(elastigroup_id, stack_name, region, batch_per_subnet, stateful_instances, spotinst_account) |
| 196 | + else: |
| 197 | + if batch_per_subnet: |
| 198 | + raise Exception("Batch per subnet is not supported when respawning stateless ElastiGroups") |
| 199 | + |
| 200 | + respawn_stateless_elastigroup(elastigroup_id, stack_name, batch_size, spotinst_account) |
| 201 | + |
| 202 | + |
| 203 | +def respawn_stateful_elastigroup( |
| 204 | + elastigroup_id: str, stack_name: str, region: str, batch_per_subnet: bool, stateful_instances: list, spotinst_account |
| 205 | +): |
| 206 | + """ |
| 207 | + Recycles stateful instances of the ElastiGroup. |
| 208 | + """ |
| 209 | + |
| 210 | + if not stateful_elastigroup_ready(stateful_instances): |
| 211 | + raise Exception( |
| 212 | + "Stateful ElastiGroup {} is not ready: some instances are not in the ACTIVE state".format(elastigroup_id) |
| 213 | + ) |
| 214 | + |
| 215 | + info( |
| 216 | + "Recycling {} stateful instances for ElastiGroup {} (ID {})".format( |
| 217 | + len(stateful_instances), stack_name, elastigroup_id |
| 218 | + ) |
| 219 | + ) |
| 220 | + |
| 221 | + if batch_per_subnet: |
| 222 | + instances_by_subnet = stateful_elastigroup_instances_by_subnet(region, stateful_instances) |
| 223 | + for subnet, subnet_instances in sorted(instances_by_subnet.items(), key=lambda item: item[0]): |
| 224 | + info("Recycling ALL stateful instances in subnet: {}".format(subnet)) |
| 225 | + |
| 226 | + for instance in sorted(subnet_instances, key=lambda i: i['privateIp']): |
| 227 | + time.sleep(WAIT_FOR_ELASTIGROUP_SEC) |
| 228 | + recycle_stateful_elastigroup_instance(elastigroup_id, instance, spotinst_account) |
| 229 | + |
| 230 | + wait_for_stateful_elastigroup(elastigroup_id, spotinst_account) |
| 231 | + |
| 232 | + else: |
| 233 | + for instance in sorted(stateful_instances, key=lambda i: i['privateIp']): |
| 234 | + recycle_stateful_elastigroup_instance(elastigroup_id, instance, spotinst_account) |
| 235 | + wait_for_stateful_elastigroup(elastigroup_id, spotinst_account) |
| 236 | + |
| 237 | + |
| 238 | +def stateful_elastigroup_instances_by_subnet(region: str, stateful_instances: list): |
| 239 | + instances_by_subnet = collections.defaultdict(list) |
| 240 | + instances_by_ec2_id = {i['instanceId']: i for i in stateful_instances} |
| 241 | + |
| 242 | + ec2 = BotoClientProxy("ec2", region) |
| 243 | + ec2_instances = ec2.describe_instances(InstanceIds=list(instances_by_ec2_id.keys())) |
| 244 | + for r in ec2_instances['Reservations']: |
| 245 | + for i in r['Instances']: |
| 246 | + subnet = "{} | {}".format( |
| 247 | + i['Placement']['AvailabilityZone'], i['SubnetId'] |
| 248 | + ) |
| 249 | + instance = instances_by_ec2_id[i['InstanceId']] |
| 250 | + instances_by_subnet[subnet].append(instance) |
| 251 | + |
| 252 | + return instances_by_subnet |
| 253 | + |
| 254 | + |
| 255 | +def stateful_elastigroup_ready(stateful_instances: list): |
| 256 | + return all(i['state'] == elastigroup_api.STATEFUL_STATE_ACTIVE for i in stateful_instances) |
| 257 | + |
| 258 | + |
| 259 | +def wait_for_stateful_elastigroup(elastigroup_id: str, spotinst_account): |
| 260 | + """ |
| 261 | + Waits for all stateful instances of the ElastiGroup to be in the ACTIVE state. |
| 262 | + """ |
| 263 | + with Action("Waiting for all stateful instances to be in the ACTIVE state") as act: |
| 264 | + while True: |
| 265 | + time.sleep(WAIT_FOR_ELASTIGROUP_SEC) |
| 266 | + act.progress() |
| 267 | + stateful_instances = elastigroup_api.get_stateful_instances(elastigroup_id, spotinst_account) |
| 268 | + if stateful_elastigroup_ready(stateful_instances): |
| 269 | + break |
| 270 | + |
| 271 | + |
| 272 | +def recycle_stateful_elastigroup_instance(elastigroup_id: str, instance: dict, spotinst_account): |
| 273 | + info( |
| 274 | + "Recycling stateful instance {} ({} | {})".format( |
| 275 | + instance['id'], instance['instanceId'], instance['privateIp'] |
| 276 | + ) |
| 277 | + ) |
| 278 | + elastigroup_api.recycle_stateful_instance(elastigroup_id, instance['id'], spotinst_account) |
| 279 | + |
| 280 | + |
| 281 | +def respawn_stateless_elastigroup( |
| 282 | + elastigroup_id: str, stack_name: str, batch_size: int, spotinst_account |
| 283 | +): |
| 284 | + """ |
| 285 | + Start a deployment of the ElastiGroup and wait for it to complete. |
| 286 | + """ |
| 287 | + |
186 | 288 | if batch_size is None or batch_size < 1: |
187 | 289 | batch_size = DEFAULT_BATCH_SIZE |
188 | 290 |
|
189 | | - spotinst_account = elastigroup_api.get_spotinst_account_data(region, stack_name) |
190 | | - |
191 | 291 | info( |
192 | | - "Redeploying the cluster for ElastiGroup {} (ID {})".format( |
| 292 | + "Redeploying instances for ElastiGroup {} (ID {})".format( |
193 | 293 | stack_name, elastigroup_id |
194 | 294 | ) |
195 | 295 | ) |
|
0 commit comments