· 4 years ago · May 07, 2021, 10:16 AM
1#!/usr/bin/python3
2
3import boto3
4import argparse
5
6parser = argparse.ArgumentParser(description="Script used to restore latest snapshots for a given instance-id into a new AMI, and start a new instance from it. (-i option). When you don't need them anymore, you can cleanup the AMI and instance created with -d and -t options.")
7group = parser.add_mutually_exclusive_group(required=True)
8parser.add_argument('-p', '--profile', dest='profile', required=True,
9 help='Takes an AWS profile in argument to authenticate against AWS API')
10parser.add_argument("-r", "--region", dest="region", default='eu-west-1',
11 help="Takes an AWS region in argument")
12group.add_argument('-i', '--instance-id', dest='instance_id',
13 help='Takes an EC2 instance ID in argument to create an AMI with latest corresponding snapshots for this instance, then start a new instance with this AMI.')
14group.add_argument('-d', '--deregister-ami', dest='deregister',
15 help='Takes a valid AMI ID in argument to deregister it')
16group.add_argument('-t', '--terminate', dest='terminate',
17 help='Takes a valid instance ID in argument to terminate it. The instance must have a tag "CreatedBy = RestoreScript"')
18
19args = parser.parse_args()
20
21session = boto3.Session(profile_name=args.profile, region_name=args.region)
22ec2 = session.resource('ec2')
23ec2_client = session.client('ec2')
24
25## Functions
26
27# get the latest snapshot id for a given volume
28def get_last_snapshot(conn, volume_id):
29 snapshots = conn.describe_snapshots(
30 Filters=[
31 {
32 'Name': 'volume-id',
33 'Values': [volume_id]
34 }
35 ]
36 )
37
38 sorted_snapshot_list = sorted(snapshots['Snapshots'], key=lambda k: k['StartTime'], reverse=True)
39
40 if (len(sorted_snapshot_list) >= 1):
41 return sorted_snapshot_list[0].get('SnapshotId')
42 else:
43 raise Exception('No snapshot found for volume %s' % volume_id)
44
45# create a volume mapping from existing volume
46def create_volume_mapping(conn, volume_id, snapshot_id):
47 volume = conn.Volume(volume_id)
48 volume_attachment = volume.attachments[0]
49 mapping = {}
50
51 mapping['DeviceName'] = volume_attachment.get('Device')
52 mapping['Ebs'] = {}
53 mapping['Ebs']['DeleteOnTermination'] = True
54 if volume.volume_type == 'io1':
55 mapping['Ebs']['Iops'] = volume.iops
56 mapping['Ebs']['SnapshotId'] = snapshot_id
57 mapping['Ebs']['VolumeSize'] = volume.size
58 mapping['Ebs']['VolumeType'] = volume.volume_type
59
60 return(mapping)
61
62# create an AMI with a defined block device mapping
63def create_ami(conn, ami_name, root_device_name, bdm):
64 create_image = conn.register_image(
65 Architecture='x86_64',
66 BlockDeviceMappings=bdm,
67 EnaSupport=True,
68 Name=ami_name,
69 RootDeviceName=root_device_name,
70 VirtualizationType='hvm'
71 )
72
73 return(create_image.get('ImageId'))
74
75def create_instance(conn, subnet_id, instance_type, ami_id, ssh_key_name, sg_id, name_tag):
76 restored_instance = conn.create_instances(
77 ImageId=ami_id,
78 InstanceType=instance_type,
79 KeyName=ssh_key_name,
80 SecurityGroupIds=[sg_id],
81 SubnetId=subnet_id,
82 MinCount=1,
83 MaxCount=1,
84 TagSpecifications=[
85 {
86 'ResourceType': 'instance',
87 'Tags': [
88 {
89 'Key': 'Name',
90 'Value': name_tag
91 },
92 {
93 'Key': 'CreatedBy',
94 'Value': 'RestoreScript'
95 },
96 ]
97 },
98 ]
99 )
100
101 return(restored_instance[0].instance_id)
102
103# terminate instance
104def terminate_instance(conn, instance_id):
105 instance_to_terminate = conn.Instance(instance_id)
106 for tag in instance_to_terminate.tags:
107 if tag.get('Key') == 'CreatedBy' and tag.get('Value') == 'RestoreScript':
108 can_terminate = True
109 try:
110 can_terminate
111 instance_to_terminate.terminate()
112 except:
113 raise Exception('Error: Tag "CreatedBy = RestoreScript" not found on instance %s. Aborting: I cannot destroy an instance that I did not create.' % instance_id)
114
115# deregister AMI
116def deregister_ami(conn, ami_id):
117 ami = conn.Image(ami_id)
118 ami.deregister()
119
120
121## Main
122
123if args.instance_id is not None:
124 # gather infos from instance
125 src_instance = ec2.Instance(args.instance_id)
126 root_volume_name = src_instance.root_device_name
127 ssh_key_name = src_instance.key_name
128 instance_type = src_instance.instance_type
129 subnet_id = src_instance.subnet_id
130
131 # get name tag of the source instance to set a name on created AMI and EC2.
132 for tag in src_instance.tags:
133 if tag.get('Key') == 'Name':
134 name_tag = tag.get('Value') + "-restore"
135 # defaults to a generic name when the source instance doesn't have a Name tag.
136 try:
137 name_tag
138 except NameError:
139 name_tag = args.instance_id + '-restore'
140
141 # get volume ids attached to the instance
142 volume_ids = []
143 for device in src_instance.block_device_mappings:
144 volume = device.get('Ebs')
145 volume_ids.append(volume.get('VolumeId'))
146
147 # get the security group id of the std (standard) SG, allowing ssh from bastion
148 for sg in src_instance.security_groups:
149 if sg.get('GroupName').endswith('-std'):
150 sg_id_std = sg.get('GroupId')
151
152 # build the EBS mapping for the AMI
153 final_ebs_mapping = []
154 for volume in volume_ids:
155 snapshot_id = get_last_snapshot(ec2_client, volume)
156 volume_mapping = create_volume_mapping(ec2, volume, snapshot_id)
157 final_ebs_mapping.append(volume_mapping)
158
159 # create the AMI and boot an instance from it
160 try:
161 ami_id = create_ami(ec2_client, name_tag, root_volume_name, final_ebs_mapping)
162 restored_instance_id = create_instance(ec2, subnet_id, instance_type, ami_id, ssh_key_name, sg_id_std, name_tag)
163 print("AMI created: " + ami_id)
164 print("EC2 created: " + restored_instance_id)
165 print("You can connect to " + ec2.Instance(restored_instance_id).private_ip_address + " from the AWS bastion of the platform")
166
167 except Exception as e:
168 print(e)
169 print("Error during AMI or instance creation. Trying to rollback.")
170 try: restored_instance_id
171 except NameError: print("No instance to clean.")
172 else:
173 print("Remove instance " + restored_instance_id + " ...")
174 terminate_instance(ec2, restored_instance_id)
175 try: ami_id
176 except NameError: print("No AMI to clean.")
177 else:
178 print("Remove AMI " + ami_id + " ...")
179 deregister_ami(ec2, ami_id)
180
181if args.deregister is not None:
182 try:
183 deregister_ami(ec2, args.deregister)
184 except Exception as e:
185 print(e)
186 print("Cannot deregister AMI " + args.deregister)
187
188if args.terminate is not None:
189 try:
190 terminate_instance(ec2, args.terminate)
191 except Exception as e:
192 print(e)
193 print("Cannot terminate instance " + args.terminate)
194