From e23731afcf7eda00e21e6e3a35a748ea6be992a2 Mon Sep 17 00:00:00 2001 From: gigimon Date: Wed, 9 Oct 2013 16:48:11 +0300 Subject: [PATCH] New improvements fo ec2 and cloudstack drivers --- libcloud/compute/base.py | 20 ++- libcloud/compute/drivers/cloudstack.py | 115 ++++++++++----- libcloud/compute/drivers/ec2.py | 164 ++++++++++++++++++++- .../listPortForwardingRules_default.json | 2 +- .../test/compute/fixtures/ec2/create_snapshot.xml | 11 ++ .../test/compute/fixtures/ec2/delete_snapshot.xml | 4 + .../test/compute/fixtures/ec2/deregister_image.xml | 4 + .../compute/fixtures/ec2/describe_snapshots.xml | 39 +++++ .../test/compute/fixtures/ec2/describe_volumes.xml | 23 +++ .../fixtures/ec2/modify_image_attribute.xml | 3 + libcloud/test/compute/test_cloudstack.py | 17 ++- libcloud/test/compute/test_ec2.py | 98 +++++++++++- 12 files changed, 457 insertions(+), 43 deletions(-) create mode 100644 libcloud/test/compute/fixtures/ec2/create_snapshot.xml create mode 100644 libcloud/test/compute/fixtures/ec2/delete_snapshot.xml create mode 100644 libcloud/test/compute/fixtures/ec2/deregister_image.xml create mode 100644 libcloud/test/compute/fixtures/ec2/describe_snapshots.xml create mode 100644 libcloud/test/compute/fixtures/ec2/describe_volumes.xml create mode 100644 libcloud/test/compute/fixtures/ec2/modify_image_attribute.xml diff --git a/libcloud/compute/base.py b/libcloud/compute/base.py index eb9f65d..ac3422b 100644 --- a/libcloud/compute/base.py +++ b/libcloud/compute/base.py @@ -433,8 +433,26 @@ class StorageVolume(UuidMixin): class VolumeSnapshot(object): - def __init__(self, driver): + """ + A base VolumeSnapshot class to derive from. + """ + def __init__(self, id, driver, size=None, extra=None): + """ + Initialize VolumeSnapshot object + + :param id: Snapshot ID + :type id: ``str`` + + :param size: A snapshot size in Gb + :type size: ``int`` + + :param extra: Platform depends parameters for snapshot + :type extra: ``dict`` + """ self.driver = driver + self.id = id + self.size = size + self.extra = extra or {} def destroy(self): """ diff --git a/libcloud/compute/drivers/cloudstack.py b/libcloud/compute/drivers/cloudstack.py index c8bef98..031bdbb 100644 --- a/libcloud/compute/drivers/cloudstack.py +++ b/libcloud/compute/drivers/cloudstack.py @@ -97,13 +97,46 @@ class CloudStackPortForwardingRule(object): "A Port forwarding rule for Source NAT." def __init__(self, node, rule_id, address, protocol, public_port, - private_port): + private_port, public_end_port=None, private_end_port=None): + """ + A Port forwarding rule for Source NAT. + + @note: This is a non-standard extension API, and only works for EC2. + + :param node: Node for rule + :type node: :class:`Node` + + :param rule_id: Rule ID + :type rule_id: ``int`` + + :param address: External IP address + :type address: :class:`CloudStackAddress` + + :param protocol: TCP/IP Protocol (TCP, UDP) + :type protocol: ``str`` + + :param public_port: External port for rule (or started port if used port range) + :type public_port: ``int`` + + :param private_port: Internal node port for rule (or started port if used port range) + :type private_port: ``int`` + + :param public_end_port: End of external port range + :type public_end_port: ``int`` + + :param private_end_port: End of internal port range + :type private_end_port: ``int`` + + :rtype: :class:`CloudStackPortForwardingRule` + """ self.node = node self.id = rule_id self.address = address self.protocol = protocol self.public_port = public_port + self.public_end_port = public_end_port self.private_port = private_port + self.private_end_port = private_end_port def delete(self): self.node.ex_delete_port_forwarding_rule(self) @@ -238,7 +271,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): for addr in addrs.get('publicipaddress', []): if 'virtualmachineid' not in addr: continue - vm_id = addr['virtualmachineid'] + vm_id = str(addr['virtualmachineid']) if vm_id not in public_ips_map: public_ips_map[vm_id] = {} public_ips_map[vm_id][addr['ipaddress']] = addr['id'] @@ -277,7 +310,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): driver=self, extra={'zoneid': vm['zoneid'], 'password': password, - 'key_name': keypair, + 'keyname': keypair, 'securitygroup': securitygroup, 'created': vm['created'] } @@ -291,7 +324,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): for addr in addresses: result = self._sync_request('listIpForwardingRules') for r in result.get('ipforwardingrule', []): - if r['virtualmachineid'] == node.id: + if str(r['virtualmachineid']) == node.id: rule = CloudStackIPForwardingRule(node, r['id'], addr, r['protocol'] @@ -302,17 +335,21 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): node.extra['ip_forwarding_rules'] = rules rules = [] - for addr in addrs: - result = self._sync_request('listPortForwardingRules') - for r in result.get('portforwardingrule', []): - if r['virtualmachineid'] == node.id: - rule = CloudStackPortForwardingRule(node, r['id'], - addr, - r['protocol'] - .upper(), - r['publicport'], - r['privateport']) - rules.append(rule) + public_ips = self.ex_list_public_ips() + result = self._sync_request('listPortForwardingRules') + for r in result.get('portforwardingrule', []): + if str(r['virtualmachineid']) == node.id: + addr = [a for a in public_ips if a.address == r['ipaddress']] + rule = CloudStackPortForwardingRule(node, r['id'], + addr[0], + r['protocol'].upper(), + r['publicport'], + r['privateport'], + r['publicendport'], + r['privateendport'],) + if not addr[0].address in node.public_ips: + node.public_ips.append(addr[0].address) + rules.append(rule) node.extra['port_forwarding_rules'] = rules nodes.append(node) @@ -384,7 +421,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): 'ip_forwarding_rules': [], 'port_forwarding_rules': [], 'password': password, - 'key_name': keypair, + 'keyname': keypair, 'securitygroup': securitygroup, 'created': node['created'] } @@ -587,14 +624,19 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): self._async_request('detachVolume', id=volume.id) return True - def list_volumes(self): + def list_volumes(self, node=None): """ List all volumes + + :type node: :class:`CloudStackNode` :rtype: ``list`` of :class:`StorageVolume` """ list_volumes = [] - volumes = self._sync_request('listVolumes') + if node: + volumes = self._sync_request('listVolumes', virtualmachineid=node.id) + else: + volumes = self._sync_request('listVolumes') for vol in volumes['volume']: list_volumes.append(StorageVolume(id=vol['id'], name=vol['name'], @@ -651,27 +693,28 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): """ rules = [] result = self._sync_request('listPortForwardingRules') - if result == {}: - pass - else: + if not result == {}: + public_ips = self.ex_list_public_ips() nodes = self.list_nodes() for rule in result['portforwardingrule']: node = [n for n in nodes - if n.id == rule['virtualmachineid']] - addr = [a for a in self.ex_list_public_ips() - if a.address == rule['ipaddress']] + if n.id == str(rule['virtualmachineid'])] + addr = [a for a in public_ips if a.address == rule['ipaddress']] rules.append(CloudStackPortForwardingRule (node[0], rule['id'], addr[0], rule['protocol'], rule['publicport'], - rule['privateport'])) + rule['privateport'], + rule['publicendport'], + rule['privateendport'])) return rules def ex_create_port_forwarding_rule(self, address, private_port, - public_port, protocol, node): + public_port, protocol, node, + public_end_port=None, private_end_port=None, openfirewall=True): """ Creates a Port Forwarding Rule, used for Source NAT @@ -698,8 +741,12 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): 'privateport': int(private_port), 'publicport': int(public_port), 'virtualmachineid': node.id, - 'openfirewall': True + 'openfirewall': openfirewall } + if public_end_port: + args['publicendport'] = int(public_end_port) + if private_end_port: + args['privateendport'] = int(private_end_port) result = self._async_request('createPortForwardingRule', **args) rule = CloudStackPortForwardingRule(node, @@ -708,9 +755,11 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): address, protocol, public_port, - private_port) + private_port, + public_end_port, + private_end_port) node.extra['port_forwarding_rules'].append(rule) - node.public_ips.append(address) + node.public_ips.append(address.address) return rule def ex_delete_port_forwarding_rule(self, node, rule): @@ -874,12 +923,12 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): res = self._sync_request('createSSHKeyPair', name=name, **extra_args) return res['keypair'] - def ex_delete_keypair(self, name, **kwargs): + def ex_delete_keypair(self, keypair, **kwargs): """ Deletes an existing SSH KeyPair - :param name: Name of the keypair (required) - :type name: ``str`` + :param keypair: Name of the keypair (required) + :type keypair: ``str`` :param projectid: The project associated with keypair :type projectid: ``str`` @@ -897,7 +946,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): extra_args = kwargs.copy() - res = self._sync_request('deleteSSHKeyPair', name=name, **extra_args) + res = self._sync_request('deleteSSHKeyPair', name=keypair, **extra_args) return res['success'] def ex_import_keypair_from_string(self, name, key_material): diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index aed6a15..68ff391 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -31,13 +31,14 @@ from libcloud.utils.py3 import b, basestring from libcloud.utils.xml import fixxpath, findtext, findattr, findall from libcloud.utils.publickey import get_pubkey_ssh2_fingerprint from libcloud.utils.publickey import get_pubkey_comment +from libcloud.utils.iso8601 import parse_date from libcloud.common.aws import AWSBaseResponse, SignedAWSConnection from libcloud.common.types import (InvalidCredsError, MalformedResponseError, LibcloudError) from libcloud.compute.providers import Provider from libcloud.compute.types import NodeState from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize -from libcloud.compute.base import NodeImage, StorageVolume +from libcloud.compute.base import NodeImage, StorageVolume, VolumeSnapshot API_VERSION = '2010-08-31' NAMESPACE = 'http://ec2.amazonaws.com/doc/%s/' % (API_VERSION) @@ -598,11 +599,31 @@ class BaseEC2NodeDriver(NodeDriver): volId = findtext(element=element, xpath='volumeId', namespace=NAMESPACE) size = findtext(element=element, xpath='size', namespace=NAMESPACE) - + state = findtext(element=element, xpath='status', namespace=NAMESPACE) + create_time = findtext(element=element, xpath='createTime', namespace=NAMESPACE) return StorageVolume(id=volId, name=name, size=int(size), - driver=self) + driver=self, + extra={'state': state, + 'device': findtext(element=element, xpath='attachmentSet/item/device', namespace=NAMESPACE), + 'create-time': parse_date(create_time)}) + + def _to_snapshots(self, response): + return [self._to_snapshot(el) for el in response.findall( + fixxpath(xpath='snapshotSet/item', namespace=NAMESPACE)) + ] + + def _to_snapshot(self, element): + snapId = findtext(element=element, xpath='snapshotId', namespace=NAMESPACE) + volId = findtext(element=element, xpath='volumeId', namespace=NAMESPACE) + size = findtext(element=element, xpath='volumeSize', namespace=NAMESPACE) + state = findtext(element=element, xpath='status', namespace=NAMESPACE) + description = findtext(element=element, xpath='description', namespace=NAMESPACE) + return VolumeSnapshot(snapId, size=int(size), driver=self, + extra={'volume_id': volId, + 'description': description, + 'state': state}) def list_nodes(self, ex_node_ids=None): """ @@ -648,21 +669,32 @@ class BaseEC2NodeDriver(NodeDriver): sizes.append(NodeSize(driver=self, **attributes)) return sizes - def list_images(self, location=None, ex_image_ids=None): + def list_images(self, location=None, ex_image_ids=None, ex_owner=None): """ List all images Ex_image_ids parameter is used to filter the list of images that should be returned. Only the images with the corresponding image ids will be returned. + + Ex_owner parameter is used to filter the list of + images that should be returned. Only the images + with the corresponding owner will be returned. + Valid values: amazon|aws-marketplace|self|all|aws id :param ex_image_ids: List of ``NodeImage.id`` :type ex_image_ids: ``list`` of ``str`` + :param ex_owner: Owner name + :type ex_image_ids: ``str`` + :rtype: ``list`` of :class:`NodeImage` """ params = {'Action': 'DescribeImages'} + if ex_owner: + params.update({'Owner.1': owner}) + if ex_image_ids: params.update(self._pathlist('ImageId', ex_image_ids)) @@ -681,6 +713,21 @@ class BaseEC2NodeDriver(NodeDriver): ) return locations + def list_volumes(self, node=None): + params = { + 'Action': 'DescribeVolumes', + } + if node: + params.update({ + 'Filter.1.Name': 'attachment.instance-id', + 'Filter.1.Value': node.id, + }) + response = self.connection.request(self.path, params=params).object + volumes = [self._to_volume(el, '') for el in response.findall( + fixxpath(xpath='volumeSet/item', namespace=NAMESPACE)) + ] + return volumes + def create_volume(self, size, name, location=None, snapshot=None): params = { 'Action': 'CreateVolume', @@ -720,6 +767,71 @@ class BaseEC2NodeDriver(NodeDriver): self.connection.request(self.path, params=params) return True + def create_volume_snapshot(self, volume, name=None): + """ + Create snapshot from volume + + :param volume: Instance of ``StorageVolume`` + :type volume: ``StorageVolume`` + + :param name: Description for snapshot + :type name: ``str`` + + :rtype: :class:`VolumeSnapshot` + """ + params = { + 'Action': 'CreateSnapshot', + 'VolumeId': volume.id, + } + if name: + params.update({ + 'Description': name, + }) + response = self.connection.request(self.path, params=params).object + snapshot = self._to_snapshot(response) + return snapshot + + def list_volume_snapshots(self, snapshot): + return self.list_snapshots(snapshot) + + def list_snapshots(self, snapshot=None, owner=None): + """ + Describe all snapshots + @param snapshot: If this setted, describe only this snapshot id + @param owner: Owner for snapshot: self|amazon|ID + @return: C{list(VolumeSnapshots)} + """ + params = { + 'Action': 'DescribeSnapshots', + } + if snapshot: + params.update({ + 'SnapshotId.1': snapshot.id, + }) + if owner: + params.update({ + 'Owner.1': owner, + }) + response = self.connection.request(self.path, params=params).object + snapshots = self._to_snapshots(response) + return snapshots + + def destroy_volume_snapshot(self, snapshot): + params = { + 'Action': 'DeleteSnapshot', + 'SnapshotId': snapshot.id + } + response = self.connection.request(self.path, params=params).object + return self._get_boolean(response) + + def ex_destroy_image(self, image): + params = { + 'Action': 'DeregisterImage', + 'ImageId': image.id + } + response = self.connection.request(self.path, params=params).object + return self._get_boolean(response) + def ex_create_keypair(self, name): """Creates a new keypair @@ -745,6 +857,25 @@ class BaseEC2NodeDriver(NodeDriver): 'keyFingerprint': key_fingerprint, } + def ex_delete_keypair(self, keypair): + """Destroy a keypair by name + + @note: This is a non-standard extension API, and only works for EC2. + + :param keypair: The name of the keypair to Delete. + :type keypair: ``str`` + + :rtype: ``bool`` + """ + params = { + 'Action': 'DeleteKeyPair', + 'KeyName.1': keypair + } + result = self.connection.request(self.path, params=params).object + element = findtext(element=result, xpath='return', + namespace=NAMESPACE) + return element == 'true' + def ex_import_keypair_from_string(self, name, key_material): """ imports a new public key where the public key is passed in as a string @@ -1290,6 +1421,31 @@ class BaseEC2NodeDriver(NodeDriver): namespace=NAMESPACE) return element == 'true' + def ex_modify_image_attribute(self, image, attributes): + """ + Modify image attributes. + + :param node: Node instance + :type node: :class:`Node` + + :param attributes: Dictionary with node attributes + :type attributes: ``dict`` + + :return: True on success, False otherwise. + :rtype: ``bool`` + """ + attributes = attributes or {} + attributes.update({'ImageId': image.id}) + + params = {'Action': 'ModifyImageAttribute'} + params.update(attributes) + + result = self.connection.request(self.path, + params=params.copy()).object + element = findtext(element=result, xpath='return', + namespace=NAMESPACE) + return element == 'true' + def ex_change_node_size(self, node, new_size): """ Change the node size. diff --git a/libcloud/test/compute/fixtures/cloudstack/listPortForwardingRules_default.json b/libcloud/test/compute/fixtures/cloudstack/listPortForwardingRules_default.json index a65d08d..d6aae0f 100644 --- a/libcloud/test/compute/fixtures/cloudstack/listPortForwardingRules_default.json +++ b/libcloud/test/compute/fixtures/cloudstack/listPortForwardingRules_default.json @@ -1 +1 @@ -{ "listportforwardingrulesresponse" : { "count":1 ,"portforwardingrule" : [ {"id":"bc7ea3ee-a2c3-4b86-a53f-01bdaa1b2e32","privateport":"33","privateendport":"33","protocol":"tcp","publicport":"33","publicendport":"33","virtualmachineid":"2600","virtualmachinename":"testlib","virtualmachinedisplayname":"testlib","ipaddressid":"96dac96f-0b5d-42c1-b5de-8a97f3e34c43","ipaddress":"1.1.1.116","state":"Active","cidrlist":"","tags":[]} ] } } +{ "listportforwardingrulesresponse" : { "count":1 ,"portforwardingrule" : [ {"id":"bc7ea3ee-a2c3-4b86-a53f-01bdaa1b2e32","privateport":"33","privateendport":"34","protocol":"tcp","publicport":"33","publicendport":"34","virtualmachineid":"2600","virtualmachinename":"testlib","virtualmachinedisplayname":"testlib","ipaddressid":"96dac96f-0b5d-42c1-b5de-8a97f3e34c43","ipaddress":"1.1.1.116","state":"Active","cidrlist":"","tags":[]} ] } } diff --git a/libcloud/test/compute/fixtures/ec2/create_snapshot.xml b/libcloud/test/compute/fixtures/ec2/create_snapshot.xml new file mode 100644 index 0000000..c395ded --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/create_snapshot.xml @@ -0,0 +1,11 @@ + + 59dbff89-35bd-4eac-99ed-be587 + snap-a7cb2hd9 + vol-4282672b + pending + 2013-08-15T16:22:30.000Z + 60% + 1836219348 + 10 + Test description + \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/ec2/delete_snapshot.xml b/libcloud/test/compute/fixtures/ec2/delete_snapshot.xml new file mode 100644 index 0000000..ff1e407 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/delete_snapshot.xml @@ -0,0 +1,4 @@ + + 5cd6fa89-35bd-4aac-99ed-na8af7 + true + \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/ec2/deregister_image.xml b/libcloud/test/compute/fixtures/ec2/deregister_image.xml new file mode 100644 index 0000000..8e202ed --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/deregister_image.xml @@ -0,0 +1,4 @@ + + d06f248d-444e-475d-a8f8-1ebb4ac39842 + true + \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/ec2/describe_snapshots.xml b/libcloud/test/compute/fixtures/ec2/describe_snapshots.xml new file mode 100644 index 0000000..3536050 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/describe_snapshots.xml @@ -0,0 +1,39 @@ + + and4xcasi-35bd-4e3c-89ab-cb183 + + + snap-428abd35 + vol-e020df80 + pending + 2013-09-15T15:40:30.000Z + 90% + 1938218230 + 30 + Daily Backup + + + Keyone + DB_Backup + + + + + + + snap-18349159 + vol-b5a2c1v9 + pending + 2013-09-15T16:00:30.000Z + 30% + 1938218230 + 15 + Weekly backup + + + Key2 + db_backup + + + + + \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/ec2/describe_volumes.xml b/libcloud/test/compute/fixtures/ec2/describe_volumes.xml new file mode 100644 index 0000000..704f5ee --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/describe_volumes.xml @@ -0,0 +1,23 @@ + + 766b978a-f574-4c8d-a974-57547a8c304e + + + vol-10ae5e2b + 1 + + us-east-1d + available + 2013-10-09T05:41:37.000Z + + + + vol-v24bfh75 + 11 + + us-east-1c + available + 2013-10-08T19:36:49.000Z + + + + \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/ec2/modify_image_attribute.xml b/libcloud/test/compute/fixtures/ec2/modify_image_attribute.xml new file mode 100644 index 0000000..d4401fa --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/modify_image_attribute.xml @@ -0,0 +1,3 @@ + + true + \ No newline at end of file diff --git a/libcloud/test/compute/test_cloudstack.py b/libcloud/test/compute/test_cloudstack.py index c0a55e7..565fb05 100644 --- a/libcloud/test/compute/test_cloudstack.py +++ b/libcloud/test/compute/test_cloudstack.py @@ -127,7 +127,7 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): size=size, ex_keyname='foobar') self.assertEqual(node.name, 'test') - self.assertEqual(node.extra['key_name'], 'foobar') + self.assertEqual(node.extra['keyname'], 'foobar') def test_list_images_no_images_available(self): CloudStackMockHttp.fixture_tag = 'notemplates' @@ -242,7 +242,7 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): self.assertEqual('test', nodes[0].name) self.assertEqual('2600', nodes[0].id) self.assertEqual([], nodes[0].extra['securitygroup']) - self.assertEqual(None, nodes[0].extra['key_name']) + self.assertEqual(None, nodes[0].extra['keyname']) def test_list_locations(self): location = self.driver.list_locations()[0] @@ -358,25 +358,36 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): node = self.driver.list_nodes()[0] address = self.driver.ex_list_public_ips()[0] private_port = 33 + private_end_port = 34 public_port = 33 + public_end_port = 34 + openfirewall = True protocol = 'TCP' rule = self.driver.ex_create_port_forwarding_rule(address, private_port, public_port, protocol, - node) + node, + public_end_port, + private_end_port, + openfirewall) self.assertEqual(rule.address, address) self.assertEqual(rule.protocol, protocol) self.assertEqual(rule.public_port, public_port) + self.assertEqual(rule.public_end_port, public_end_port) self.assertEqual(rule.private_port, private_port) + self.assertEqual(rule.private_end_port, private_end_port) def test_ex_list_port_forwarding_rules(self): rules = self.driver.ex_list_port_forwarding_rules() self.assertEqual(len(rules), 1) rule = rules[0] + self.assertTrue(rule.node) self.assertEqual(rule.protocol, 'tcp') self.assertEqual(rule.public_port, '33') + self.assertEqual(rule.public_end_port, '34') self.assertEqual(rule.private_port, '33') + self.assertEqual(rule.private_end_port, '34') self.assertEqual(rule.address.address, '1.1.1.116') def test_ex_delete_port_forwarding_rule(self): diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py index c51dc5a..8120505 100644 --- a/libcloud/test/compute/test_ec2.py +++ b/libcloud/test/compute/test_ec2.py @@ -17,6 +17,7 @@ from __future__ import with_statement import os import sys +from datetime import datetime from mock import Mock @@ -37,7 +38,7 @@ from libcloud.compute.drivers.ec2 import REGION_DETAILS from libcloud.compute.drivers.ec2 import ExEC2AvailabilityZone from libcloud.utils.py3 import urlparse from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation -from libcloud.compute.base import StorageVolume +from libcloud.compute.base import StorageVolume, VolumeSnapshot from libcloud.test import MockHttpTestCase, LibcloudTestCase from libcloud.test.compute import TestCaseMixin @@ -326,6 +327,13 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): self.assertEqual(images[0].name, 'ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml') + def ex_destroy_image(self): + images = self.driver.list_images() + image = images[0] + + resp = self.driver.ex_destroy_image(image) + self.assertTrue(resp) + def test_ex_list_availability_zones(self): availability_zones = self.driver.ex_list_availability_zones() availability_zone = availability_zones[0] @@ -356,6 +364,10 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): self.assertEqual(keypair2['keyName'], 'gsg-keypair') self.assertEqual(keypair2['keyFingerprint'], null_fingerprint) + def ex_delete_keypair(self): + resp = self.driver.ex_delete_keypair('testkey') + self.assertTrue(resp) + def test_ex_describe_tags(self): node = Node('i-4382922a', None, None, None, None, self.driver) tags = self.driver.ex_describe_tags(resource=node) @@ -455,12 +467,28 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): result = self.driver.ex_change_node_size(node=node, new_size=size) self.assertTrue(result) + def test_list_volumes(self): + volumes = self.driver.list_volumes() + + self.assertEqual(len(volumes), 2) + + self.assertEqual('vol-10ae5e2b', volumes[0].id) + self.assertEqual(1, volumes[0].size) + self.assertEqual('available', volumes[0].extra['state']) + + self.assertEqual('vol-v24bfh75', volumes[1].id) + self.assertEqual(11, volumes[1].size) + self.assertEqual('available', volumes[1].extra['state']) + + def test_create_volume(self): location = self.driver.list_locations()[0] vol = self.driver.create_volume(10, 'vol', location) self.assertEqual(10, vol.size) self.assertEqual('vol', vol.name) + self.assertEqual('creating', vol.extra['state']) + self.assertTrue(isinstance(vol.extra['create-time'], datetime)) def test_destroy_volume(self): vol = StorageVolume( @@ -488,6 +516,45 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): retValue = self.driver.detach_volume(vol) self.assertTrue(retValue) + def test_create_volume_snapshot(self): + vol = StorageVolume( + id='vol-4282672b', name='test', + size=10, driver=self.driver) + snap = self.driver.create_volume_snapshot(vol, 'Test description') + + self.assertEqual('snap-a7cb2hd9', snap.id) + self.assertEqual(vol.size, snap.size) + self.assertEqual('Test description', snap.extra['description']) + self.assertEqual(vol.id, snap.extra['volume_id']) + self.assertEqual('pending', snap.extra['state']) + + def test_list_snapshots(self): + snaps = self.driver.list_snapshots() + + self.assertEqual(len(snaps), 2) + + self.assertEqual('snap-428abd35', snaps[0].id) + self.assertEqual('vol-e020df80', snaps[0].extra['volume_id']) + self.assertEqual(30, snaps[0].size) + self.assertEqual('Daily Backup', snaps[0].extra['description']) + + self.assertEqual('snap-18349159', snaps[1].id) + self.assertEqual('vol-b5a2c1v9', snaps[1].extra['volume_id']) + self.assertEqual(15, snaps[1].size) + self.assertEqual('Weekly backup', snaps[1].extra['description']) + + def test_destroy_snapshot(self): + snap = VolumeSnapshot(id='snap-428abd35', size=10, driver=self.driver) + resp = snap.destroy() + self.assertTrue(resp) + + def test_ex_modify_image_attribute(self): + images = self.driver.list_images() + image = images[0] + + resp = self.driver.ex_modify_image_attribute(image, {'LaunchPermission.Add.1.Group': 'all'}) + self.assertTrue(resp) + def test_create_node_ex_security_groups(self): EC2MockHttp.type = 'ex_security_groups' @@ -733,6 +800,35 @@ class EC2MockHttp(MockHttpTestCase): body = self.fixtures.load('detach_volume.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _DescribeVolumes(self, method, url, body, headers): + body = self.fixtures.load('describe_volumes.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _CreateSnapshot(self, method, url, body, headers): + body = self.fixtures.load('create_snapshot.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DescribeSnapshots(self, method, url, body, headers): + body = self.fixtures.load('describe_snapshots.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DeleteSnapshot(self, method, url, body, headers): + body = self.fixtures.load('delete_snapshot.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DeregisterImage(self, method, url, body, headers): + body = self.fixtures.load('deregister_image.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DeleteKeypair(self, method, url, body, headers): + body = self.fixtures.load('delete_keypair.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _ModifyImageAttribute(self, method, url, body, headers): + body = self.fixtures.load('modify_image_attribute.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + class EucMockHttp(EC2MockHttp): fixtures = ComputeFileFixtures('ec2') -- 1.8.4