Begin addition of snapshot support
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..a75a5fb --- /dev/null +++ b/ec2stack/providers/cloudstack/snapshots.py
@@ -0,0 +1,123 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""This module contains functions for handling requests in relation to snapshots. +""" + +import uuid + +from flask import current_app + +from ec2stack import errors +from ec2stack import helpers +from ec2stack.providers import cloudstack +from ec2stack.providers.cloudstack import requester, zones + + +@helpers.authentication_required +def create_snapshot(): + """ + Create a snapshot. + + @return: Response. + """ + response = _create_snapshot_request() + return _create_snapshot_response(response) + + +def _create_snapshot_request(): + """ + Request to create a snapshot. + + @return: Response. + """ + args = {'command': 'createSnapshot'} + + 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. + """ + + 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']) + _delete_snapshot_request() + return _delete_snapshot_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(): + """ + Generates a response for delete snapshot request. + + @return: Response. + """ + 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..b7f6ce0 --- /dev/null +++ b/ec2stack/templates/create_snapshot.xml
@@ -0,0 +1,8 @@ +{% extends "response.xml" %} +{% block response_content %} + <snapshot> + <snapshotId>{{ response.id }}</snapshotId> + <volumeId>{{ snapshot.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/valid_create_snapshot.json b/tests/data/valid_create_snapshot.json new file mode 100644 index 0000000..f8661cd --- /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": { + "vmsnapshot": { + "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_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..b4888a2 --- /dev/null +++ b/tests/data/valid_describe_snapshot.json
@@ -0,0 +1,16 @@ +{ + "listvmsnapshotresponse": { + "count": 1, + "vmSnapshot": [{ + "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..6753646 --- /dev/null +++ b/tests/data/valid_describe_snapshots.json
@@ -0,0 +1,25 @@ +{ + "listvmsnapshotresponse": { + "count": 2, + "vmSnapshot": [{ + "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/snapshot_tests.py b/tests/snapshot_tests.py new file mode 100644 index 0000000..a100252 --- /dev/null +++ b/tests/snapshot_tests.py
@@ -0,0 +1,118 @@ +#!/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'] = 'DescribeVpcs' + 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 + ) + + 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'] = 'DescribeVpcs' + 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 'InvalidSnapshotID.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