diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index cfd17386d8..c011b9ed4a 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -355,6 +355,14 @@ def volume_update(request, volume_id, name, description): **vol_data) +def volume_set_metadata(request, volume_id, metadata): + return cinderclient(request).volumes.set_metadata(volume_id, metadata) + + +def volume_delete_metadata(request, volume_id, keys): + return cinderclient(request).volumes.delete_metadata(volume_id, keys) + + def volume_reset_state(request, volume_id, state): return cinderclient(request).volumes.reset_state(volume_id, state) @@ -445,6 +453,16 @@ def volume_snapshot_update(request, snapshot_id, name, description): **snapshot_data) +def volume_snapshot_set_metadata(request, snapshot_id, metadata): + return cinderclient(request).volume_snapshots.set_metadata( + snapshot_id, metadata) + + +def volume_snapshot_delete_metadata(request, snapshot_id, keys): + return cinderclient(request).volume_snapshots.delete_metadata( + snapshot_id, keys) + + def volume_snapshot_reset_state(request, snapshot_id, state): return cinderclient(request).volume_snapshots.reset_state( snapshot_id, state) @@ -792,7 +810,7 @@ def volume_type_extra_set(request, type_id, metadata): def volume_type_extra_delete(request, type_id, keys): vol_type = volume_type_get(request, type_id) - return vol_type.unset_keys([keys]) + return vol_type.unset_keys(keys) def qos_spec_list(request): diff --git a/openstack_dashboard/api/rest/cinder.py b/openstack_dashboard/api/rest/cinder.py index f626807a43..fd1e6a759f 100644 --- a/openstack_dashboard/api/rest/cinder.py +++ b/openstack_dashboard/api/rest/cinder.py @@ -132,6 +132,34 @@ class VolumeTypes(generic.View): return {'items': [api.cinder.VolumeType(u).to_dict() for u in result]} +@urls.register +class VolumeMetadata(generic.View): + """API for volume metadata""" + url_regex = r'cinder/volumes/(?P[^/]+)/metadata$' + + @rest_utils.ajax() + def get(self, request, volume_id): + """Get a specific volume's metadata + + http://localhost/api/cinder/volumes/1/metadata + """ + return api.cinder.volume_get(request, + volume_id).to_dict().get('metadata') + + @rest_utils.ajax() + def patch(self, request, volume_id): + """Update metadata items for specific volume + + http://localhost/api/cinder/volumes/1/metadata + """ + updated = request.DATA['updated'] + removed = request.DATA['removed'] + if updated: + api.cinder.volume_set_metadata(request, volume_id, updated) + if removed: + api.cinder.volume_delete_metadata(request, volume_id, removed) + + @urls.register class VolumeType(generic.View): """API for getting a volume type. @@ -179,6 +207,74 @@ class VolumeSnapshots(generic.View): return {'items': [u.to_dict() for u in result]} +@urls.register +class VolumeSnapshotMetadata(generic.View): + """API for getting snapshots metadata""" + url_regex = r'cinder/volumesnapshots/' \ + r'(?P[^/]+)/metadata$' + + @rest_utils.ajax() + def get(self, request, volume_snapshot_id): + """Get a specific volumes snapshot metadata + + http://localhost/api/cinder/volumesnapshots/1/metadata + """ + result = api.cinder.volume_snapshot_get(request, + volume_snapshot_id).\ + to_dict().get('metadata') + return result + + @rest_utils.ajax() + def patch(self, request, volume_snapshot_id): + """Update metadata for specific volume snapshot + + http://localhost/api/cinder/volumesnapshots/1/metadata + """ + updated = request.DATA['updated'] + removed = request.DATA['removed'] + if updated: + api.cinder.volume_snapshot_set_metadata(request, + volume_snapshot_id, + updated) + if removed: + api.cinder.volume_snapshot_delete_metadata(request, + volume_snapshot_id, + removed) + + +@urls.register +class VolumeTypeMetadata(generic.View): + """API for getting snapshots metadata""" + url_regex = r'cinder/volumetypes/(?P[^/]+)/metadata$' + + @rest_utils.ajax() + def get(self, request, volume_type_id): + """Get a specific volume's metadata + + http://localhost/api/cinder/volumetypes/1/metadata + """ + metadata = api.cinder.volume_type_extra_get(request, volume_type_id) + result = {x.key: x.value for x in metadata} + return result + + @rest_utils.ajax() + def patch(self, request, volume_type_id): + """Update metadata for specific volume + + http://localhost/api/cinder/volumetypes/1/metadata + """ + updated = request.DATA['updated'] + removed = request.DATA['removed'] + if updated: + api.cinder.volume_type_extra_set(request, + volume_type_id, + updated) + if removed: + api.cinder.volume_type_extra_delete(request, + volume_type_id, + removed) + + @urls.register class Extensions(generic.View): """API for cinder extensions. diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py index adebae281b..1a5c5847ba 100644 --- a/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tables.py @@ -72,7 +72,8 @@ class VolumeSnapshotsTable(volumes_tables.VolumesTableBase): table_actions = (snapshots_tables.VolumeSnapshotsFilterAction, snapshots_tables.DeleteVolumeSnapshot,) row_actions = (snapshots_tables.DeleteVolumeSnapshot, - UpdateVolumeSnapshotStatus,) + UpdateVolumeSnapshotStatus, + snapshots_tables.UpdateMetadata) row_class = UpdateRow status_columns = ("status",) columns = ('tenant', 'host', 'name', 'description', 'size', 'status', diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py index 89b9c5b9c2..1b7bafdc8c 100644 --- a/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py @@ -19,7 +19,7 @@ from openstack_dashboard.test import helpers as test from openstack_dashboard.dashboards.admin.volumes.snapshots import forms -INDEX_URL = reverse('horizon:admin:volumes:index') +INDEX_URL = 'horizon:admin:volumes:index' class VolumeSnapshotsViewTests(test.BaseAdminViewTests): @@ -79,7 +79,7 @@ class VolumeSnapshotsViewTests(test.BaseAdminViewTests): self.assertNoFormErrors(res) self.assertMessageCount(error=1) - self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertRedirectsNoFollow(res, reverse(INDEX_URL)) @test.create_stubs({cinder: ('volume_snapshot_get', 'volume_get')}) @@ -101,7 +101,7 @@ class VolumeSnapshotsViewTests(test.BaseAdminViewTests): self.assertNoFormErrors(res) self.assertMessageCount(error=1) - self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertRedirectsNoFollow(res, reverse(INDEX_URL)) def test_get_snapshot_status_choices_without_current(self): current_status = {'status': 'available'} diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py index 9a67a1ba41..20f3529d16 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py @@ -195,6 +195,23 @@ class UpdateRow(tables.Row): return volume_type +class UpdateMetadata(tables.LinkAction): + name = "update_metadata" + verbose_name = _("Update Metadata") + ajax = False + attrs = {"ng-controller": "MetadataModalHelperController as modal"} + + def __init__(self, **kwargs): + kwargs['preempt'] = True + super(UpdateMetadata, self).__init__(**kwargs) + + def get_link_url(self, datum): + obj_id = self.table.get_object_id(datum) + self.attrs['ng-click'] = ( + "modal.openMetadataModal('volume_type', '%s', true)" % obj_id) + return "javascript:void(0);" + + class VolumeTypesTable(tables.DataTable): name = tables.WrappingColumn("name", verbose_name=_("Name"), form_field=forms.CharField(max_length=64)) @@ -233,7 +250,8 @@ class VolumeTypesTable(tables.DataTable): EditVolumeType, UpdateVolumeTypeEncryption, DeleteVolumeTypeEncryption, - DeleteVolumeType,) + DeleteVolumeType, + UpdateMetadata) row_class = UpdateRow diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py b/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py index b28f7d750b..36db46f58c 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/tables.py @@ -111,6 +111,7 @@ class VolumesTable(volumes_tables.VolumesTable): row_actions = (volumes_tables.DeleteVolume, UpdateVolumeStatusAction, UnmanageVolumeAction, - MigrateVolume) + MigrateVolume, + volumes_tables.UpdateMetadata) columns = ('tenant', 'host', 'name', 'size', 'status', 'volume_type', 'attachments', 'bootable', 'encryption',) diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/tables.py b/openstack_dashboard/dashboards/project/volumes/snapshots/tables.py index ebb8dfab5d..ca565c5f2b 100644 --- a/openstack_dashboard/dashboards/project/volumes/snapshots/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/snapshots/tables.py @@ -132,6 +132,24 @@ class CreateVolumeFromSnapshot(tables.LinkAction): return False +class UpdateMetadata(tables.LinkAction): + name = "update_metadata" + verbose_name = _("Update Metadata") + + ajax = False + attrs = {"ng-controller": "MetadataModalHelperController as modal"} + + def __init__(self, **kwargs): + kwargs['preempt'] = True + super(UpdateMetadata, self).__init__(**kwargs) + + def get_link_url(self, datum): + obj_id = self.table.get_object_id(datum) + self.attrs['ng-click'] = ( + "modal.openMetadataModal('volume_snapshot', '%s', true)" % obj_id) + return "javascript:void(0);" + + class UpdateRow(tables.Row): ajax = True @@ -191,7 +209,8 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase): launch_actions = (LaunchSnapshotNG,) + launch_actions row_actions = ((CreateVolumeFromSnapshot,) + launch_actions + - (EditVolumeSnapshot, DeleteVolumeSnapshot)) + (EditVolumeSnapshot, DeleteVolumeSnapshot, + UpdateMetadata)) row_class = UpdateRow status_columns = ("status",) permissions = [( diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py index 09e1d151d9..b02366c3bf 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py @@ -469,6 +469,23 @@ class VolumesFilterAction(tables.FilterAction): if q in volume.name.lower()] +class UpdateMetadata(tables.LinkAction): + name = "update_metadata" + verbose_name = _("Update Metadata") + ajax = False + attrs = {"ng-controller": "MetadataModalHelperController as modal"} + + def __init__(self, **kwargs): + kwargs['preempt'] = True + super(UpdateMetadata, self).__init__(**kwargs) + + def get_link_url(self, datum): + obj_id = self.table.get_object_id(datum) + self.attrs['ng-click'] = ( + "modal.openMetadataModal('volume', '%s', true)" % obj_id) + return "javascript:void(0);" + + class VolumesTable(VolumesTableBase): name = tables.WrappingColumn("name", verbose_name=_("Name"), @@ -504,7 +521,7 @@ class VolumesTable(VolumesTableBase): launch_actions + (EditAttachments, CreateSnapshot, CreateBackup, RetypeVolume, UploadToImage, CreateTransfer, - DeleteTransfer, DeleteVolume)) + DeleteTransfer, DeleteVolume, UpdateMetadata)) class DetachVolume(tables.BatchAction): diff --git a/openstack_dashboard/static/app/core/metadata/metadata.service.js b/openstack_dashboard/static/app/core/metadata/metadata.service.js index 1ba833b704..40d250d071 100644 --- a/openstack_dashboard/static/app/core/metadata/metadata.service.js +++ b/openstack_dashboard/static/app/core/metadata/metadata.service.js @@ -22,7 +22,8 @@ metadataService.$inject = [ 'horizon.app.core.openstack-service-api.nova', - 'horizon.app.core.openstack-service-api.glance' + 'horizon.app.core.openstack-service-api.glance', + 'horizon.app.core.openstack-service-api.cinder' ]; /** @@ -32,7 +33,7 @@ * * Unified acquisition and modification of metadata. */ - function metadataService(nova, glance) { + function metadataService(nova, glance, cinder) { var service = { getMetadata: getMetadata, editMetadata: editMetadata, @@ -52,7 +53,10 @@ aggregate: nova.getAggregateExtraSpecs, flavor: nova.getFlavorExtraSpecs, image: glance.getImageProps, - instance: nova.getInstanceMetadata + instance: nova.getInstanceMetadata, + volume: cinder.getVolumeMetadata, + volume_snapshot: cinder.getVolumeSnapshotMetadata, + volume_type: cinder.getVolumeTypeMetadata }[resource](id); } @@ -69,7 +73,10 @@ aggregate: nova.editAggregateExtraSpecs, flavor: nova.editFlavorExtraSpecs, image: glance.editImageProps, - instance: nova.editInstanceMetadata + instance: nova.editInstanceMetadata, + volume: cinder.editVolumeMetadata, + volume_snapshot: cinder.editVolumeSnapshotMetadata, + volume_type: cinder.editVolumeTypeMetadata }[resource](id, updated, removed); } @@ -86,7 +93,10 @@ aggregate: 'OS::Nova::Aggregate', flavor: 'OS::Nova::Flavor', image: 'OS::Glance::Image', - instance: 'OS::Nova::Server' + instance: 'OS::Nova::Server', + volume: 'OS::Cinder::Volume', + volume_snapshot: 'OS::Cinder::Snapshot', + volume_type: 'OS:Cinder::VolumeType' }[resource] }; if (propertiesTarget) { diff --git a/openstack_dashboard/static/app/core/metadata/metadata.service.spec.js b/openstack_dashboard/static/app/core/metadata/metadata.service.spec.js index ea33882af1..f65bd6ea25 100644 --- a/openstack_dashboard/static/app/core/metadata/metadata.service.spec.js +++ b/openstack_dashboard/static/app/core/metadata/metadata.service.spec.js @@ -31,10 +31,17 @@ editImageProps: function() {}, getNamespaces: function() {}}; + var cinder = {getVolumeMetadata:function() {}, + getVolumeSnapshotMetadata:function() {}, + getVolumeTypeMetadata:function() {}, + editVolumeMetadata: function() {}, + editVolumeSnapshotMetadata: function() {}}; + beforeEach(function() { module(function($provide) { $provide.value('horizon.app.core.openstack-service-api.nova', nova); $provide.value('horizon.app.core.openstack-service-api.glance', glance); + $provide.value('horizon.app.core.openstack-service-api.cinder', cinder); }); }); @@ -97,6 +104,18 @@ expect(glance.editImageProps).toHaveBeenCalledWith('1', 'updated', ['removed']); }); + it('should edit volume metadata', function() { + spyOn(cinder, 'editVolumeMetadata'); + metadataService.editMetadata('volume', '1', 'updated', ['removed']); + expect(cinder.editVolumeMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + + it('should edit volume snapshot metadata', function() { + spyOn(cinder, 'editVolumeSnapshotMetadata'); + metadataService.editMetadata('volume_snapshot', '1', 'updated', ['removed']); + expect(cinder.editVolumeSnapshotMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); + }); + it('should get image namespace', function() { spyOn(glance, 'getNamespaces'); metadataService.getNamespaces('image'); @@ -111,6 +130,13 @@ expect(actual).toBe(expected); }); + it('should get volume metadata', function() { + var expected = 'volume metadata'; + spyOn(cinder, 'getVolumeMetadata').and.returnValue(expected); + var actual = metadataService.getMetadata('volume', '1'); + expect(actual).toBe(expected); + }); + it('should edit instance metadata', function() { spyOn(nova, 'editInstanceMetadata'); metadataService.editMetadata('instance', '1', 'updated', ['removed']); diff --git a/openstack_dashboard/static/app/core/metadata/modal/modal.html b/openstack_dashboard/static/app/core/metadata/modal/modal.html index 8387da7cd9..b6eccf9c98 100644 --- a/openstack_dashboard/static/app/core/metadata/modal/modal.html +++ b/openstack_dashboard/static/app/core/metadata/modal/modal.html @@ -7,6 +7,9 @@ Update Flavor Metadata Update Image Metadata Update Instance Metadata + Update Volume Metadata + Update Volume Snapshot Metadata + Update Volume Type Metadata