clean up instance attribute modification and add base AMI stuff

This commit is contained in:
Steve Pulec 2013-02-23 14:22:09 -05:00
parent 301c23a499
commit 351aca3c68
6 changed files with 210 additions and 46 deletions

View File

@ -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']
}
# {
# #'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']
# }

View File

@ -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:

View File

@ -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 = """<CreateImageResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imageId>{{ image.id }}</imageId>
</CreateImageResponse>"""
# TODO almost all of these params should actually be templated based on the ec2 image
DESCRIBE_IMAGES_RESPONSE = """<DescribeImagesResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imagesSet>
{% for image in images %}
<item>
<imageId>{{ image.id }}</imageId>
<imageLocation>amazon/getting-started</imageLocation>
<imageState>available</imageState>
<imageOwnerId>111122223333</imageOwnerId>
<isPublic>true</isPublic>
<architecture>i386</architecture>
<imageType>machine</imageType>
<kernelId>{{ image.kernel_id }}</kernelId>
<ramdiskId>ari-1a2b3c4d</ramdiskId>
<imageOwnerAlias>amazon</imageOwnerAlias>
<name>{{ image.name }}</name>
<description>{{ image.description }}</description>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda</rootDeviceName>
<blockDeviceMapping>
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<snapshotId>snap-1a2b3c4d</snapshotId>
<volumeSize>15</volumeSize>
<deleteOnTermination>false</deleteOnTermination>
<volumeType>standard</volumeType>
</ebs>
</item>
</blockDeviceMapping>
<virtualizationType>{{ image.virtualization_type }}</virtualizationType>
<tagSet/>
<hypervisor>xen</hypervisor>
</item>
{% endfor %}
</imagesSet>
</DescribeImagesResponse>"""
DESCRIBE_IMAGE_RESPONSE = """<DescribeImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imageId>{{ image.id }}</imageId>
<{{ key }}>
<value>{{ value }}</value>
</{{key }}>
</DescribeImageAttributeResponse>"""
DEREGISTER_IMAGE_RESPONSE = """<DeregisterImageResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>{{ success }}</return>
</DeregisterImageResponse>"""

View File

@ -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

View File

@ -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():

View File

@ -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('<ami-image-id>')
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('<ami-image-id>')
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('<ami-image-id>')
# 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")