Repository: libcloud Updated Branches: refs/heads/trunk 9f4d0a263 -> a5731abb0
Added Import Snapshot and Describe Import Snapshot to EC2 compute driver Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/0e73f775 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/0e73f775 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/0e73f775 Branch: refs/heads/trunk Commit: 0e73f7758caa6ca11b979c286c314e8b7c91f821 Parents: c62d7c9 Author: Nirzari Iyer <niyer@localhost.localdomain> Authored: Tue Feb 14 14:36:41 2017 -0500 Committer: nirzari <ni...@redhat.com> Committed: Sun Apr 9 22:46:39 2017 -0400 ---------------------------------------------------------------------- libcloud/compute/drivers/ec2.py | 228 +++++++++++++++++++ .../ec2/describe_import_snapshot_tasks.xml | 18 ++ .../describe_import_snapshot_tasks_active.xml | 17 ++ .../compute/fixtures/ec2/import_snapshot.xml | 16 ++ libcloud/test/compute/test_ec2.py | 41 ++++ 5 files changed, 320 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/0e73f775/libcloud/compute/drivers/ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py index 466b802..461248c 100644 --- a/libcloud/compute/drivers/ec2.py +++ b/libcloud/compute/drivers/ec2.py @@ -22,6 +22,7 @@ import sys import base64 import copy import warnings +import time try: from lxml import etree as ET @@ -67,6 +68,7 @@ __all__ = [ 'EC2NodeLocation', 'EC2ReservedNode', 'EC2SecurityGroup', + 'EC2ImportSnapshotTask', 'EC2PlacementGroup', 'EC2Network', 'EC2NetworkSubnet', @@ -2872,6 +2874,22 @@ class EC2SecurityGroup(object): % (self.id, self.name)) +class EC2ImportSnapshotTask(object): + """ + Represents information about a describe_import_snapshot_task. + + Note: This class is EC2 specific. + """ + + def __init__(self, status, snapshotId): + self.status = status + self.snapshotId = snapshotId + + def __repr__(self): + return (('<EC2SecurityGroup: status=%s, snapshotId=%s') + % (self.status, self.snapshotId)) + + class EC2PlacementGroup(object): """ Represents information about a Placement Grous @@ -3906,6 +3924,138 @@ class BaseEC2NodeDriver(NodeDriver): response = self.connection.request(self.path, params=params).object return self._get_boolean(response) + def ex_import_snapshot(self, client_data=None, + client_token=None, description=None, + disk_container=None, dry_run=None, role_name=None): + """ + Imports a disk into an EBS snapshot. More information can be found + at https://goo.gl/sbXkYA. + + :param client_data: Describes the client specific data (optional) + :type client_data: ``dict`` + + :param client_token: The token to enable idempotency for VM + import requests.(optional) + :type client_token: ``str`` + + :param description: The description string for the + import snapshot task.(optional) + :type description: ``str`` + + :param disk_container:The disk container object for the + import snapshot request. + :type disk_container:``dict`` + + :param dry_run: Checks whether you have the permission for + the action, without actually making the request, + and provides an error response.(optional) + :type dry_run: ``bool`` + + :param role_name: The name of the role to use when not using the + default role, 'vmimport'.(optional) + :type role_name: ``str`` + + :rtype: :class: ``VolumeSnapshot`` + """ + + params = {'Action': 'ImportSnapshot'} + + if client_data is not None: + params.update(self._get_client_date_params(client_data)) + + if client_token is not None: + params['ClientToken'] = client_token + + if description is not None: + params['Description'] = description + + if disk_container is not None: + params.update(self._get_disk_container_params(disk_container)) + + if dry_run is not None: + params['DryRun'] = dry_run + + if role_name is not None: + params['RoleName'] = role_name + + importSnapshot = self.connection.request(self.path, + params=params).object + + importTaskId = findtext(element=importSnapshot, + xpath='importTaskId', + namespace=NAMESPACE) + + volumeSnapshot = self._wait_for_import_snapshot_completion( + import_task_id=importTaskId, timeout=1800, interval=15) + + return volumeSnapshot + + def _wait_for_import_snapshot_completion(self, + import_task_id, + timeout=1800, + interval=15): + """ + It waits for import snapshot to be completed + + :param import_task_id: Import task Id for the + current Import Snapshot Task + :type import_task_id: ``str`` + + :param timeout: Timeout value for snapshot generation + :type timeout: ``float`` + + :param interval: Time interval for repetative describe + import snapshot tasks requests + :type interval: ``float`` + + :rtype: :class:``VolumeSnapshot`` + """ + start_time = time.time() + snapshotId = None + while snapshotId is None: + if (time.time() - start_time >= timeout): + raise Exception('Timeout while waiting ' + 'for import task Id %s' + % import_task_id) + res = self.ex_describe_import_snapshot_tasks(import_task_id) + snapshotId = res.snapshotId + + if snapshotId is None: + time.sleep(interval) + + volumeSnapshot = VolumeSnapshot(snapshotId, driver=self) + return volumeSnapshot + + def ex_describe_import_snapshot_tasks(self, import_task_id, dry_run=None): + """ + Describes your import snapshot tasks. More information can be found + at https://goo.gl/CI0MdS. + + :param import_task_id: Import task Id for the current + Import Snapshot Task + :type import_task_id: ``str`` + + :param dry_run: Checks whether you have the permission for + the action, without actually making the request, + and provides an error response.(optional) + :type dry_run: ``bool`` + + :rtype: :class:``DescribeImportSnapshotTasks Object`` + + """ + params = {'Action': 'DescribeImportSnapshotTasks'} + + if dry_run is not None: + params['DryRun'] = dry_run + + # This can be extended for multiple import snapshot tasks + params['ImportTaskId.1'] = import_task_id + + res = self._to_import_snapshot_task( + self.connection.request(self.path, params=params).object + ) + return res + def ex_list_placement_groups(self, names=None): """ A list of placement groups. @@ -6040,6 +6190,19 @@ class BaseEC2NodeDriver(NodeDriver): state=state, name=name) + def _to_import_snapshot_task(self, element): + status = findtext(element=element, xpath='importSnapshotTaskSet/item/' + 'snapshotTaskDetail/status', namespace=NAMESPACE) + + if status != 'completed': + snapshotId = None + else: + xpath = 'importSnapshotTaskSet/item/snapshotTaskDetail/snapshotId' + snapshotId = findtext(element=element, xpath=xpath, + namespace=NAMESPACE) + + return EC2ImportSnapshotTask(status, snapshotId=snapshotId) + def _to_key_pairs(self, elems): key_pairs = [self._to_key_pair(elem=elem) for elem in elems] return key_pairs @@ -6696,6 +6859,71 @@ class BaseEC2NodeDriver(NodeDriver): % (idx, k, key)] = str(value) return params + def _get_disk_container_params(self, disk_container): + """ + Return a list of dictionaries with query parameters for + a valid disk container. + + :param disk_container: List of dictionaries with + disk_container details + :type disk_container: ``list`` or ``dict`` + + :return: Dictionary representation of the disk_container + :rtype: ``dict`` + """ + + if not isinstance(disk_container, (list, tuple)): + raise AttributeError('disk_container not list or tuple') + + params = {} + + for idx, content in enumerate(disk_container): + idx += 1 # We want 1-based indexes + if not isinstance(content, dict): + raise AttributeError( + 'content %s in disk_container not a dict' % content) + + for k, v in content.items(): + if not isinstance(v, dict): + params['DiskContainer.%s' % (k)] = str(v) + + else: + for key, value in v.items(): + params['DiskContainer.%s.%s' + % (k, key)] = str(value) + + return params + + def _get_client_data_params(self, client_data): + """ + Return a dictionary with query parameters for + a valid client data. + + :param client_data: List of dictionaries with the disk + upload details + :type client_data: ``dict`` + + :return: Dictionary representation of the client data + :rtype: ``dict`` + """ + + if not isinstance(client_data, (list, tuple)): + raise AttributeError('client_data not list or tuple') + + params = {} + + for idx, content in enumerate(client_data): + idx += 1 # We want 1-based indexes + if not isinstance(content, dict): + raise AttributeError( + 'content %s in client_data' + 'not a dict' % content) + + for k, v in content.items(): + params['ClientData.%s' % (k)] = str(v) + + return params + def _get_common_security_group_params(self, group_id, protocol, from_port, to_port, cidr_ips, group_pairs): http://git-wip-us.apache.org/repos/asf/libcloud/blob/0e73f775/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks.xml b/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks.xml new file mode 100644 index 0000000..86ae679 --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks.xml @@ -0,0 +1,18 @@ +<DescribeImportSnapshotTasksResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/"> + <requestId>a8c44ac4-8e63-46e9-b8b2-8c72ce3d9b18</requestId> + <importSnapshotTaskSet> + <item> + <importTaskId>import-snap-fh7y6i6w</importTaskId> + <snapshotTaskDetail> + <snapshotId>snap-0ea83e8a87e138f39</snapshotId> + <format>RAW</format> + <diskImageSize>1.073741824E10</diskImageSize> + <userBucket> + <s3Bucket>dummy-bucket</s3Bucket> + <s3Key>dummy-key</s3Key> + </userBucket> + <status>completed</status> + </snapshotTaskDetail> + </item> + </importSnapshotTaskSet> +</DescribeImportSnapshotTasksResponse> http://git-wip-us.apache.org/repos/asf/libcloud/blob/0e73f775/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks_active.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks_active.xml b/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks_active.xml new file mode 100644 index 0000000..edb00cd --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/describe_import_snapshot_tasks_active.xml @@ -0,0 +1,17 @@ +<DescribeImportSnapshotTasksResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/"> + <requestId>a8c44ac4-8e63-46e9-b8b2-8c72ce3d9b18</requestId> + <importSnapshotTaskSet> + <item> + <importTaskId>import-snap-fh7y6i6w</importTaskId> + <snapshotTaskDetail> + <format>RAW</format> + <diskImageSize>1.073741824E10</diskImageSize> + <userBucket> + <s3Bucket>dummy-bucket</s3Bucket> + <s3Key>dummy-key</s3Key> + </userBucket> + <status>active</status> + </snapshotTaskDetail> + </item> + </importSnapshotTaskSet> +</DescribeImportSnapshotTasksResponse> http://git-wip-us.apache.org/repos/asf/libcloud/blob/0e73f775/libcloud/test/compute/fixtures/ec2/import_snapshot.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/ec2/import_snapshot.xml b/libcloud/test/compute/fixtures/ec2/import_snapshot.xml new file mode 100644 index 0000000..e2d51de --- /dev/null +++ b/libcloud/test/compute/fixtures/ec2/import_snapshot.xml @@ -0,0 +1,16 @@ +<ImportSnapshotResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/"> + <requestId>0d490bf6-19cf-4456-9c71-31d097faf46d</requestId> + <importTaskId>import-snap-fgsddbhv</importTaskId> + <snapshotTaskDetail> + <format>RAW</format> + <progress>3</progress> + <diskImageSize>0.0</diskImageSize> + <statusMessage>pending</statusMessage> + <userBucket> + <s3Bucket>dummy-bucket</s3Bucket> + <s3Key>dummy-key</s3Key> + </userBucket> + <status>active</status> + </snapshotTaskDetail> +</ImportSnapshotResponse> + http://git-wip-us.apache.org/repos/asf/libcloud/blob/0e73f775/libcloud/test/compute/test_ec2.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_ec2.py b/libcloud/test/compute/test_ec2.py index 7702d82..6b942e3 100644 --- a/libcloud/test/compute/test_ec2.py +++ b/libcloud/test/compute/test_ec2.py @@ -562,6 +562,35 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): ena_support=True) self.assertEqual(image.id, 'ami-57c2fb3e') + def test_ex_import_snapshot(self): + disk_container = [{'Description': 'Dummy import snapshot task', + 'Format': 'raw', + 'UserBucket': {'S3Bucket': 'dummy-bucket', 'S3Key': 'dummy-key'}}] + + snap = self.driver.ex_import_snapshot(disk_container=disk_container) + self.assertEqual(snap.id, 'snap-0ea83e8a87e138f39') + + def test_wait_for_import_snapshot_completion(self): + snap = self.driver._wait_for_import_snapshot_completion( + import_task_id='import-snap-fhdysyq6') + self.assertEqual(snap.id, 'snap-0ea83e8a87e138f39') + + def test_timeout_wait_for_import_snapshot_completion(self): + import_task_id = 'import-snap-fhdysyq6' + EC2MockHttp.type = 'timeout' + with self.assertRaises(Exception) as context: + self.driver._wait_for_import_snapshot_completion( + import_task_id=import_task_id, timeout=0.01, interval=0.001) + self.assertEqual('Timeout while waiting for import task Id %s' + % import_task_id, str(context.exception)) + + def test_ex_describe_import_snapshot_tasks(self): + snap = self.driver.ex_describe_import_snapshot_tasks( + import_task_id='import-snap-fh7y6i6w<') + + self.assertEqual(snap.snapshotId, 'snap-0ea83e8a87e138f39') + self.assertEqual(snap.status, 'completed') + def test_ex_list_availability_zones(self): availability_zones = self.driver.ex_list_availability_zones() availability_zone = availability_zones[0] @@ -1264,6 +1293,18 @@ class EC2MockHttp(MockHttpTestCase): body = self.fixtures.load('register_image.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _ImportSnapshot(self, method, url, body, headers): + body = self.fixtures.load('import_snapshot.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DescribeImportSnapshotTasks(self, method, url, body, headers): + body = self.fixtures.load('describe_import_snapshot_tasks.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _timeout_DescribeImportSnapshotTasks(self, method, url, body, headers): + body = self.fixtures.load('describe_import_snapshot_tasks_active.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _ex_imageids_DescribeImages(self, method, url, body, headers): body = self.fixtures.load('describe_images_ex_imageids.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK])