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:
parent
5d3bea0ae3
commit
3789b36fd4
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
})();
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
})();
|
@ -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
|
@ -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>
|
@ -11,5 +11,3 @@
|
||||
The maximum length for each metadata key and value is 255 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user