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