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