From 351aca3c6807c213974ecca469e8013b643e0222 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 23 Feb 2013 14:22:09 -0500 Subject: [PATCH] clean up instance attribute modification and add base AMI stuff --- moto/ec2/models.py | 112 +++++++++++++++++++++++--------- moto/ec2/responses/__init__.py | 4 +- moto/ec2/responses/amis.py | 84 ++++++++++++++++++++++-- moto/ec2/responses/instances.py | 8 +-- moto/ec2/utils.py | 4 ++ tests/test_ec2/test_amis.py | 44 ++++++++++++- 6 files changed, 210 insertions(+), 46 deletions(-) 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 }} + +""" + +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")