Skip to content

Commit 502aee7

Browse files
Merge pull request #20 from pranavgaikwad/master
Add cloudformation deletion script
2 parents a0a3554 + d847932 commit 502aee7

File tree

4 files changed

+75
-6
lines changed

4 files changed

+75
-6
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ share/python-wheels/
2626
.installed.cfg
2727
*.egg
2828
MANIFEST
29-
29+
.vscode/
3030
# PyInstaller
3131
# Usually these files are written by a python script from a template
3232
# before PyInstaller builds the exe, so as to inject date/other infos into it.

aws/reporting/cloudformation.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import boto3
2+
import logging
3+
from common import get_all_regions
4+
5+
DELETEABLE_STATUS = ["CREATE_FAILED", "DELETE_FAILED"]
6+
7+
AWS_RESOURCE_TYPE_EC2_INSTANCE = "AWS::EC2::Instance"
8+
9+
AWS_EKS_MANAGED_TAGS = ["alpha.eksctl.io/cluster-name"]
10+
11+
logger = logging.getLogger(__name__)
12+
13+
def get_deleteable_cf_templates(client):
14+
deleteable_stacks = []
15+
response = client.describe_stacks()
16+
for stack in response.get('Stacks', []):
17+
stackName = stack.get("StackName", "")
18+
if stackName != "":
19+
is_eks_managed = False
20+
for tag in stack.get('Tags', []):
21+
if tag.get('Key', '') in AWS_EKS_MANAGED_TAGS:
22+
is_eks_managed = True
23+
logger.info("{} Found EKS managed stack {}".format(client.meta.region_name, stackName))
24+
if not is_eks_managed:
25+
if stack.get('StackStatus', '') in DELETEABLE_STATUS:
26+
deleteable_stacks.append(stack)
27+
logger.info("{} Found stack with deleteable status {}".format(client.meta.region_name, stackName))
28+
elif not does_cf_template_have_ec2_instances(client, stackName):
29+
deleteable_stacks.append(stack)
30+
logger.info("{} Found stack without instances {}".format(client.meta.region_name, stackName))
31+
return deleteable_stacks
32+
33+
def does_cf_template_have_ec2_instances(client, stack_name: str):
34+
for resource in client.describe_stack_resources(StackName=stack_name)['StackResources']:
35+
if resource.get('ResourceType', '') == AWS_RESOURCE_TYPE_EC2_INSTANCE:
36+
return True
37+
return False
38+
39+
def delete_stacks(dry_run = False):
40+
for region in get_all_regions():
41+
client = boto3.client('cloudformation', region_name=region)
42+
stacks = get_deleteable_cf_templates(client)
43+
for stack in stacks:
44+
stackName = stack.get("StackName", "")
45+
if stackName != "":
46+
try:
47+
logger.info("{} Attempting to delete stack {}".format(region, stackName))
48+
if not dry_run:
49+
client.delete_stack(StackName=stackName)
50+
logger.info("{} Deleted stack {}".format(region, stackName))
51+
except:
52+
logger.info("{} Failed deleting stack {}".format(region, stackName))

aws/reporting/main.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from common import save_to_file, load_from_file
1414
from s3 import get_all_buckets, reformat_buckets_data
1515
from vpc import get_all_vpcs, delete_orphan_vpcs
16+
from cloudformation import delete_stacks
1617

1718
# number of days to qualify an instance as old
1819
OLD_INSTANCE_THRESHOLD = 30
@@ -92,9 +93,14 @@ def delete_vpcs():
9293
deleted_vpcs = delete_orphan_vpcs(vpcs)
9394
return deleted_vpcs
9495

95-
def get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet):
96+
def get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet, summarySheet):
9697
sheet_link = os.environ['SHEET_LINK']
9798
old_instances = prepare_old_instances_data(allInstancesSheet, oldInstancesSheet)
99+
if summarySheet is not None:
100+
try:
101+
total_ec2_deleted = summarySheet.read_custom('J1', 'J1')[0][0]
102+
except:
103+
total_ec2_deleted = None
98104
message = """
99105
All,<br>
100106
<br>
@@ -125,7 +131,7 @@ def get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet):
125131

126132
inst_id = inst.get('InstanceId', '')
127133
owner = inst.get('owner', 'OwnerNotFound')
128-
if owner != 'OwnerNotFound':
134+
if owner != 'OwnerNotFound' and owner != "":
129135
guid = inst.get('guid', '')
130136
if owner in unique_owners:
131137
unique_owners[owner]['count'] += 1
@@ -142,9 +148,11 @@ def get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet):
142148
summary_email += "- {} unsaved instances owned by {} associated with GUIDs <b>{}</b><br>"\
143149
.format(v.get('count'), k, v.get('guids'))
144150
if len(orphan_instances) > 0:
145-
summary_email += "\n Following instances could not be associated with owners:\n"
151+
summary_email += "<br><br> Following instances could not be associated with owners:<br>"
146152
for inst in orphan_instances:
147153
summary_email += "- Instance Id : {}, Region : {}".format(inst['InstanceId'], inst['AvailabilityZone'])
154+
if total_ec2_deleted is not None:
155+
summary_email += "<br><br> Total EC2 instances deleted so far: <b>{}</b><br>".format(str(total_ec2_deleted))
148156
return message.format(sheet_link, sheet_link, scheduled, summary_email)
149157

150158
if __name__ == "__main__":
@@ -235,9 +243,10 @@ def get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet):
235243
elif args[1] == 'purge_instances':
236244
numberOfInstancesDeleted = terminate_instances(oldInstancesSheet, allInstancesSheet)
237245
summaryRow['EC2 Cleanup'] = 'Deleted {} instances'.format(numberOfInstancesDeleted)
238-
246+
delete_stacks()
247+
239248
elif args[1] == 'generate_ec2_deletion_summary':
240-
summaryEmail = get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet)
249+
summaryEmail = get_old_instances_email_summary(oldInstancesSheet, allInstancesSheet, summarySheet)
241250
if summaryEmail is not None:
242251
smtp_addr = os.environ['SMTP_ADDR']
243252
smtp_username = os.environ['SMTP_USERNAME']

aws/reporting/sheet.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ def _column_to_letter_identifier(self, column_id):
4343
def get_sheet_range(self):
4444
return "{}!{}{}:Z".format(
4545
self.sheet_name, 'A', self.title_rows+1)
46+
47+
def get_custom_range(self, start, end):
48+
return "{}!{}:{}".format(
49+
self.sheet_name, start, end)
50+
51+
def read_custom(self, start, end, indexField=None):
52+
return self.client.service.spreadsheets().values().get(
53+
spreadsheetId=self.sheet_id, range=self.get_custom_range(start, end)).execute()['values']
4654

4755
def read_spreadsheet(self, indexField=None):
4856
return self.from_sheet_data(self.client.service.spreadsheets().values().get(

0 commit comments

Comments
 (0)