Add basic endpoints for EC2 Launch Templates

Specifically, add the CreateLaunchTemplate, CreateLaunchTemplateVersion,
DescribeLaunchTemplates, and DescribeLaunchTemplateVersions endpoints.
This commit is contained in:
Don Kuntz 2019-08-14 16:11:05 -05:00
parent aeacd3d0c4
commit aa3b6085d1
6 changed files with 557 additions and 2 deletions

View File

@ -523,3 +523,10 @@ class OperationNotPermitted3(EC2ClientError):
pcx_id,
acceptor_region)
)
class InvalidLaunchTemplateNameError(EC2ClientError):
def __init__(self):
super(InvalidLaunchTemplateNameError, self).__init__(
"InvalidLaunchTemplateName.AlreadyExistsException",
"Launch template name already in use."
)

View File

@ -19,7 +19,8 @@ from boto.ec2.instance import Instance as BotoInstance, Reservation
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest
from boto.ec2.launchspecification import LaunchSpecification
from xml.etree import ElementTree
from xml.dom import minidom
from moto.compat import OrderedDict
from moto.core import BaseBackend
@ -49,6 +50,7 @@ from .exceptions import (
InvalidKeyPairDuplicateError,
InvalidKeyPairFormatError,
InvalidKeyPairNameError,
InvalidLaunchTemplateNameError,
InvalidNetworkAclIdError,
InvalidNetworkAttachmentIdError,
InvalidNetworkInterfaceIdError,
@ -98,6 +100,7 @@ from .utils import (
random_internet_gateway_id,
random_ip,
random_ipv6_cidr,
random_launch_template_id,
random_nat_gateway_id,
random_key_pair,
random_private_ip,
@ -4112,6 +4115,84 @@ class NatGatewayBackend(object):
def delete_nat_gateway(self, nat_gateway_id):
return self.nat_gateways.pop(nat_gateway_id)
class LaunchTemplateVersion(object):
def __init__(self, template, number, data, description):
self.template = template
self.number = number
self.data = data
self.description = description
self.create_time = utc_date_and_time()
class LaunchTemplate(TaggedEC2Resource):
def __init__(self, backend, name, template_data, version_description):
self.ec2_backend = backend
self.name = name
self.id = random_launch_template_id()
self.create_time = utc_date_and_time()
self.versions = []
self.create_version(template_data, version_description)
self.default_version_number = 1
def create_version(self, data, description):
num = len(self.versions) + 1
version = LaunchTemplateVersion(self, num, data, description)
self.versions.append(version)
return version
def is_default(self, version):
return self.default_version == version.number
def get_version(self, num):
return self.versions[num-1]
def default_version(self):
return self.versions[self.default_version_number-1]
def latest_version(self):
return self.versions[-1]
@property
def latest_version_number(self):
return self.latest_version().number
def get_filter_value(self, filter_name):
if filter_name == 'launch-template-name':
return self.name
else:
return super(LaunchTemplate, self).get_filter_value(
filter_name, "DescribeLaunchTemplates")
class LaunchTemplateBackend(object):
def __init__(self):
self.launch_templates_by_name = {}
self.launch_templates_by_id = {}
super(LaunchTemplateBackend, self).__init__()
def create_launch_template(self, name, description, template_data):
if name in self.launch_templates_by_name:
raise InvalidLaunchTemplateNameError()
template = LaunchTemplate(self, name, template_data, description)
self.launch_templates_by_id[template.id] = template
self.launch_templates_by_name[template.name] = template
return template
def get_launch_template_by_name(self, name):
return self.launch_templates_by_name[name]
def get_launch_template_by_id(self, templ_id):
return self.launch_templates_by_id[templ_id]
def get_launch_templates(self, template_names=None, template_ids=None, filters=None):
if template_ids:
templates = [self.launch_templates_by_id[tid] for tid in template_ids]
elif template_names:
templates = [self.launch_templates_by_name[name] for name in template_names]
else:
templates = list(self.launch_templates_by_name.values())
return generic_filter(filters, templates)
class EC2Backend(BaseBackend, InstanceBackend, TagBackend, EBSBackend,
RegionsAndZonesBackend, SecurityGroupBackend, AmiBackend,
@ -4122,7 +4203,7 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, EBSBackend,
VPCGatewayAttachmentBackend, SpotFleetBackend,
SpotRequestBackend, ElasticAddressBackend, KeyPairBackend,
DHCPOptionsSetBackend, NetworkAclBackend, VpnGatewayBackend,
CustomerGatewayBackend, NatGatewayBackend):
CustomerGatewayBackend, NatGatewayBackend, LaunchTemplateBackend):
def __init__(self, region_name):
self.region_name = region_name
super(EC2Backend, self).__init__()
@ -4177,6 +4258,8 @@ class EC2Backend(BaseBackend, InstanceBackend, TagBackend, EBSBackend,
elif resource_prefix == EC2_RESOURCE_TO_PREFIX['internet-gateway']:
self.describe_internet_gateways(
internet_gateway_ids=[resource_id])
elif resource_prefix == EC2_RESOURCE_TO_PREFIX['launch-template']:
self.get_launch_template_by_id(resource_id)
elif resource_prefix == EC2_RESOURCE_TO_PREFIX['network-acl']:
self.get_all_network_acls()
elif resource_prefix == EC2_RESOURCE_TO_PREFIX['network-interface']:

View File

@ -14,6 +14,7 @@ from .instances import InstanceResponse
from .internet_gateways import InternetGateways
from .ip_addresses import IPAddresses
from .key_pairs import KeyPairs
from .launch_templates import LaunchTemplates
from .monitoring import Monitoring
from .network_acls import NetworkACLs
from .placement_groups import PlacementGroups
@ -49,6 +50,7 @@ class EC2Response(
InternetGateways,
IPAddresses,
KeyPairs,
LaunchTemplates,
Monitoring,
NetworkACLs,
PlacementGroups,

View File

@ -0,0 +1,243 @@
import json
import six
import uuid
from moto.core.responses import BaseResponse
from moto.ec2.models import OWNER_ID
from moto.ec2.exceptions import FilterNotImplementedError
from moto.ec2.utils import filters_from_querystring
from xml.etree import ElementTree
from xml.dom import minidom
def xml_root(name):
root = ElementTree.Element(name, {
"xmlns": "http://ec2.amazonaws.com/doc/2016-11-15/"
})
request_id = str(uuid.uuid4()) + "example"
ElementTree.SubElement(root, "requestId").text = request_id
return root
def xml_serialize(tree, key, value):
if key:
name = key[0].lower() + key[1:]
if isinstance(value, list):
if name[-1] == 's':
name = name[:-1]
name = name + 'Set'
node = ElementTree.SubElement(tree, name)
else:
node = tree
if isinstance(value, (str, int, float)):
node.text = str(value)
elif isinstance(value, bool):
node.text = str(value).lower()
elif isinstance(value, dict):
for dictkey, dictvalue in six.iteritems(value):
xml_serialize(node, dictkey, dictvalue)
elif isinstance(value, list):
for item in value:
xml_serialize(node, 'item', item)
def pretty_xml(tree):
rough = ElementTree.tostring(tree, 'utf-8')
parsed = minidom.parseString(rough)
return parsed.toprettyxml(indent=' ')
def parse_object(raw_data):
out_data = {}
for key, value in six.iteritems(raw_data):
key_fix_splits = key.split("_")
l = len(key_fix_splits)
new_key = ""
for i in range(0, l):
new_key += key_fix_splits[i][0].upper() + key_fix_splits[i][1:]
data = out_data
splits = new_key.split(".")
for split in splits[:-1]:
if split not in data:
data[split] = {}
data = data[split]
data[splits[-1]] = value
out_data = parse_lists(out_data)
return out_data
def parse_lists(data):
for key, value in six.iteritems(data):
if isinstance(value, dict):
keys = data[key].keys()
is_list = all(map(lambda k: k.isnumeric(), keys))
if is_list:
new_value = []
keys = sorted(list(keys))
for k in keys:
lvalue = value[k]
if isinstance(lvalue, dict):
lvalue = parse_lists(lvalue)
new_value.append(lvalue)
data[key] = new_value
return data
class LaunchTemplates(BaseResponse):
def create_launch_template(self):
name = self._get_param('LaunchTemplateName')
version_description = self._get_param('VersionDescription')
tag_spec = self._get_param('TagSpecifications')
raw_template_data = self._get_dict_param('LaunchTemplateData.')
parsed_template_data = parse_object(raw_template_data)
if tag_spec:
if 'TagSpecifications' not in parsed_template_data:
parsed_template_data['TagSpecifications'] = []
parsed_template_data['TagSpecifications'].extend(tag_spec)
if self.is_not_dryrun('CreateLaunchTemplate'):
template = self.ec2_backend.create_launch_template(name, version_description, parsed_template_data)
version = template.default_version()
tree = xml_root("CreateLaunchTemplateResponse")
xml_serialize(tree, "launchTemplate", {
"createTime": version.create_time,
"createdBy": "arn:aws:iam::{OWNER_ID}:root".format(OWNER_ID=OWNER_ID),
"defaultVersionNumber": template.default_version_number,
"latestVersionNumber": version.number,
"launchTemplateId": template.id,
"launchTemplateName": template.name
})
return pretty_xml(tree)
def create_launch_template_version(self):
name = self._get_param('LaunchTemplateName')
tmpl_id = self._get_param('LaunchTemplateId')
if name:
template = self.ec2_backend.get_launch_template_by_name(name)
if tmpl_id:
template = self.ec2_backend.get_launch_template_by_id(tmpl_id)
version_description = self._get_param('VersionDescription')
tag_spec = self._get_param('TagSpecifications')
# source_version = self._get_int_param('SourceVersion')
raw_template_data = self._get_dict_param('LaunchTemplateData.')
template_data = parse_object(raw_template_data)
if self.is_not_dryrun('CreateLaunchTemplate'):
version = template.create_version(template_data, version_description)
tree = xml_root("CreateLaunchTemplateVersionResponse")
xml_serialize(tree, "launchTemplateVersion", {
"createTime": version.create_time,
"createdBy": "arn:aws:iam::{OWNER_ID}:root".format(OWNER_ID=OWNER_ID),
"defaultVersion": template.is_default(version),
"launchTemplateData": version.data,
"launchTemplateId": template.id,
"launchTemplateName": template.name,
"versionDescription": version.description,
"versionNumber": version.number,
})
return pretty_xml(tree)
# def delete_launch_template(self):
# pass
# def delete_launch_template_versions(self):
# pass
def describe_launch_template_versions(self):
name = self._get_param('LaunchTemplateName')
template_id = self._get_param('LaunchTemplateId')
if name:
template = self.ec2_backend.get_launch_template_by_name(name)
if template_id:
template = self.ec2_backend.get_launch_template_by_id(template_id)
max_results = self._get_int_param("MaxResults", 15)
versions = self._get_multi_param("Versions")
min_version = self._get_int_param("MinVersion")
max_version = self._get_int_param("MaxVersion")
filters = filters_from_querystring(self.querystring)
if filters:
raise FilterNotImplementedError("all filters", "DescribeLaunchTemplateVersions")
if self.is_not_dryrun('DescribeLaunchTemplateVersions'):
tree = ElementTree.Element("DescribeLaunchTemplateVersionsResponse", {
"xmlns": "http://ec2.amazonaws.com/doc/2016-11-15/",
})
request_id = ElementTree.SubElement(tree, "requestId")
request_id.text = "65cadec1-b364-4354-8ca8-4176dexample"
versions_node = ElementTree.SubElement(tree, "launchTemplateVersionSet")
ret_versions = []
if versions:
for v in versions:
ret_versions.append(template.get_version(int(v)))
elif min_version:
if max_version:
vMax = max_version
else:
vMax = min_version + max_results
ret_versions = template.versions[min_version-1:vMax-1]
elif max_version:
ret_versions = template.versions[0:max_version-1]
else:
ret_versions = template.versions
ret_versions = ret_versions[:max_results]
for version in ret_versions:
xml_serialize(versions_node, "item", {
"createTime": version.create_time,
"createdBy": "arn:aws:iam::{OWNER_ID}:root".format(OWNER_ID=OWNER_ID),
"defaultVersion": True,
"launchTemplateData": version.data,
"launchTemplateId": template.id,
"launchTemplateName": template.name,
"versionDescription": version.description,
"versionNumber": version.number,
})
return pretty_xml(tree)
def describe_launch_templates(self):
max_results = self._get_int_param("MaxResults", 15)
template_names = self._get_multi_param("LaunchTemplateName")
template_ids = self._get_multi_param("LaunchTemplateId")
filters = filters_from_querystring(self.querystring)
if self.is_not_dryrun("DescribeLaunchTemplates"):
tree = ElementTree.Element("DescribeLaunchTemplatesResponse")
templates_node = ElementTree.SubElement(tree, "launchTemplates")
templates = self.ec2_backend.get_launch_templates(template_names=template_names, template_ids=template_ids, filters=filters)
templates = templates[:max_results]
for template in templates:
xml_serialize(templates_node, "item", {
"createTime": template.create_time,
"createdBy": "arn:aws:iam::{OWNER_ID}:root".format(OWNER_ID=OWNER_ID),
"defaultVersionNumber": template.default_version_number,
"latestVersionNumber": template.latest_version_number,
"launchTemplateId": template.id,
"launchTemplateName": template.name,
})
return pretty_xml(tree)
# def modify_launch_template(self):
# pass

View File

@ -20,6 +20,7 @@ EC2_RESOURCE_TO_PREFIX = {
'image': 'ami',
'instance': 'i',
'internet-gateway': 'igw',
'launch-template': 'lt',
'nat-gateway': 'nat',
'network-acl': 'acl',
'network-acl-subnet-assoc': 'aclassoc',
@ -161,6 +162,10 @@ def random_nat_gateway_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX['nat-gateway'], size=17)
def random_launch_template_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX['launch-template'], size=17)
def random_public_ip():
return '54.214.{0}.{1}'.format(random.choice(range(255)),
random.choice(range(255)))

View File

@ -0,0 +1,215 @@
import boto3
import sure # noqa
from nose.tools import assert_raises
from botocore.client import ClientError
from moto import mock_ec2
@mock_ec2
def test_launch_template_create():
cli = boto3.client("ec2")
resp = cli.create_launch_template(
LaunchTemplateName="test-template",
# the absolute minimum needed to create a template without other resources
LaunchTemplateData={
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [{
"Key": "test",
"Value": "value",
}],
}],
},
)
resp.should.have.key("LaunchTemplate")
lt = resp["LaunchTemplate"]
lt["LaunchTemplateName"].should.equal("test-template")
lt["DefaultVersionNumber"].should.equal(1)
lt["LatestVersionNumber"].should.equal(1)
with assert_raises(ClientError) as ex:
cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData={
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [{
"Key": "test",
"Value": "value",
}],
}],
},
)
str(ex.exception).should.equal(
'An error occurred (InvalidLaunchTemplateName.AlreadyExistsException) when calling the CreateLaunchTemplate operation: Launch template name already in use.')
@mock_ec2
def test_describe_launch_template_versions():
template_data = {
"ImageId": "ami-abc123",
"DisableApiTermination": False,
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [{
"Key": "test",
"Value": "value",
}],
}],
"SecurityGroupIds": [
"sg-1234",
"sg-ab5678",
],
}
cli = boto3.client("ec2")
create_resp = cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData=template_data)
# test using name
resp = cli.describe_launch_template_versions(
LaunchTemplateName="test-template",
Versions=['1'])
templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
templ.should.equal(template_data)
# test using id
resp = cli.describe_launch_template_versions(
LaunchTemplateId=create_resp["LaunchTemplate"]["LaunchTemplateId"],
Versions=['1'])
templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
templ.should.equal(template_data)
@mock_ec2
def test_create_launch_template_version():
cli = boto3.client("ec2")
create_resp = cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
version_resp = cli.create_launch_template_version(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-def456"
},
VersionDescription="new ami")
version_resp.should.have.key("LaunchTemplateVersion")
version = version_resp["LaunchTemplateVersion"]
version["DefaultVersion"].should.equal(False)
version["LaunchTemplateId"].should.equal(create_resp["LaunchTemplate"]["LaunchTemplateId"])
version["VersionDescription"].should.equal("new ami")
version["VersionNumber"].should.equal(2)
@mock_ec2
def test_describe_template_versions_with_multiple_versions():
cli = boto3.client("ec2")
cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
cli.create_launch_template_version(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-def456"
},
VersionDescription="new ami")
resp = cli.describe_launch_template_versions(
LaunchTemplateName="test-template")
resp["LaunchTemplateVersions"].should.have.length_of(2)
resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]["ImageId"].should.equal("ami-abc123")
resp["LaunchTemplateVersions"][1]["LaunchTemplateData"]["ImageId"].should.equal("ami-def456")
@mock_ec2
def test_describe_launch_templates():
cli = boto3.client("ec2")
lt_ids = []
r = cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
lt_ids.append(r["LaunchTemplate"]["LaunchTemplateId"])
r = cli.create_launch_template(
LaunchTemplateName="test-template2",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
lt_ids.append(r["LaunchTemplate"]["LaunchTemplateId"])
# general call, all templates
resp = cli.describe_launch_templates()
resp.should.have.key("LaunchTemplates")
resp["LaunchTemplates"].should.have.length_of(2)
resp["LaunchTemplates"][0]["LaunchTemplateName"].should.equal("test-template")
resp["LaunchTemplates"][1]["LaunchTemplateName"].should.equal("test-template2")
# filter by names
resp = cli.describe_launch_templates(
LaunchTemplateNames=["test-template2", "test-template"])
resp.should.have.key("LaunchTemplates")
resp["LaunchTemplates"].should.have.length_of(2)
resp["LaunchTemplates"][0]["LaunchTemplateName"].should.equal("test-template2")
resp["LaunchTemplates"][1]["LaunchTemplateName"].should.equal("test-template")
# filter by ids
resp = cli.describe_launch_templates(LaunchTemplateIds=lt_ids)
resp.should.have.key("LaunchTemplates")
resp["LaunchTemplates"].should.have.length_of(2)
resp["LaunchTemplates"][0]["LaunchTemplateName"].should.equal("test-template")
resp["LaunchTemplates"][1]["LaunchTemplateName"].should.equal("test-template2")
@mock_ec2
def test_describe_launch_templates_with_filters():
cli = boto3.client("ec2")
r = cli.create_launch_template(
LaunchTemplateName="test-template",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
cli.create_tags(
Resources=[r["LaunchTemplate"]["LaunchTemplateId"]],
Tags=[
{"Key": "tag1", "Value": "a value"},
{"Key": "another-key", "Value": "this value"},
])
cli.create_launch_template(
LaunchTemplateName="no-tags",
LaunchTemplateData={
"ImageId": "ami-abc123"
})
resp = cli.describe_launch_templates(Filters=[{
"Name": "tag:tag1", "Values": ["a value"]
}])
resp["LaunchTemplates"].should.have.length_of(1)
resp["LaunchTemplates"][0]["LaunchTemplateName"].should.equal("test-template")
resp = cli.describe_launch_templates(Filters=[{
"Name": "launch-template-name", "Values": ["no-tags"]
}])
resp["LaunchTemplates"].should.have.length_of(1)
resp["LaunchTemplates"][0]["LaunchTemplateName"].should.equal("no-tags")