diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index 1e1209e22..56fb6a774 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -3,7 +3,7 @@ from collections import defaultdict
from boto.ec2.instance import Instance, InstanceState, Reservation
from moto.core import BaseBackend
-from .utils import random_instance_id, random_reservation_id
+from .utils import random_instance_id, random_reservation_id, random_ami_id
class InstanceBackend(object):
@@ -65,6 +65,16 @@ class InstanceBackend(object):
return rebooted_instances
+ def modify_instance_attribute(self, instance_id, key, value):
+ instance = self.get_instance(instance_id)
+ setattr(instance, key, value)
+ return instance
+
+ def describe_instance_attribute(self, instance_id, key):
+ instance = self.get_instance(instance_id)
+ value = getattr(instance, key)
+ return instance, value
+
def all_instances(self):
instances = []
for reservation in self.all_reservations():
@@ -104,7 +114,45 @@ class TagBackend(object):
return results
-class EC2Backend(BaseBackend, InstanceBackend, TagBackend):
+class Ami(object):
+ def __init__(self, ami_id, instance, name, description):
+ self.id = ami_id
+ self.instance = instance
+ self.instance_id = instance.id
+ self.name = name
+ self.description = description
+
+ self.virtualization_type = instance.virtualization_type
+ self.kernel_id = instance.kernel
+
+class AmiBackend(object):
+ def __init__(self):
+ self.amis = {}
+ super(AmiBackend, self).__init__()
+
+ def create_image(self, instance_id, name, description):
+ # TODO: check that instance exists and pull info from it.
+ ami_id = random_ami_id()
+ instance = ec2_backend.get_instance(instance_id)
+ if not instance:
+ return None
+ ami = Ami(ami_id, instance, name, description)
+ self.amis[ami_id] = ami
+ return ami
+
+ def describe_images(self):
+ return self.amis.values()
+
+ def get_image(self, ami_id):
+ return self.amis[ami_id]
+
+ def deregister_image(self, ami_id):
+ if ami_id in self.amis:
+ self.amis.pop(ami_id)
+ return True
+ return False
+
+class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend):
pass
@@ -112,33 +160,33 @@ ec2_backend = EC2Backend()
-{
-#'Instances': ['DescribeInstanceAttribute', 'DescribeInstances', '\n\t\t\tDescribeInstanceStatus\n\t\t', 'ImportInstance', 'ModifyInstanceAttribute', 'RebootInstances', 'ReportInstanceStatus', 'ResetInstanceAttribute', 'RunInstances', 'StartInstances', 'StopInstances', 'TerminateInstances'],
-#'Tags': ['CreateTags', 'DeleteTags', 'DescribeTags'],
-'IP Addresses': ['AssignPrivateIpAddresses', 'UnassignPrivateIpAddresses'],
-'Monitoring': ['MonitorInstances', 'UnmonitorInstances'],
-'Reserved Instances': ['CancelReservedInstancesListing', 'CreateReservedInstancesListing', 'DescribeReservedInstances', 'DescribeReservedInstancesListings', 'DescribeReservedInstancesOfferings', 'PurchaseReservedInstancesOffering'],
-'VPN Connections (Amazon VPC)': ['CreateVpnConnection', 'DeleteVpnConnection', 'DescribeVpnConnections'],
-'DHCP Options (Amazon VPC)': ['AssociateDhcpOptions', 'CreateDhcpOptions', 'DeleteDhcpOptions', 'DescribeDhcpOptions'],
-'Network ACLs (Amazon VPC)': ['CreateNetworkAcl', 'CreateNetworkAclEntry', 'DeleteNetworkAcl', 'DeleteNetworkAclEntry', 'DescribeNetworkAcls', 'ReplaceNetworkAclAssociation', 'ReplaceNetworkAclEntry'],
-'Elastic Block Store': ['AttachVolume', 'CopySnapshot', 'CreateSnapshot', 'CreateVolume', 'DeleteSnapshot', 'DeleteVolume', 'DescribeSnapshotAttribute', 'DescribeSnapshots', 'DescribeVolumes', 'DescribeVolumeAttribute', 'DescribeVolumeStatus', 'DetachVolume', 'EnableVolumeIO', 'ImportVolume', 'ModifySnapshotAttribute', 'ModifyVolumeAttribute', 'ResetSnapshotAttribute'],
-'Customer Gateways (Amazon VPC)': ['CreateCustomerGateway', 'DeleteCustomerGateway', 'DescribeCustomerGateways'],
-'Subnets (Amazon VPC)': ['CreateSubnet', 'DeleteSubnet', 'DescribeSubnets'],
-'AMIs': ['CreateImage', 'DeregisterImage', 'DescribeImageAttribute', 'DescribeImages', 'ModifyImageAttribute', 'RegisterImage', 'ResetImageAttribute'],
-'Virtual Private Gateways (Amazon VPC)': ['AttachVpnGateway', 'CreateVpnGateway', 'DeleteVpnGateway', 'DescribeVpnGateways', 'DetachVpnGateway'],
-'Availability Zones and Regions': ['DescribeAvailabilityZones', 'DescribeRegions'],
-'VPCs (Amazon VPC)': ['CreateVpc', 'DeleteVpc', 'DescribeVpcs'],
-'Windows': ['BundleInstance', 'CancelBundleTask', 'DescribeBundleTasks', 'GetPasswordData'],
-'VM Import': ['CancelConversionTask', 'DescribeConversionTasks', 'ImportInstance', 'ImportVolume'],
-'Placement Groups': ['CreatePlacementGroup', 'DeletePlacementGroup', 'DescribePlacementGroups'],
-'Key Pairs': ['CreateKeyPair', 'DeleteKeyPair', 'DescribeKeyPairs', 'ImportKeyPair'],
-'Amazon DevPay': ['ConfirmProductInstance'],
-'Internet Gateways (Amazon VPC)': ['AttachInternetGateway', 'CreateInternetGateway', 'DeleteInternetGateway', 'DescribeInternetGateways', 'DetachInternetGateway'],
-'Route Tables (Amazon VPC)': ['AssociateRouteTable', 'CreateRoute', 'CreateRouteTable', 'DeleteRoute', 'DeleteRouteTable', 'DescribeRouteTables', 'DisassociateRouteTable', 'ReplaceRoute', 'ReplaceRouteTableAssociation'],
-'Elastic Network Interfaces (Amazon VPC)': ['AttachNetworkInterface', 'CreateNetworkInterface', 'DeleteNetworkInterface', 'DescribeNetworkInterfaceAttribute', 'DescribeNetworkInterfaces', 'DetachNetworkInterface', 'ModifyNetworkInterfaceAttribute', 'ResetNetworkInterfaceAttribute'],
-'Elastic IP Addresses': ['AllocateAddress', 'AssociateAddress', 'DescribeAddresses', 'DisassociateAddress', 'ReleaseAddress'],
-'Security Groups': ['AuthorizeSecurityGroupEgress', 'AuthorizeSecurityGroupIngress', 'CreateSecurityGroup', 'DeleteSecurityGroup', 'DescribeSecurityGroups', 'RevokeSecurityGroupEgress', 'RevokeSecurityGroupIngress'],
-'General': ['GetConsoleOutput'],
-'VM Export': ['CancelExportTask', 'CreateInstanceExportTask', 'DescribeExportTasks'],
-'Spot Instances': ['CancelSpotInstanceRequests', 'CreateSpotDatafeedSubscription', 'DeleteSpotDatafeedSubscription', 'DescribeSpotDatafeedSubscription', 'DescribeSpotInstanceRequests', 'DescribeSpotPriceHistory', 'RequestSpotInstances']
-}
\ No newline at end of file
+# {
+# #'Instances': ['DescribeInstanceAttribute', 'DescribeInstances', '\n\t\t\tDescribeInstanceStatus\n\t\t', 'ImportInstance', 'ModifyInstanceAttribute', 'RebootInstances', 'ReportInstanceStatus', 'ResetInstanceAttribute', 'RunInstances', 'StartInstances', 'StopInstances', 'TerminateInstances'],
+# #'Tags': ['CreateTags', 'DeleteTags', 'DescribeTags'],
+# 'IP Addresses': ['AssignPrivateIpAddresses', 'UnassignPrivateIpAddresses'],
+# 'Monitoring': ['MonitorInstances', 'UnmonitorInstances'],
+# 'Reserved Instances': ['CancelReservedInstancesListing', 'CreateReservedInstancesListing', 'DescribeReservedInstances', 'DescribeReservedInstancesListings', 'DescribeReservedInstancesOfferings', 'PurchaseReservedInstancesOffering'],
+# 'VPN Connections (Amazon VPC)': ['CreateVpnConnection', 'DeleteVpnConnection', 'DescribeVpnConnections'],
+# 'DHCP Options (Amazon VPC)': ['AssociateDhcpOptions', 'CreateDhcpOptions', 'DeleteDhcpOptions', 'DescribeDhcpOptions'],
+# 'Network ACLs (Amazon VPC)': ['CreateNetworkAcl', 'CreateNetworkAclEntry', 'DeleteNetworkAcl', 'DeleteNetworkAclEntry', 'DescribeNetworkAcls', 'ReplaceNetworkAclAssociation', 'ReplaceNetworkAclEntry'],
+# 'Elastic Block Store': ['AttachVolume', 'CopySnapshot', 'CreateSnapshot', 'CreateVolume', 'DeleteSnapshot', 'DeleteVolume', 'DescribeSnapshotAttribute', 'DescribeSnapshots', 'DescribeVolumes', 'DescribeVolumeAttribute', 'DescribeVolumeStatus', 'DetachVolume', 'EnableVolumeIO', 'ImportVolume', 'ModifySnapshotAttribute', 'ModifyVolumeAttribute', 'ResetSnapshotAttribute'],
+# 'Customer Gateways (Amazon VPC)': ['CreateCustomerGateway', 'DeleteCustomerGateway', 'DescribeCustomerGateways'],
+# 'Subnets (Amazon VPC)': ['CreateSubnet', 'DeleteSubnet', 'DescribeSubnets'],
+# 'AMIs': ['CreateImage', 'DeregisterImage', 'DescribeImageAttribute', 'DescribeImages', 'ModifyImageAttribute', 'RegisterImage', 'ResetImageAttribute'],
+# 'Virtual Private Gateways (Amazon VPC)': ['AttachVpnGateway', 'CreateVpnGateway', 'DeleteVpnGateway', 'DescribeVpnGateways', 'DetachVpnGateway'],
+# 'Availability Zones and Regions': ['DescribeAvailabilityZones', 'DescribeRegions'],
+# 'VPCs (Amazon VPC)': ['CreateVpc', 'DeleteVpc', 'DescribeVpcs'],
+# 'Windows': ['BundleInstance', 'CancelBundleTask', 'DescribeBundleTasks', 'GetPasswordData'],
+# 'VM Import': ['CancelConversionTask', 'DescribeConversionTasks', 'ImportInstance', 'ImportVolume'],
+# 'Placement Groups': ['CreatePlacementGroup', 'DeletePlacementGroup', 'DescribePlacementGroups'],
+# 'Key Pairs': ['CreateKeyPair', 'DeleteKeyPair', 'DescribeKeyPairs', 'ImportKeyPair'],
+# 'Amazon DevPay': ['ConfirmProductInstance'],
+# 'Internet Gateways (Amazon VPC)': ['AttachInternetGateway', 'CreateInternetGateway', 'DeleteInternetGateway', 'DescribeInternetGateways', 'DetachInternetGateway'],
+# 'Route Tables (Amazon VPC)': ['AssociateRouteTable', 'CreateRoute', 'CreateRouteTable', 'DeleteRoute', 'DeleteRouteTable', 'DescribeRouteTables', 'DisassociateRouteTable', 'ReplaceRoute', 'ReplaceRouteTableAssociation'],
+# 'Elastic Network Interfaces (Amazon VPC)': ['AttachNetworkInterface', 'CreateNetworkInterface', 'DeleteNetworkInterface', 'DescribeNetworkInterfaceAttribute', 'DescribeNetworkInterfaces', 'DetachNetworkInterface', 'ModifyNetworkInterfaceAttribute', 'ResetNetworkInterfaceAttribute'],
+# 'Elastic IP Addresses': ['AllocateAddress', 'AssociateAddress', 'DescribeAddresses', 'DisassociateAddress', 'ReleaseAddress'],
+# 'Security Groups': ['AuthorizeSecurityGroupEgress', 'AuthorizeSecurityGroupIngress', 'CreateSecurityGroup', 'DeleteSecurityGroup', 'DescribeSecurityGroups', 'RevokeSecurityGroupEgress', 'RevokeSecurityGroupIngress'],
+# 'General': ['GetConsoleOutput'],
+# 'VM Export': ['CancelExportTask', 'CreateInstanceExportTask', 'DescribeExportTasks'],
+# 'Spot Instances': ['CancelSpotInstanceRequests', 'CreateSpotDatafeedSubscription', 'DeleteSpotDatafeedSubscription', 'DescribeSpotDatafeedSubscription', 'DescribeSpotInstanceRequests', 'DescribeSpotPriceHistory', 'RequestSpotInstances']
+# }
\ No newline at end of file
diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py
index ab034badd..ab7e2b685 100644
--- a/moto/ec2/responses/__init__.py
+++ b/moto/ec2/responses/__init__.py
@@ -3,7 +3,7 @@ from urlparse import parse_qs
from moto.ec2.utils import camelcase_to_underscores, method_namess_from_class
from .amazon_dev_pay import AmazonDevPay
-from .amis import AMIs
+from .amis import AmisResponse
from .availability_zonesand_regions import AvailabilityZonesandRegions
from .customer_gateways import CustomerGateways
from .dhcp_options import DHCPOptions
@@ -35,7 +35,7 @@ from .tags import TagResponse
class EC2Response(object):
- sub_responses = [InstanceResponse, TagResponse]
+ sub_responses = [InstanceResponse, TagResponse, AmisResponse]
def dispatch(self, uri, body, headers):
if body:
diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py
index 8d763e5a9..9113c4a76 100644
--- a/moto/ec2/responses/amis.py
+++ b/moto/ec2/responses/amis.py
@@ -1,21 +1,37 @@
from jinja2 import Template
from moto.ec2.models import ec2_backend
-from moto.ec2.utils import resource_ids_from_querystring
+from moto.ec2.utils import instance_ids_from_querystring
-class AMIs(object):
+class AmisResponse(object):
+ def __init__(self, querystring):
+ self.querystring = querystring
+ self.instance_ids = instance_ids_from_querystring(querystring)
+
def create_image(self):
- raise NotImplementedError('AMIs.create_image is not yet implemented')
+ name = self.querystring.get('Name')[0]
+ description = self.querystring.get('Description')[0]
+ instance_id = self.instance_ids[0]
+ image = ec2_backend.create_image(instance_id, name, description)
+ if not image:
+ return "There is not instance with id {}".format(instance_id), dict(status=404)
+ template = Template(CREATE_IMAGE_RESPONSE)
+ return template.render(image=image)
def deregister_image(self):
- raise NotImplementedError('AMIs.deregister_image is not yet implemented')
+ ami_id = self.querystring.get('ImageId')[0]
+ success = ec2_backend.deregister_image(ami_id)
+ template = Template(DESCRIBE_IMAGES_RESPONSE)
+ return template.render(success=str(success).lower())
def describe_image_attribute(self):
raise NotImplementedError('AMIs.describe_image_attribute is not yet implemented')
def describe_images(self):
- raise NotImplementedError('AMIs.describe_images is not yet implemented')
+ images = ec2_backend.describe_images()
+ template = Template(DESCRIBE_IMAGES_RESPONSE)
+ return template.render(images=images)
def modify_image_attribute(self):
raise NotImplementedError('AMIs.modify_image_attribute is not yet implemented')
@@ -26,3 +42,61 @@ class AMIs(object):
def reset_image_attribute(self):
raise NotImplementedError('AMIs.reset_image_attribute is not yet implemented')
+
+CREATE_IMAGE_RESPONSE = """
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ {{ image.id }}
+"""
+
+# TODO almost all of these params should actually be templated based on the ec2 image
+DESCRIBE_IMAGES_RESPONSE = """
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ {% for image in images %}
+ -
+ {{ image.id }}
+ amazon/getting-started
+ available
+ 111122223333
+ true
+ i386
+ machine
+ {{ image.kernel_id }}
+ ari-1a2b3c4d
+ amazon
+ {{ image.name }}
+ {{ image.description }}
+ ebs
+ /dev/sda
+
+
-
+ /dev/sda1
+
+ snap-1a2b3c4d
+ 15
+ false
+ standard
+
+
+
+ {{ image.virtualization_type }}
+
+ xen
+
+ {% endfor %}
+
+"""
+
+DESCRIBE_IMAGE_RESPONSE = """
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ {{ image.id }}
+ <{{ key }}>
+ {{ value }}
+ {{key }}>
+"""
+
+DEREGISTER_IMAGE_RESPONSE = """
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+ {{ success }}
+"""
+
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 0e3ac9a5b..758b89a38 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -42,10 +42,9 @@ class InstanceResponse(object):
def describe_instance_attribute(self):
# TODO this and modify below should raise IncorrectInstanceState if instance not in stopped state
attribute = self.querystring.get("Attribute")[0]
- normalized_attribute = camelcase_to_underscores(attribute)
+ key = camelcase_to_underscores(attribute)
instance_id = self.instance_ids[0]
- instance = ec2_backend.get_instance(instance_id)
- value = getattr(instance, normalized_attribute)
+ instance, value = ec2_backend.describe_instance_attribute(instance_id, key)
template = Template(EC2_DESCRIBE_INSTANCE_ATTRIBUTE)
return template.render(instance=instance, attribute=attribute, value=value)
@@ -57,8 +56,7 @@ class InstanceResponse(object):
value = self.querystring.get(key)[0]
normalized_attribute = camelcase_to_underscores(key.split(".")[0])
instance_id = self.instance_ids[0]
- instance = ec2_backend.get_instance(instance_id)
- setattr(instance, normalized_attribute, value)
+ instance = ec2_backend.modify_instance_attribute(instance_id, normalized_attribute, value)
return EC2_MODIFY_INSTANCE_ATTRIBUTE
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 19ff92297..c86bafa71 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -18,6 +18,10 @@ def random_reservation_id():
return random_id(prefix='r')
+def random_ami_id():
+ return random_id(prefix='ami')
+
+
def instance_ids_from_querystring(querystring_dict):
instance_ids = []
for key, value in querystring_dict.iteritems():
diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py
index ac6e11efa..463effed4 100644
--- a/tests/test_ec2/test_amis.py
+++ b/tests/test_ec2/test_amis.py
@@ -1,9 +1,49 @@
import boto
+from boto.exception import EC2ResponseError
+
from sure import expect
from moto import mock_ec2
@mock_ec2
-def test_amis():
- pass
+def test_ami_create_and_delete():
+ conn = boto.connect_ec2('the_key', 'the_secret')
+ reservation = conn.run_instances('')
+ instance = reservation.instances[0]
+ image = instance.create_image("test-ami", "this is a test ami")
+
+ all_images = conn.get_all_images()
+ all_images[0].id.should.equal(image)
+
+ success = conn.deregister_image(image)
+ success.should.be.true
+
+
+@mock_ec2
+def test_ami_create_from_missing_instance():
+ conn = boto.connect_ec2('the_key', 'the_secret')
+ conn.create_image.when.called_with("i-abcdefg", "test-ami", "this is a test ami").should.throw(EC2ResponseError)
+
+
+@mock_ec2
+def test_ami_pulls_attributes_from_instance():
+ conn = boto.connect_ec2('the_key', 'the_secret')
+ reservation = conn.run_instances('')
+ instance = reservation.instances[0]
+ instance.modify_attribute("kernel", "test-kernel")
+
+ image_id = instance.create_image("test-ami", "this is a test ami")
+ image = conn.get_image(image_id)
+ image.kernel_id.should.equal('test-kernel')
+
+
+# @mock_ec2
+# def test_ami_attributes():
+# conn = boto.connect_ec2('the_key', 'the_secret')
+# reservation = conn.run_instances('')
+# instance = reservation.instances[0]
+# image = instance.create_image("test-ami", "this is a test ami")
+
+# launch_permission = conn.get_image_attribute(image, 'description')
+# expect(launch_permission.description).should.equal("this is a test ami")