Commit d5cc53f9 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Extend volume API with metadata methods

Extend API of 'volume' app with endpoints for handling metadata of
volumes and snapshots:

Volumes:

* GET /volumes/<vol_id>/metadata: Show volume's metadata
* POST /volumes/<vol_id>/metadata: Update volume's metadata
* PUT /volumes/<vol_id>/metadata: Replace volume's metadata
* DELETE /volumes/<vol_id>/metadata/<meta_key>: Delete volume's metadata
                                                item

Snapshots:

* GET /snapshots/<vol_id>/metadata: Show snapshot's metadata
* POST /snapshots/<vol_id>/metadata: Update snapshot's metadata
* PUT /snapshots/<vol_id>/metadata: Replace snapshot's metadata
* DELETE /snapshots/<vol_id>/metadata/<meta_key>: Delete snapshot's metadata
                                                  item
parent 0aab33af
......@@ -150,10 +150,10 @@ class PlanktonBackend(object):
self._update_metadata(uuid, location, properties, replace=False)
@handle_pithos_backend
def update_properties(self, uuid, properties):
def update_properties(self, uuid, properties, replace=False):
location, _ = self._get_raw_metadata(uuid)
properties = self._prefix_properties(properties)
self._update_metadata(uuid, location, properties, replace=False)
self._update_metadata(uuid, location, properties, replace=replace)
@staticmethod
def _prefix_properties(properties):
......
#from .api_tests import *
from .api import *
from .volumes import *
from .volume_types import *
......@@ -15,14 +15,73 @@
import json
from mock import patch
from snf_django.utils.testing import BaseAPITest
from synnefo.db.models_factory import VolumeTypeFactory
from synnefo.db.models_factory import VolumeFactory, VolumeTypeFactory
from synnefo.lib.services import get_service_path
from synnefo.cyclades_settings import cyclades_services
from synnefo.lib import join_urls
VOLUME_URL = get_service_path(cyclades_services, 'volume',
version='v2.0')
VOLUMES_URL = join_urls(VOLUME_URL, "volumes")
class VolumeMetadataAPITest(BaseAPITest):
def test_volume_metadata(self):
vol = VolumeFactory()
volume_metadata_url = join_urls(join_urls(VOLUMES_URL, str(vol.id)),
"metadata")
# Empty metadata
response = self.get(volume_metadata_url, vol.userid)
self.assertSuccess(response)
metadata = json.loads(response.content)["metadata"]
self.assertEqual(metadata, {})
# Create metadata items
meta1 = {"metadata": {"key1": "val1", "\u2601": "\u2602"}}
response = self.post(volume_metadata_url, vol.userid,
json.dumps(meta1), "json")
self.assertSuccess(response)
response = self.get(volume_metadata_url, vol.userid)
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta1)
# Update existing metadata and add new
meta2 = {"metadata": {"\u2601": "unicode_val_2", "key3": "val3"}}
meta_db = {"metadata": {"key1": "val1",
"\u2601": "unicode_val_2",
"key3": "val3"}}
response = self.post(volume_metadata_url, vol.userid,
json.dumps(meta2), "json")
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta_db)
response = self.get(volume_metadata_url, vol.userid)
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta_db)
# Replace all metadata
meta3 = {"metadata": {"key4": "val4"}}
response = self.put(volume_metadata_url, vol.userid,
json.dumps(meta3), "json")
self.assertSuccess(response)
response = self.get(volume_metadata_url, vol.userid)
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta3)
# Delete metadata key
response = self.delete(join_urls(volume_metadata_url, "key4"),
vol.userid)
self.assertSuccess(response)
response = self.get(volume_metadata_url, vol.userid)
self.assertSuccess(response)
metadata = json.loads(response.content)["metadata"]
self.assertEqual(metadata, {})
VOLUME_TYPES_URL = join_urls(VOLUME_URL, "types/")
......@@ -59,3 +118,82 @@ class VolumeTypeAPITest(BaseAPITest):
self.assertEqual(api_vtype["SNF:disk_template"], "drbd")
self.assertEqual(api_vtype["name"], "drbd1")
self.assertEqual(api_vtype["deleted"], True)
SNAPSHOTS_URL = join_urls(VOLUME_URL, "snapshots")
@patch("synnefo.plankton.backend.PlanktonBackend")
class SnapshotMetadataAPITest(BaseAPITest):
def test_snapshot_metadata(self, mimage):
snap_id = u"1234-4321-1234"
snap_meta_url = join_urls(join_urls(SNAPSHOTS_URL, snap_id),
"metadata")
mimage().__enter__().get_snapshot.return_value = {"properties": {}}
# Empty metadata
response = self.get(snap_meta_url, "user")
self.assertSuccess(response)
metadata = json.loads(response.content)["metadata"]
self.assertEqual(metadata, {})
# Create metadata items
properties = {"key1": "val1", "\u2601": "\u2602"}
meta = {"metadata": properties}
mimage().__enter__().get_snapshot.return_value = \
{"properties": properties}
response = self.post(snap_meta_url, "user",
json.dumps(meta), "json")
self.assertSuccess(response)
mimage().__enter__().update_properties.assert_called_with(
snap_id, properties, replace=False)
response = self.get(snap_meta_url, "user")
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta)
# Update existing metadata and add new
properties = {"\u2601": "unicode_val_2", "key3": "val3"}
db_properties = {"key1": "val1",
"\u2601": "unicode_val_2",
"key3": "val3"}
meta = {"metadata": properties}
meta_db = {"metadata": {"key1": "val1",
"\u2601": "unicode_val_2",
"key3": "val3"}}
mimage().__enter__().get_snapshot.return_value = \
{"properties": db_properties}
response = self.post(snap_meta_url, "user",
json.dumps(meta), "json")
self.assertSuccess(response)
mimage().__enter__().update_properties.assert_called_with(
snap_id, properties, replace=False)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta_db)
response = self.get(snap_meta_url, "user")
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta_db)
properties = {"key4": "val4"}
meta = {"metadata": properties}
mimage().__enter__().get_snapshot.return_value = \
{"properties": properties}
# Replace all metadata
response = self.put(snap_meta_url, "user",
json.dumps(meta), "json")
mimage().__enter__().update_properties.assert_called_with(
snap_id, properties, replace=True)
self.assertSuccess(response)
response = self.get(snap_meta_url, "user")
self.assertSuccess(response)
metadata = json.loads(response.content)
self.assertEqual(metadata, meta)
# Delete metadata key
response = self.delete(join_urls(snap_meta_url, "key4"),
"user")
self.assertSuccess(response)
mimage().__enter__().remove_property.assert_called_with(
snap_id, "key4")
......@@ -39,6 +39,24 @@ def volume_item_demux(request, volume_id):
return HttpResponseNotAllowed(["GET", "PUT", "DELETE"])
def volume_metadata_demux(request, volume_id):
if request.method == 'GET':
return views.list_volume_metadata(request, volume_id)
elif request.method == 'POST':
return views.update_volume_metadata(request, volume_id, reset=False)
elif request.method == 'PUT':
return views.update_volume_metadata(request, volume_id, reset=True)
else:
return HttpResponseNotAllowed(['GET', 'POST', 'PUT'])
def volume_metadata_item_demux(request, volume_id, key):
if request.method == 'DELETE':
return views.delete_volume_metadata_item(request, volume_id, key)
else:
return HttpResponseNotAllowed(['DELETE'])
def snapshot_demux(request):
if request.method == 'GET':
return views.list_snapshots(request)
......@@ -58,14 +76,39 @@ def snapshot_item_demux(request, snapshot_id):
else:
return HttpResponseNotAllowed(["GET", "PUT", "DELETE"])
def snapshot_metadata_demux(request, snapshot_id):
if request.method == 'GET':
return views.list_snapshot_metadata(request, snapshot_id)
elif request.method == 'POST':
return views.update_snapshot_metadata(request, snapshot_id,
reset=False)
elif request.method == 'PUT':
return views.update_snapshot_metadata(request, snapshot_id, reset=True)
else:
return HttpResponseNotAllowed(['GET', 'POST', 'PUT'])
def snapshot_metadata_item_demux(request, snapshot_id, key):
if request.method == 'DELETE':
return views.delete_snapshot_metadata_item(request, snapshot_id, key)
else:
return HttpResponseNotAllowed(['DELETE'])
volume_v2_patterns = patterns(
'',
(r'^volumes/?(?:.json)?$', volume_demux),
(r'^volumes/detail(?:.json)?$', views.list_volumes, {'detail': True}),
(r'^volumes/(\d+)(?:.json)?$', volume_item_demux),
(r'^volumes/(\d+)/metadata/?(?:.json)?$', volume_metadata_demux),
(r'^volumes/(\d+)/metadata/(.+)(?:.json)?$', volume_metadata_item_demux),
(r'^snapshots/?(?:.json)?$', snapshot_demux),
(r'^snapshots/detail$', views.list_snapshots, {'detail': True}),
(r'^snapshots/(\d+)(?:.json)?$', snapshot_item_demux),
(r'^snapshots/([\w-]+)(?:.json)?$', snapshot_item_demux),
(r'^snapshots/([\w-]+)/metadata/?(?:.json)?$', snapshot_metadata_demux),
(r'^snapshots/([\w-]+)/metadata/(.+)(?:.json)?$',
snapshot_metadata_item_demux),
(r'^types/?(?:.json)?$', views.list_volume_types),
(r'^types/(\d+)(?:.json)?$', views.get_volume_type),
)
......
......@@ -15,6 +15,7 @@
from itertools import ifilter
from logging import getLogger
from django.db import transaction
from django.http import HttpResponse
from django.utils import simplejson as json
from django.utils.encoding import smart_unicode
......@@ -25,8 +26,9 @@ from snf_django.lib import api
from snf_django.lib.api import faults, utils
from synnefo.volume import volumes, snapshots, util
from synnefo.db.models import Volume, VolumeType
from synnefo.plankton.backend import PlanktonBackend
from synnefo.db.models import Volume, VolumeType, VolumeMetadata
from synnefo.plankton import backend
from synnefo.logic.utils import check_name_length
log = getLogger('synnefo.volume')
......@@ -202,6 +204,61 @@ def update_volume(request, volume_id):
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(http_method="GET", user_required=True, logger=log)
def list_volume_metadata(request, volume_id):
log.debug('list_volume_meta volume_id: %s', volume_id)
volume = util.get_volume(request.user_uniq, volume_id, for_update=False)
metadata = volume.metadata.values_list('key', 'value')
data = json.dumps({"metadata": dict(metadata)})
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(user_required=True, logger=log)
@transaction.commit_on_success
def update_volume_metadata(request, volume_id, reset=False):
req = utils.get_json_body(request)
log.debug('update_volume_meta volume_id: %s, reset: %s request: %s',
volume_id, reset, req)
volume = util.get_volume(request.user_uniq, volume_id, for_update=True)
meta_dict = utils.get_attribute(req, "metadata", required=True,
attr_type=dict)
for key, value in meta_dict.items():
check_name_length(key, VolumeMetadata.KEY_LENGTH,
"Metadata key is too long.")
check_name_length(value, VolumeMetadata.VALUE_LENGTH,
"Metadata value is too long.")
if reset:
volume.metadata.all().delete()
for key, value in meta_dict.items():
volume.metadata.create(key=key, value=value)
else:
for key, value in meta_dict.items():
try:
# Update existing metadata
meta = volume.metadata.get(key=key)
meta.value = value
meta.save()
except VolumeMetadata.DoesNotExist:
# Or create a new one
volume.metadata.create(key=key, value=value)
metadata = volume.metadata.values_list('key', 'value')
data = json.dumps({"metadata": dict(metadata)})
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(http_method="DELETE", user_required=True, logger=log)
@transaction.commit_on_success
def delete_volume_metadata_item(request, volume_id, key):
log.debug('delete_volume_meta_item volume_id: %s, key: %s',
volume_id, key)
volume = util.get_volume(request.user_uniq, volume_id, for_update=False)
try:
volume.metadata.get(key=key).delete()
except VolumeMetadata.DoesNotExist:
raise faults.BadRequest("Metadata key not found")
return HttpResponse(status=200)
def snapshot_to_dict(snapshot, detail=True):
owner = snapshot['owner']
status = snapshot['status']
......@@ -265,8 +322,8 @@ def create_snapshot(request):
def list_snapshots(request, detail=False):
log.debug('list_snapshots detail=%s', detail)
since = utils.isoparse(request.GET.get('changes-since'))
with PlanktonBackend(request.user_uniq) as backend:
snapshots = backend.list_snapshots()
with backend.PlanktonBackend(request.user_uniq) as b:
snapshots = b.list_snapshots()
if since:
updated_since = lambda snap:\
date_parse(snap["updated_at"]) >= since
......@@ -325,6 +382,44 @@ def update_snapshot(request, snapshot_id):
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(http_method="GET", user_required=True, logger=log)
def list_snapshot_metadata(request, snapshot_id):
log.debug('list_snapshot_meta snapshot_id: %s', snapshot_id)
snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
metadata = snapshot["properties"]
data = json.dumps({"metadata": dict(metadata)})
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(user_required=True, logger=log)
@transaction.commit_on_success
def update_snapshot_metadata(request, snapshot_id, reset=False):
req = utils.get_json_body(request)
log.debug('update_snapshot_meta snapshot_id: %s, reset: %s request: %s',
snapshot_id, reset, req)
snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
meta_dict = utils.get_attribute(req, "metadata", required=True,
attr_type=dict)
with backend.PlanktonBackend(request.user_uniq) as b:
b.update_properties(snapshot_id, meta_dict, replace=reset)
snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
metadata = snapshot["properties"]
data = json.dumps({"metadata": dict(metadata)})
return HttpResponse(data, content_type="application/json", status=200)
@api.api_method(http_method="DELETE", user_required=True, logger=log)
@transaction.commit_on_success
def delete_snapshot_metadata_item(request, snapshot_id, key):
log.debug('delete_snapshot_meta_item snapshot_id: %s, key: %s',
snapshot_id, key)
snapshot = util.get_snapshot(request.user_uniq, snapshot_id)
if key in snapshot["properties"]:
with backend.PlanktonBackend(request.user_uniq) as b:
b.remove_property(snapshot_id, key)
return HttpResponse(status=200)
def volume_type_to_dict(volume_type):
vtype_info = {
"id": volume_type.id,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment