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
This commit is contained in:
AgnesNM 2024-09-13 15:47:44 +03:00
parent 5d3bea0ae3
commit 3789b36fd4
11 changed files with 416 additions and 9 deletions

View File

@ -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,

View File

@ -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<image_id>[^/]+)/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<image_id>[^/]+)/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."""

View File

@ -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
}
});
};

View File

@ -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,

View File

@ -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;
}
}
})();

View File

@ -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

View File

@ -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;
}
}
})();

View File

@ -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

View File

@ -0,0 +1,23 @@
<div ng-controller="horizon.app.core.images.steps.DeactivateController as ctrl">
<div class="content">
<h4 translate>Image Status</h4>
<div class="selected-source clearfix">
<div class="row form-group">
<div class="col-xs-6 col-sm-6">
<div class="form-group">
<label class="control-label required" translate>Status</label>
<div class="form-field">
<div class="btn-group" name="status">
<label class="btn btn-default"
ng-repeat="option in ctrl.imageStatusOptions"
ng-change="ctrl.onStatusChange()"
ng-model="ctrl.image.status"
uib-btn-radio="option.value">{$ ::option.label $}</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -11,5 +11,3 @@
The maximum length for each metadata key and value is 255 characters.
</p>
</div>

View File

@ -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