Merge branch 'snapshot_support', closes #54
diff --git a/ec2stack/controllers/default.py b/ec2stack/controllers/default.py
index d0b05bd..4371c96 100644
--- a/ec2stack/controllers/default.py
+++ b/ec2stack/controllers/default.py
@@ -8,7 +8,7 @@
from ec2stack.core import Ec2stackError
from ec2stack.services import USERS
from ec2stack.providers.cloudstack import images, instances, keypairs, \
- passwords, security_groups, zones, volumes, tags, vpcs
+ passwords, security_groups, zones, volumes, tags, vpcs, snapshots
DEFAULT = Blueprint('default', __name__)
@@ -45,11 +45,13 @@
security_groups.authenticate_security_group_ingress,
'CreateKeyPair': keypairs.create_keypair,
'CreateSecurityGroup': security_groups.create_security_group,
+ 'CreateSnapshot': snapshots.create_snapshot,
'CreateTags': tags.create_tags,
'CreateVolume': volumes.create_volume,
'CreateVpc': vpcs.create_vpc,
'DeleteKeyPair': keypairs.delete_keypair,
'DeleteSecurityGroup': security_groups.delete_security_group,
+ 'DeleteSnapshot': snapshots.delete_snapshot,
'DeleteTags': tags.delete_tags,
'DeleteVolume': volumes.delete_volume,
'DeleteVpc': vpcs.delete_vpc,
@@ -60,6 +62,7 @@
'DescribeInstances': instances.describe_instances,
'DescribeKeyPairs': keypairs.describe_keypairs,
'DescribeSecurityGroups': security_groups.describe_security_groups,
+ 'DescribeSnapshots': snapshots.describe_snapshots,
'DescribeTags': tags.describe_tags,
'DescribeVolumes': volumes.describe_volumes,
'DescribeVpcs': vpcs.describe_vpcs,
diff --git a/ec2stack/providers/cloudstack/snapshots.py b/ec2stack/providers/cloudstack/snapshots.py
new file mode 100644
index 0000000..6542d84
--- /dev/null
+++ b/ec2stack/providers/cloudstack/snapshots.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+"""This module contains functions for handling requests in relation to snapshots.
+"""
+
+from ec2stack import errors
+from ec2stack import helpers
+from ec2stack.providers import cloudstack
+from ec2stack.providers.cloudstack import requester
+
+
+@helpers.authentication_required
+def create_snapshot():
+ """
+ Create a snapshot.
+
+ @return: Response.
+ """
+ helpers.require_parameters(['VolumeId'])
+ response = _create_snapshot_request()
+ return _create_snapshot_response(response)
+
+
+def _create_snapshot_request():
+ """
+ Request to create a snapshot.
+
+ @return: Response.
+ """
+ args = {'command': 'createSnapshot', 'volumeid': helpers.get('VolumeId')}
+
+ response = requester.make_request_async(args)
+
+ return response
+
+
+def _create_snapshot_response(response):
+ """
+ Generates a response for create snapshot request.
+
+ @param response: Response from Cloudstack.
+ @return: Response.
+ """
+ if 'errortext' in response:
+ if 'Invalid parameter volumeid' in response['errortext']:
+ errors.invalid_volume_id()
+ else:
+ response = response['snapshot']
+ return {
+ 'template_name_or_list': 'create_snapshot.xml',
+ 'response_type': 'CreateSnapshotResponse',
+ 'response': response
+ }
+
+
+@helpers.authentication_required
+def delete_snapshot():
+ """
+ Delete a snapshot.
+
+ @return: Response.
+ """
+ helpers.require_parameters(['SnapshotId'])
+ response = _delete_snapshot_request()
+ return _delete_snapshot_response(response)
+
+
+def _delete_snapshot_request():
+ """
+ Request to delete a snapshot.
+
+ @return: Response.
+ """
+ args = {'command': 'deleteSnapshot', 'id': helpers.get('SnapshotId')}
+
+ response = requester.make_request_async(args)
+
+ return response
+
+
+def _delete_snapshot_response(response):
+ """
+ Generates a response for delete snapshot request.
+
+ @return: Response.
+ """
+ if 'errortext' in response:
+ if 'Invalid parameter id' in response['errortext']:
+ errors.invalid_snapshot_id()
+ return {
+ 'template_name_or_list': 'status.xml',
+ 'response_type': 'DeleteSnapshotResponse',
+ 'return': 'true'
+ }
+
+
+@helpers.authentication_required
+def describe_snapshots():
+ """
+ Describes a specific snapshot or all snapshots.
+
+ @return: Response.
+ """
+ args = {'command': 'listSnapshots'}
+ response = cloudstack.describe_item(
+ args, 'snapshot', errors.invalid_snapshot_id, 'SnapshotId'
+ )
+
+ return _describe_snapshot_response(
+ response
+ )
+
+
+def _describe_snapshot_response(response):
+ """
+ Generates a response for describe snapshot request.
+
+ @param response: Response from Cloudstack.
+ @return: Response.
+ """
+ return {
+ 'template_name_or_list': 'snapshots.xml',
+ 'response_type': 'DescribeSnapshotsResponse',
+ 'response': response
+ }
diff --git a/ec2stack/templates/create_snapshot.xml b/ec2stack/templates/create_snapshot.xml
new file mode 100644
index 0000000..91ca1cb
--- /dev/null
+++ b/ec2stack/templates/create_snapshot.xml
@@ -0,0 +1,8 @@
+{% extends "response.xml" %}
+{% block response_content %}
+ <snapshot>
+ <snapshotId>{{ response.id }}</snapshotId>
+ <volumeId>{{ response.volumeid }}</volumeId>
+ <status>{{ response.state }}</status>
+ </snapshot>
+{% endblock %}
\ No newline at end of file
diff --git a/ec2stack/templates/snapshots.xml b/ec2stack/templates/snapshots.xml
new file mode 100644
index 0000000..d4a3cee
--- /dev/null
+++ b/ec2stack/templates/snapshots.xml
@@ -0,0 +1,12 @@
+{% extends "response.xml" %}
+{% block response_content %}
+ <snapshotSet>
+ {% for snapshot in response.snapshot %}
+ <item>
+ <snapshotId>{{ snapshot.id }}</snapshotId>
+ <volumeId>{{ snapshot.volumeid }}</volumeId>
+ <status>{{ response.state }}</status>
+ </item>
+ {% endfor %}
+ </snapshotSet>
+{% endblock %}
\ No newline at end of file
diff --git a/tests/data/invalid_create_snapshot_volume_not_found.json b/tests/data/invalid_create_snapshot_volume_not_found.json
new file mode 100644
index 0000000..51440fd
--- /dev/null
+++ b/tests/data/invalid_create_snapshot_volume_not_found.json
@@ -0,0 +1,8 @@
+{
+ "createsnapshotresponse": {
+ "errorcode": 431,
+ "uuidlist": [],
+ "cserrorcode": 9999,
+ "errortext": "Unable to execute API command createsnapshot due to invalid value. Invalid parameter volumeid value=120a5425-6092-4700-baaf-600fcbaa10 due to incorrect long value format, or entity does not exist or due to incorrect parameter annotation for the field in api cmd class."
+ }
+}
\ No newline at end of file
diff --git a/tests/data/invalid_create_tag_invalid_id.json b/tests/data/invalid_create_tag_not_found.json
similarity index 100%
rename from tests/data/invalid_create_tag_invalid_id.json
rename to tests/data/invalid_create_tag_not_found.json
diff --git a/tests/data/invalid_delete_snapshot_snapshot_not_found.json b/tests/data/invalid_delete_snapshot_snapshot_not_found.json
new file mode 100644
index 0000000..04ac9cf
--- /dev/null
+++ b/tests/data/invalid_delete_snapshot_snapshot_not_found.json
@@ -0,0 +1,8 @@
+{
+ "deletesnapshotresponse": {
+ "errorcode": 431,
+ "uuidlist": [],
+ "cserrorcode": 9999,
+ "errortext": "Unable to execute API command deletesnapshot due to invalid value. Invalid parameter id value=abdb81fa-f4ac-4ec6-822c-34af7cd461 due to incorrect long value format, or entity does not exist or due to incorrect parameter annotation for the field in api cmd class."
+ }
+}
diff --git a/tests/data/invalid_delete_tag_invalid_tag_id.json b/tests/data/invalid_delete_tag_tag_not_found.json
similarity index 100%
rename from tests/data/invalid_delete_tag_invalid_tag_id.json
rename to tests/data/invalid_delete_tag_tag_not_found.json
diff --git a/tests/data/invalid_describe_image_invalid_image_id.json b/tests/data/invalid_describe_image_image_not_found.json
similarity index 100%
rename from tests/data/invalid_describe_image_invalid_image_id.json
rename to tests/data/invalid_describe_image_image_not_found.json
diff --git a/tests/data/invalid_describe_volume_invalid_instance_id.json b/tests/data/invalid_describe_volume_instance_not_found.json
similarity index 100%
rename from tests/data/invalid_describe_volume_invalid_instance_id.json
rename to tests/data/invalid_describe_volume_instance_not_found.json
diff --git a/tests/data/valid_create_snapshot.json b/tests/data/valid_create_snapshot.json
new file mode 100644
index 0000000..5d711da
--- /dev/null
+++ b/tests/data/valid_create_snapshot.json
@@ -0,0 +1,25 @@
+{
+ "queryasyncjobresultresponse": {
+ "accountid": "f7ca69ba-1cd5-11e4-b589-080027ce083d",
+ "userid": "f7ca8d3c-1cd5-11e4-b589-080027ce083d",
+ "cmd": "org.apache.cloudstack.api.command.user.vmsnapshot.CreateVMSnapshotCmd",
+ "jobstatus": 1,
+ "jobprocstatus": 0,
+ "jobresultcode": 0,
+ "jobresulttype": "object",
+ "jobresult": {
+ "snapshot": {
+ "id": "e0e04b84-6178-49a7-9d2f-a2cc6a82d4e4",
+ "name": "i-2-12-VM_VS_20140805203303",
+ "state": "Ready",
+ "displayname": "testsnapshot",
+ "virtualmachineid": "daa492b4-bd09-46b0-a4ad-142e187ecdbe",
+ "current": true,
+ "type": "Disk",
+ "created": "2014-08-05T21:33:04+0100"
+ }
+ },
+ "created": "2014-08-05T21:33:04+0100",
+ "jobid": "8f89700a-6bd0-4b4b-a3f9-272fff1e8fc7"
+ }
+}
\ No newline at end of file
diff --git a/tests/data/valid_create_volume_response.json b/tests/data/valid_create_volume.json
similarity index 100%
rename from tests/data/valid_create_volume_response.json
rename to tests/data/valid_create_volume.json
diff --git a/tests/data/valid_delete_snapshot.json b/tests/data/valid_delete_snapshot.json
new file mode 100644
index 0000000..681b102
--- /dev/null
+++ b/tests/data/valid_delete_snapshot.json
@@ -0,0 +1,16 @@
+{
+ "queryasyncjobresultresponse": {
+ "accountid": "f7ca69ba-1cd5-11e4-b589-080027ce083d",
+ "userid": "f7ca8d3c-1cd5-11e4-b589-080027ce083d",
+ "cmd": "org.apache.cloudstack.api.command.user.vmsnapshot.DeleteVMSnapshotCmd",
+ "jobstatus": 1,
+ "jobprocstatus": 0,
+ "jobresultcode": 0,
+ "jobresulttype": "object",
+ "jobresult": {
+ "success": true
+ },
+ "created": "2014-08-05T21:37:50+0100",
+ "jobid": "f94a67b2-e389-4790-ae89-f19968d31318"
+ }
+}
\ No newline at end of file
diff --git a/tests/data/valid_describe_snapshot.json b/tests/data/valid_describe_snapshot.json
new file mode 100644
index 0000000..ec18171
--- /dev/null
+++ b/tests/data/valid_describe_snapshot.json
@@ -0,0 +1,16 @@
+{
+ "listvmsnapshotresponse": {
+ "count": 1,
+ "snapshot": [{
+ "id": "examplesnapshot",
+ "name": "i-2-12-VM_VS_20140805203432",
+ "state": "Ready",
+ "displayname": "testsnapshot1",
+ "virtualmachineid": "daa492b4-bd09-46b0-a4ad-142e187ecdbe",
+ "parentName": "testsnapshot",
+ "current": true,
+ "type": "Disk",
+ "created": "2014-08-05T21:34:32+0100"
+ }]
+ }
+}
\ No newline at end of file
diff --git a/tests/data/valid_describe_snapshots.json b/tests/data/valid_describe_snapshots.json
new file mode 100644
index 0000000..a1d5ca1
--- /dev/null
+++ b/tests/data/valid_describe_snapshots.json
@@ -0,0 +1,25 @@
+{
+ "listvmsnapshotresponse": {
+ "count": 2,
+ "snapshot": [{
+ "id": "9f2f067f-3ae7-427d-95d9-3c338dcbd5a3",
+ "name": "i-2-12-VM_VS_20140805203432",
+ "state": "Ready",
+ "displayname": "testsnapshot1",
+ "virtualmachineid": "daa492b4-bd09-46b0-a4ad-142e187ecdbe",
+ "parentName": "testsnapshot",
+ "current": true,
+ "type": "Disk",
+ "created": "2014-08-05T21:34:32+0100"
+ }, {
+ "id": "e0e04b84-6178-49a7-9d2f-a2cc6a82d4e4",
+ "name": "i-2-12-VM_VS_20140805203303",
+ "state": "Ready",
+ "displayname": "testsnapshot",
+ "virtualmachineid": "daa492b4-bd09-46b0-a4ad-142e187ecdbe",
+ "current": false,
+ "type": "Disk",
+ "created": "2014-08-05T21:33:04+0100"
+ }]
+ }
+}
\ No newline at end of file
diff --git a/tests/disk_offering_tests.py b/tests/disk_offering_tests.py
index fcdaca2..3c077ec 100644
--- a/tests/disk_offering_tests.py
+++ b/tests/disk_offering_tests.py
@@ -20,7 +20,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/valid_create_volume_response.json'
+ 'tests/data/valid_create_volume.json'
)
get.return_value.status_code = 200
diff --git a/tests/snapshot_tests.py b/tests/snapshot_tests.py
new file mode 100644
index 0000000..ec9a842
--- /dev/null
+++ b/tests/snapshot_tests.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# encoding: utf-8
+
+from base64 import b64encode
+
+import mock
+import json
+
+from ec2stack.helpers import read_file, generate_signature
+from . import Ec2StackAppTestCase
+
+
+class SnapshotTestCase(Ec2StackAppTestCase):
+
+ def test_create_snapshot(self):
+ data = self.get_example_data()
+ data['Action'] = 'CreateSnapshot'
+ data['VolumeId'] = 'daa492b4-bd09-46b0-a4ad-142e187ecdbe'
+ data['Signature'] = generate_signature(data, 'POST', 'localhost', '/')
+
+ get = mock.Mock()
+ get.return_value.text = read_file(
+ 'tests/data/valid_create_snapshot.json'
+ )
+ get.return_value.status_code = 200
+
+ with mock.patch('requests.get', get):
+ response = self.post(
+ '/',
+ data=data
+ )
+
+ self.assert_ok(response)
+ assert 'CreateSnapshotResponse' in response.data
+
+ def test_describe_snapshots(self):
+ data = self.get_example_data()
+ data['Action'] = 'DescribeSnapshots'
+ data['Signature'] = generate_signature(data, 'POST', 'localhost', '/')
+
+ get = mock.Mock()
+ get.return_value.text = read_file(
+ 'tests/data/valid_describe_snapshots.json'
+ )
+ get.return_value.status_code = 200
+
+ with mock.patch('requests.get', get):
+ response = self.post(
+ '/',
+ data=data
+ )
+
+ self.assert_ok(response)
+ assert 'DescribeSnapshotsResponse' in response.data
+
+ def test_describe_snapshot_by_name(self):
+ data = self.get_example_data()
+ data['Action'] = 'DescribeSnapshots'
+ data['SnapshotId'] = 'examplesnapshot'
+ data['Signature'] = generate_signature(data, 'POST', 'localhost', '/')
+
+ get = mock.Mock()
+ get.return_value.text = read_file(
+ 'tests/data/valid_describe_snapshot.json'
+ )
+ get.return_value.status_code = 200
+
+ with mock.patch('requests.get', get):
+ response = self.post(
+ '/',
+ data=data
+ )
+
+ print response.data
+ self.assert_ok(response)
+ assert 'DescribeSnapshotsResponse' in response.data
+ assert 'examplesnapshot' in response.data
+
+ def test_describe_snapshot_by_name_invalid_name(self):
+ data = self.get_example_data()
+ data['Action'] = 'DescribeSnapshots'
+ data['SnapshotId'] = 'invalidsnapshot'
+ data['Signature'] = generate_signature(data, 'POST', 'localhost', '/')
+
+ get = mock.Mock()
+ get.return_value.text = read_file(
+ 'tests/data/valid_describe_snapshot.json'
+ )
+ get.return_value.status_code = 200
+
+ with mock.patch('requests.get', get):
+ response = self.post(
+ '/',
+ data=data
+ )
+
+ self.assert_bad_request(response)
+ assert 'InvalidSnapshot.NotFound' in response.data
+
+ def test_delete_snapshot(self):
+ data = self.get_example_data()
+ data['Action'] = 'DeleteSnapshot'
+ data['SnapshotId'] = 'Test'
+ data['Signature'] = generate_signature(data, 'POST', 'localhost', '/')
+
+ get = mock.Mock()
+ get.return_value.text = read_file(
+ 'tests/data/valid_delete_snapshot.json'
+ )
+ get.return_value.status_code = 200
+
+ with mock.patch('requests.get', get):
+ response = self.post(
+ '/',
+ data=data
+ )
+
+ self.assert_ok(response)
+ assert 'DeleteSnapshotResponse' in response.data
\ No newline at end of file
diff --git a/tests/tags_tests.py b/tests/tags_tests.py
index 6dc1958..e8e57cc 100644
--- a/tests/tags_tests.py
+++ b/tests/tags_tests.py
@@ -73,7 +73,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/invalid_create_tag_invalid_id.json'
+ 'tests/data/invalid_create_tag_not_found.json'
)
get.return_value.status_code = 200
@@ -145,7 +145,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/invalid_delete_tag_invalid_tag_id.json'
+ 'tests/data/invalid_delete_tag_tag_not_found.json'
)
get.return_value.status_code = 200
diff --git a/tests/volume_tests.py b/tests/volume_tests.py
index 6e6c5c3..7d56402 100644
--- a/tests/volume_tests.py
+++ b/tests/volume_tests.py
@@ -112,7 +112,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/valid_create_volume_response.json'
+ 'tests/data/valid_create_volume.json'
)
get.return_value.status_code = 200
@@ -152,7 +152,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/valid_create_volume_response.json'
+ 'tests/data/valid_create_volume.json'
)
get.return_value.status_code = 200
diff --git a/tests/zones_tests.py b/tests/zones_tests.py
index 5f0d9e5..13f3331 100644
--- a/tests/zones_tests.py
+++ b/tests/zones_tests.py
@@ -104,7 +104,7 @@
get = mock.Mock()
get.return_value.text = read_file(
- 'tests/data/valid_create_volume_response.json'
+ 'tests/data/valid_create_volume.json'
)
get.return_value.status_code = 200