Merge "Deactivate/reactivate images"
This commit is contained in:
commit
2e0e9c2d56
@ -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