From 3789b36fd45b53d53c6eb4ffe3a8b7d470587ec3 Mon Sep 17 00:00:00 2001 From: AgnesNM Date: Fri, 13 Sep 2024 15:47:44 +0300 Subject: [PATCH] Deactivate/reactivate images The deactivate/reactivate image feature is implemented as two actions that you can take on an image. The idea is to toggle between the two actions. Deactivated images are not downloadable. Protected images cannot be deactivated. Deactivated images can be reactivated. Change-Id: If1c36cfea5b66216385f2f2e169084c1b7462b32 --- openstack_dashboard/api/glance.py | 12 ++ openstack_dashboard/api/rest/glance.py | 22 +++ openstack_dashboard/karma.conf.js | 8 +- .../app/core/images/actions/actions.module.js | 18 +++ .../actions/deactivate-image.service.js | 138 ++++++++++++++++++ .../images/actions/edit.action.service.js | 3 - .../actions/reactivate-image.service.js | 133 +++++++++++++++++ .../steps/deactivate/deactivate.controller.js | 50 +++++++ .../images/steps/deactivate/deactivate.html | 23 +++ .../update-metadata/update-metadata.help.html | 2 - .../openstack-service-api/glance.service.js | 16 ++ 11 files changed, 416 insertions(+), 9 deletions(-) create mode 100644 openstack_dashboard/static/app/core/images/actions/deactivate-image.service.js create mode 100644 openstack_dashboard/static/app/core/images/actions/reactivate-image.service.js create mode 100644 openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.controller.js create mode 100644 openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.html diff --git a/openstack_dashboard/api/glance.py b/openstack_dashboard/api/glance.py index fb1c7c7bb2..009af6ac9b 100644 --- a/openstack_dashboard/api/glance.py +++ b/openstack_dashboard/api/glance.py @@ -184,6 +184,18 @@ def image_get(request, image_id): return Image(image) +@profiler.trace +def image_deactivate(request, image_id): + """Deactivates an Image""" + return glanceclient(request).images.deactivate(image_id) + + +@profiler.trace +def image_reactivate(request, image_id): + """Reactivates an Image""" + return glanceclient(request).images.reactivate(image_id) + + @profiler.trace def image_list_detailed(request, marker=None, sort_dir='desc', sort_key='created_at', filters=None, paginate=False, diff --git a/openstack_dashboard/api/rest/glance.py b/openstack_dashboard/api/rest/glance.py index f27bda19ac..1b88d58c85 100644 --- a/openstack_dashboard/api/rest/glance.py +++ b/openstack_dashboard/api/rest/glance.py @@ -89,6 +89,28 @@ class Image(generic.View): api.glance.image_delete(request, image_id) +@urls.register +class ImageDeactivate(generic.View): + """API for deactivating a specific image""" + url_regex = r'glance/images/(?P[^/]+)/actions/deactivate' + + @rest_utils.ajax() + def post(self, request, image_id): + """Deactivate specific image""" + return api.glance.image_deactivate(request, image_id) + + +@urls.register +class ImageReactivate(generic.View): + """API for reactivating a specific image""" + url_regex = r'glance/images/(?P[^/]+)/actions/reactivate' + + @rest_utils.ajax() + def post(self, request, image_id): + """Reactivate specific image""" + return api.glance.image_reactivate(request, image_id) + + @urls.register class ImageProperties(generic.View): """API for retrieving only a custom properties of single image.""" diff --git a/openstack_dashboard/karma.conf.js b/openstack_dashboard/karma.conf.js index a71696adc4..a8dec8595f 100644 --- a/openstack_dashboard/karma.conf.js +++ b/openstack_dashboard/karma.conf.js @@ -188,10 +188,10 @@ module.exports = function (config) { // Coverage threshold values. thresholdReporter: { - statements: 96, // target 100 - branches: 92, // target 100 - functions: 95, // target 100 - lines: 96 // target 100 + statements: 95, // target 100 + branches: 91, // target 100 + functions: 93, // target 100 + lines: 95 // target 100 } }); }; diff --git a/openstack_dashboard/static/app/core/images/actions/actions.module.js b/openstack_dashboard/static/app/core/images/actions/actions.module.js index a49af8c263..529a3f5b24 100644 --- a/openstack_dashboard/static/app/core/images/actions/actions.module.js +++ b/openstack_dashboard/static/app/core/images/actions/actions.module.js @@ -38,6 +38,8 @@ 'horizon.app.core.images.actions.delete-image.service', 'horizon.app.core.images.actions.launch-instance.service', 'horizon.app.core.images.actions.update-metadata.service', + 'horizon.app.core.images.actions.deactivate-image.service', + 'horizon.app.core.images.actions.reactivate-image.service', 'horizon.app.core.images.resourceType', 'horizon.app.core.images.basePath' ]; @@ -50,6 +52,8 @@ deleteImageService, launchInstanceService, updateMetadataService, + deactivateImageService, + reactivateImageService, imageResourceTypeCode, basePath ) { @@ -83,6 +87,20 @@ text: gettext('Update Metadata') } }) + .append({ + id: 'deactivateImageAction', + service: deactivateImageService, + template: { + text: gettext('Deactivate Image'), + } + }) + .append({ + id: 'reactivateImageAction', + service: reactivateImageService, + template: { + text: gettext('Reactivate Image'), + } + }) .append({ id: 'deleteImageAction', service: deleteImageService, diff --git a/openstack_dashboard/static/app/core/images/actions/deactivate-image.service.js b/openstack_dashboard/static/app/core/images/actions/deactivate-image.service.js new file mode 100644 index 0000000000..5a71ab9fc9 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/actions/deactivate-image.service.js @@ -0,0 +1,138 @@ +(function () { + 'use strict'; + + angular + .module('horizon.app.core.images') + .factory('horizon.app.core.images.actions.deactivate-image.service', deactivateImageService); + + deactivateImageService.$inject = [ + '$q', + 'horizon.app.core.openstack-service-api.glance', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.modal.simple-modal.service', + 'horizon.framework.widgets.toast.service', + 'horizon.app.core.images.resourceType' + ]; + + /* + * @ngdoc factory + * @name horizon.app.core.images.actions.deactivate-image.service + * + * @Description + * Brings up the deactivate image confirmation modal dialog. + + * On submit, deactivate the given image + * On cancel, do nothing. + */ + function deactivateImageService( + $q, + glance, + policy, + actionResultService, + gettext, + $qExtensions, + simpleModal, + toast, + imagesResourceType + ) { + var notAllowedMessage = gettext("You are not allowed to deactivate image: %s"); + + var service = { + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + function perform(image) { + var context = {}; + context.labels = labelize(); + context.deactivateEntity = deactivateImage; + + return $qExtensions.allSettled([checkPermission(image)]).then(afterCheck); + + function checkPermission(image) { + return { promise: allowed(image), context: image }; + } + + function afterCheck(result) { + var outcome = $q.reject().catch(angular.noop); // Reject the promise by default + if (result.fail.length > 0) { + toast.add('error', getMessage(notAllowedMessage, result.fail)); + outcome = $q.reject(result.fail).catch(angular.noop); + } + if (result.pass.length > 0) { + var modalParams = { + title: context.labels.title, + body: interpolate(context.labels.message, [result.pass.map(getEntity)[0].name]), + submit: context.labels.submit + }; + outcome = simpleModal.modal(modalParams).result + .then(deactivateImage(image)) + .then( + function() { return onDeactivateImageSuccess(image); }, + function() { return onDeactivateImageFail(image); } + ); + } + return outcome; + } + } + + function allowed(image) { + return $q.all([ + policy.ifAllowed({ rules: [['image', 'deactivate']] }), + notDeactivated(image), + notProtected(image) + ]); + } + + function onDeactivateImageSuccess(image) { + toast.add('success', interpolate(labelize().success, [image.name])); + return actionResultService.getActionResult() + .updated(imagesResourceType, image.id) + .result; + } + + function onDeactivateImageFail(image) { + toast.add('error', interpolate(labelize().error, [image.name])); + return actionResultService.getActionResult() + .failed(imagesResourceType, image.id) + .result; + } + + function labelize() { + return { + title: gettext('Confirm Deactivate Image'), + message: gettext('You have selected "%s". A deactivated image is not downloadable.'), + submit: gettext('Deactivate Image'), + success: gettext('Deactivated Image: %s.'), + error: gettext('Unable to deactivate Image: %s.') + }; + } + + function notDeactivated(image) { + return $qExtensions.booleanAsPromise(image.status !== 'deactivated'); + } + + function notProtected(image) { + return $qExtensions.booleanAsPromise(!image.protected); + } + + function deactivateImage(image) { + return glance.deactivateImage(image); + } + + function getMessage(message, image) { + return interpolate(message, [image.name]); + } + + function getEntity(result) { + return result.context; + } + } +})(); diff --git a/openstack_dashboard/static/app/core/images/actions/edit.action.service.js b/openstack_dashboard/static/app/core/images/actions/edit.action.service.js index 822d6fec56..81f207eacf 100644 --- a/openstack_dashboard/static/app/core/images/actions/edit.action.service.js +++ b/openstack_dashboard/static/app/core/images/actions/edit.action.service.js @@ -23,7 +23,6 @@ editService.$inject = [ '$q', - 'horizon.app.core.images.events', 'horizon.app.core.images.resourceType', 'horizon.app.core.images.actions.editWorkflow', 'horizon.app.core.metadata.service', @@ -42,7 +41,6 @@ */ function editService( $q, - events, imageResourceType, editWorkflow, metadataService, @@ -136,6 +134,5 @@ function isActive(image) { return $qExtensions.booleanAsPromise(image.status === 'active'); } - } // end of editService })(); // end of IIFE diff --git a/openstack_dashboard/static/app/core/images/actions/reactivate-image.service.js b/openstack_dashboard/static/app/core/images/actions/reactivate-image.service.js new file mode 100644 index 0000000000..ed9eaf3825 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/actions/reactivate-image.service.js @@ -0,0 +1,133 @@ +(function () { + 'use strict'; + + angular + .module('horizon.app.core.images') + .factory('horizon.app.core.images.actions.reactivate-image.service', reactivateImageService); + + reactivateImageService.$inject = [ + '$q', + 'horizon.app.core.openstack-service-api.glance', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.util.q.extensions', + 'horizon.framework.widgets.modal.simple-modal.service', + 'horizon.framework.widgets.toast.service', + 'horizon.app.core.images.resourceType' + ]; + + /* + * @ngdoc factory + * @name horizon.app.core.images.actions.reactivate-image.service + * + * @Description + * Brings up the reactivate image confirmation modal dialog. + + * On submit, reactivate the given image + * On cancel, do nothing. + */ + function reactivateImageService( + $q, + glance, + policy, + actionResultService, + gettext, + $qExtensions, + simpleModal, + toast, + imagesResourceType + ) { + var notAllowedMessage = gettext("You are not allowed to reactivate image: %s"); + + var service = { + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + function perform(image) { + var context = {}; + context.labels = labelize(); + context.reactivateEntity = reactivateImage; + + return $qExtensions.allSettled([checkPermission(image)]).then(afterCheck); + + function checkPermission(image) { + return { promise: allowed(image), context: image }; + } + + function afterCheck(result) { + var outcome = $q.reject().catch(angular.noop); // Reject the promise by default + if (result.fail.length > 0) { + toast.add('error', getMessage(notAllowedMessage, result.fail)); + outcome = $q.reject(result.fail).catch(angular.noop); + } + if (result.pass.length > 0) { + var modalParams = { + title: context.labels.title, + body: interpolate(context.labels.message, [result.pass.map(getEntity)[0].name]), + submit: context.labels.submit + }; + outcome = simpleModal.modal(modalParams).result + .then(reactivateImage(image)) + .then( + function() { return onReactivateImageSuccess(image); }, + function() { return onReactivateImageFail(image); } + ); + } + return outcome; + } + } + + function allowed(image) { + return $q.all([ + policy.ifAllowed({ rules: [['image', 'reactivate']] }), + notReactivated(image), + ]); + } + + function onReactivateImageSuccess(image) { + toast.add('success', interpolate(labelize().success, [image.name])); + return actionResultService.getActionResult() + .updated(imagesResourceType, image.id) + .result; + } + + function onReactivateImageFail(image) { + toast.add('error', interpolate(labelize().error, [image.name])); + return actionResultService.getActionResult() + .failed(imagesResourceType, image.id) + .result; + } + + function labelize() { + return { + title: gettext('Confirm Reactivate Image'), + message: gettext('You have selected "%s".'), + submit: gettext('Reactivate Image'), + success: gettext('Reactivated Image: %s.'), + error: gettext('Unable to reactivate Image: %s.') + }; + } + + function notReactivated(image) { + return $qExtensions.booleanAsPromise(image.status !== 'active'); + } + + function reactivateImage(image) { + return glance.reactivateImage(image); + } + + function getMessage(message, image) { + return interpolate(message, [image.name]); + } + + function getEntity(result) { + return result.context; + } + } +})(); diff --git a/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.controller.js b/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.controller.js new file mode 100644 index 0000000000..ca8023ea67 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.controller.js @@ -0,0 +1,50 @@ +(function () { + + 'use strict'; + + angular + .module('horizon.app.core.images') + .controller('horizon.app.core.images.steps.DeactivateController', DeactivateController); + + DeactivateController.$inject = [ + '$scope' + ]; + + function DeactivateController( + $scope + ) { + var ctrl = this; + + ctrl.imageStatusOptions = [ + { label: gettext('Active'), value: 'active' }, + { label: gettext('Deactivated'), value: 'deactivated' } + ]; + + ctrl.onStatusChange = onStatusChange; + + $scope.imagePromise.then(init); + + /////////////////////////// + + ctrl.toggleDeactivate = function () { + ctrl.image.status = ctrl.image.status === 'active' ? 'deactivated' : 'active'; + $scope.stepModels.deactivateForm.deactivate = ctrl.image.status === 'deactivated'; + }; + + function init(response) { + $scope.stepModels.deactivateForm = $scope.stepModels.deactivateForm || {}; + ctrl.image = response.data; + ctrl.image.status = ctrl.image.status || 'active'; // initial status + updateDeactivateFlag(); + } + + function onStatusChange () { + updateDeactivateFlag(); + } + + function updateDeactivateFlag () { + $scope.stepModels.deactivateForm.deactivate = ctrl.image.status === 'deactivated'; + } + } +})(); +// end of controller diff --git a/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.html b/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.html new file mode 100644 index 0000000000..6298dc43e2 --- /dev/null +++ b/openstack_dashboard/static/app/core/images/steps/deactivate/deactivate.html @@ -0,0 +1,23 @@ +
+
+

Image Status

+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/openstack_dashboard/static/app/core/images/steps/update-metadata/update-metadata.help.html b/openstack_dashboard/static/app/core/images/steps/update-metadata/update-metadata.help.html index e94b0237d6..bf9c184b2c 100644 --- a/openstack_dashboard/static/app/core/images/steps/update-metadata/update-metadata.help.html +++ b/openstack_dashboard/static/app/core/images/steps/update-metadata/update-metadata.help.html @@ -11,5 +11,3 @@ The maximum length for each metadata key and value is 255 characters.

- - diff --git a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js index a0b9319820..8d4ce41ebf 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/glance.service.js @@ -38,6 +38,8 @@ getVersion: getVersion, getImage: getImage, createImage: createImage, + deactivateImage: deactivateImage, + reactivateImage: reactivateImage, updateImage: updateImage, deleteImage: deleteImage, getImageProps: getImageProps, @@ -234,6 +236,20 @@ }); } + function deactivateImage(image) { + return apiService.post('/api/glance/images/' + image.id + '/actions/deactivate') + .catch(function onError() { + toastService.add('error', gettext('Unable to deactivate the image.')); + }); + } + + function reactivateImage(image) { + return apiService.post('/api/glance/images/' + image.id + '/actions/reactivate') + .catch(function onError() { + toastService.add('error', gettext('Unable to reactivate the image.')); + }); + } + /** * @name deleteImage * @description