tools/ami-creator/create_ami.py (240 lines of code) (raw):
#!/usr/bin/env python3
import boto3
import sys, os, subprocess
import time, datetime
import logging
from optparse import OptionParser
import base64, binascii, getpass, optparse, sys
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5
ec2Resource = boto3.resource('ec2')
ec2Client = boto3.client('ec2')
def read_userdata(file):
logging.info("Reading userdata from file %s", file)
with open(file, "r") as fh:
return fh.read()
def create_instance(instance_type, disk_size, userdata_file, ami, security_group, ssh_key):
logging.info("Creating instance type %s for image creation", instance_type)
instances = ec2Resource.create_instances(
BlockDeviceMappings=[
{
'DeviceName': '/dev/sda1',
'Ebs': {
'DeleteOnTermination': True,
'VolumeSize': disk_size,
'VolumeType': 'gp2',
'Encrypted': False
}
}
],
ImageId=ami,
InstanceType=instance_type,
KeyName=ssh_key,
MaxCount=1,
MinCount=1,
SecurityGroupIds=[
security_group
],
UserData=read_userdata(userdata_file),
InstanceInitiatedShutdownBehavior='stop',
TagSpecifications=[
{
'ResourceType': 'instance',
'Tags': [
{ 'Key': 'Name', 'Value': 'ami-builder-tmp-instance' },
{ 'Key': 'mxnet', 'Value': 'ami-builder' }
]
}
]
)
logging.info("Created instance %s", instances[0].id)
logging.info("Public IP: %s", instances[0].public_ip_address)
logging.info("Platform: %s", instances[0].platform)
return instances[0]
def decrypt_windows_password(encrypted_password, keyfile):
with open(keyfile, "r") as fh_key:
key = RSA.importKey(fh_key.read())
cipher = PKCS1_v1_5.new(key)
sentinel = "password decryption failed!!!"
password = cipher.decrypt(encrypted_password, sentinel)
#password = password[2:-1]
return password.decode("utf-8")
def wait_for_instance(instance, private_key):
instance_id = instance.id
current_state = instance.state
logging.info("Waiting for instance to install software and shut down. Current state: %s", instance.state['Name'])
windows_password = None
last_log_size = 0
last_install_log_size = 0
while (current_state['Code'] != 80):
time.sleep(20)
i = ec2Resource.Instance(instance_id)
if current_state['Code'] != i.state['Code']:
current_state = i.state
logging.info("Instance state changed to: %s", current_state['Name'])
if current_state['Name'] == "running" and i.public_ip_address != None:
if i.platform == "windows":
if windows_password == None:
logging.debug("Attempting to get password info for instance")
try:
pwdata = i.password_data()
if pwdata['PasswordData'] != '':
logging.debug("Got password data, decrypting [%s]", pwdata['PasswordData'])
password = decrypt_windows_password(base64.b64decode(pwdata['PasswordData']), private_key)
logging.info("Public IP Address: %s", i.public_ip_address)
logging.info("Decrypted password: %s", password)
windows_password = password
except:
logging.exception("Unable to get password data for windows instance")
# attempt to save the latest userdata execute log
logfile = "log/userdata-{}.log".format(instance_id)
ret = subprocess.run(["scp","-q","-T","-o","StrictHostKeyChecking=no","-o","ConnectTimeout=10","-i",private_key,"administrator@{}:\"C:\\ProgramData\Amazon\\EC2-Windows\\Launch\\Log\\UserdataExecution.log\"".format(i.public_ip_address),logfile])
if ret.returncode == 0:
if os.stat(logfile).st_size != last_log_size:
last_log_size = os.stat(logfile).st_size
logging.info("Updated userdata execution log to %s (size=%d)", logfile, last_log_size)
else:
logging.debug("Unable to retrieve userdata log via ssh, does this windows system have sshd installed and running?")
continue
install_logfile = "log/install-{}.log".format(instance_id)
ret = subprocess.run(["scp","-q","-T","-o","StrictHostKeyChecking=no","-o","ConnectTimeout=10","-i",private_key,"administrator@{}:\"C:\\install.log\"".format(i.public_ip_address),install_logfile])
if ret.returncode == 0:
if os.stat(install_logfile).st_size != last_install_log_size:
last_install_log_size = os.stat(install_logfile).st_size
logging.info("Updated install log to %s (size=%d)", install_logfile, last_install_log_size)
else:
logging.info("Attempting to get cloud-init output log.")
os.system("ssh -o StrictHostKeyChecking=no -i {} ubuntu@{} tail -n +0 -f /var/log/cloud-init-output.log 2>/dev/null".format(private_key, i.public_ip_address))
logging.info("Instance stopped")
def create_ami(name, instance):
ami_name = "{}-{}".format(name, datetime.datetime.now().strftime("%Y%m%d%H%M"))
logging.info("Creating AMI from instance, name: %s", ami_name)
image = instance.create_image(
Description = "Image auto-created",
Name = ami_name
)
logging.info("Created image %s", image.id)
return image
def wait_for_ami(image):
ami_id = image.id
current_state = image.state
logging.info("Waiting for AMI to become available")
while (current_state != 'available'):
time.sleep(5)
i = ec2Resource.Image(ami_id)
if current_state != i.state:
current_state = i.state
logging.info("Image state changed to %s", current_state)
logging.info("AMI %s is now available", ami_id)
def terminate_instance(instance):
logging.info("Terminating instance %s", instance.id)
instance.terminate()
def get_current_lt_version(lt_id):
logging.debug("Looking up current version for LT %s", lt_id)
response = ec2Client.describe_launch_template_versions(
LaunchTemplateId = lt_id,
Versions = [ '$Latest' ]
)
try:
latest_version = response['LaunchTemplateVersions'][0]['VersionNumber']
logging.debug("Found latest version %s of LT %s", latest_version, lt_id)
return latest_version
except:
logging.error("Unable to get latest LT version for LT %s", lt_id)
def set_default_lt_version(lt_id, version):
logging.debug("Setting the default version to %s for LT %s", version, lt_id)
try:
response = ec2Client.modify_launch_template(
LaunchTemplateId = lt_id,
DefaultVersion = str(version)
)
except:
logging.error("Unable to set default LT version for LT %s", lt_id)
return False
return True
def update_launch_template(lt_id, ami_id):
latest_version = get_current_lt_version(lt_id)
if not latest_version:
logging.error("Unable to get current LT version for LT %s, not updating.", lt_id)
return None
logging.info("Updating Launch Template %s with new AMI %s", lt_id, ami_id)
response = ec2Client.create_launch_template_version(
LaunchTemplateId = lt_id,
SourceVersion = str(latest_version),
LaunchTemplateData = {
'ImageId': ami_id
}
)
new_version = response['LaunchTemplateVersion']['VersionNumber']
logging.debug("Successfully created new LT %s version %s", lt_id, new_version)
set_default_lt_version(lt_id, new_version)
return new_version
def main():
parser = OptionParser()
parser.add_option("-i", "--instance-type", dest="instance_type",
help="Instance type to create")
parser.add_option("-a", "--ami", dest="ami",
help="AMI to start with")
parser.add_option("-n", "--name", dest="name",
help="Prefix to use for AMI name (timestamp will be appended)")
parser.add_option("-d", "--disk-size", dest="disk_size", type="int",
default=10, help="Size of disk to use for image creation (in GB)")
parser.add_option("-s", "--security-group", dest="security_group",
help="Security group ID for instance")
parser.add_option("-k", "--key-name", dest="ssh_key",
help="SSH key pair name to use")
parser.add_option("-l", "--launch-templates", dest="launch_templates",
help="Comma separated list of launch template IDs to update with newly created AMI")
parser.add_option("-p", "--private-key", dest="private_key",
help="Private key used to SSH into instance or decrypt windows password")
parser.add_option("-u", "--userdata", dest="userdata",
help="UserData file to use")
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="don't print status messages to stdout")
(options, args) = parser.parse_args()
# ensure required parameters are passed
if options.instance_type is None:
logging.error("You must pass --instance-type option")
sys.exit(-1)
if options.name is None:
logging.error("You must pass --name option")
sys.exit(-1)
if options.security_group is None:
logging.error("You must pass --security-group option")
sys.exit(-1)
if options.userdata is None:
logging.error("You must pass --userdata option")
sys.exit(-1)
if options.private_key is None:
logging.error("You must pass --private-key option")
sys.exit(-1)
loglev = logging.WARNING
if options.verbose:
loglev = logging.INFO
logging.basicConfig(
level = loglev,
format = '%(asctime)s %(levelname)s %(message)s'
)
if options.userdata:
userdata = options.userdata
else:
userdata = 'userdata/{}.txt'.format(options.name)
instance = create_instance(
instance_type=options.instance_type,
disk_size=options.disk_size,
userdata_file=userdata,
ami=options.ami,
security_group=options.security_group,
ssh_key=options.ssh_key
)
wait_for_instance(instance, options.private_key)
image = create_ami(options.name, instance)
wait_for_ami(image)
terminate_instance(instance)
lt_list = options.launch_templates.split(",")
if len(lt_list) > 0:
logging.info("Updating launch templates with new AMI")
for lt_id in lt_list:
new_version = update_launch_template(lt_id, image.id)
if new_version is None:
logging.error("Unable to update LT %s", lt_id)
else:
logging.info("Created new version %s of LT %s with new AMI", new_version, lt_id)
main()