Merge "[NG] Support local file upload in Create Image workflow"
This commit is contained in:
commit
72ad1e3fd5
|
@ -57,6 +57,7 @@
|
|||
// if user is not authorized, log user out
|
||||
// this can happen when session expires
|
||||
$httpProvider.interceptors.push(redirect);
|
||||
$httpProvider.interceptors.push(stripAjaxHeaderForCORS);
|
||||
|
||||
redirect.$inject = ['$q'];
|
||||
|
||||
|
@ -71,6 +72,25 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
stripAjaxHeaderForCORS.$inject = [];
|
||||
// Standard CORS middleware used in OpenStack services doesn't expect
|
||||
// X-Requested-With header to be set for requests and rejects requests
|
||||
// which have it. Since there is no reason to treat Horizon specially when
|
||||
// dealing handling CORS requests, it's better for Horizon not to set this
|
||||
// header when it sends CORS requests. Detect CORS request by presence of
|
||||
// X-Auth-Token headers which normally should be provided because of
|
||||
// Keystone authentication.
|
||||
function stripAjaxHeaderForCORS() {
|
||||
return {
|
||||
request: function(config) {
|
||||
if ('X-Auth-Token' in config.headers) {
|
||||
delete config.headers['X-Requested-With'];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
run.$inject = ['$window', '$rootScope'];
|
||||
|
|
|
@ -29,11 +29,17 @@ limitations under the License.
|
|||
|
||||
var httpCall = function (method, url, data, config) {
|
||||
var backend = $http;
|
||||
/* eslint-disable angular/window-service */
|
||||
url = $window.WEBROOT + url;
|
||||
/* eslint-enable angular/window-service */
|
||||
// An external call goes directly to some OpenStack service, say Glance
|
||||
// API, not to the Horizon API wrapper layer. Thus it doesn't need a
|
||||
// WEBROOT prefix
|
||||
var external = pop(config, 'external');
|
||||
if (!external) {
|
||||
/* eslint-disable angular/window-service */
|
||||
url = $window.WEBROOT + url;
|
||||
/* eslint-enable angular/window-service */
|
||||
|
||||
url = url.replace(/\/+/g, '/');
|
||||
url = url.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
if (angular.isUndefined(config)) {
|
||||
config = {};
|
||||
|
@ -44,7 +50,10 @@ limitations under the License.
|
|||
if (angular.isDefined(data)) {
|
||||
config.data = data;
|
||||
}
|
||||
if (angular.isObject(config.data)) {
|
||||
|
||||
if (uploadService.isFile(config.data)) {
|
||||
backend = uploadService.http;
|
||||
} else if (angular.isObject(config.data)) {
|
||||
for (var key in config.data) {
|
||||
if (config.data.hasOwnProperty(key) && uploadService.isFile(config.data[key])) {
|
||||
backend = uploadService.upload;
|
||||
|
@ -52,7 +61,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backend(config);
|
||||
};
|
||||
|
||||
|
@ -77,4 +85,14 @@ limitations under the License.
|
|||
return httpCall('DELETE', url, data, config);
|
||||
};
|
||||
}
|
||||
|
||||
function pop(obj, key) {
|
||||
if (!angular.isObject(obj)) {
|
||||
return undefined;
|
||||
}
|
||||
var value = obj[key];
|
||||
delete obj[key];
|
||||
return value;
|
||||
}
|
||||
|
||||
}());
|
||||
|
|
|
@ -9,8 +9,11 @@
|
|||
|
||||
describe('api service', function () {
|
||||
var api, $httpBackend;
|
||||
var WEBROOT = '/horizon/';
|
||||
|
||||
beforeEach(module('horizon.framework'));
|
||||
beforeEach(module('horizon.framework', function($provide) {
|
||||
$provide.value('$window', {WEBROOT: WEBROOT});
|
||||
}));
|
||||
beforeEach(inject(function ($injector) {
|
||||
api = $injector.get('horizon.framework.util.http.service');
|
||||
$httpBackend = $injector.get('$httpBackend');
|
||||
|
@ -26,10 +29,11 @@
|
|||
|
||||
function testGoodCall(apiMethod, verb, data) {
|
||||
var called = {};
|
||||
var url = WEBROOT + 'good';
|
||||
data = data || 'some complicated data';
|
||||
var suppliedData = verb === 'GET' ? undefined : data;
|
||||
$httpBackend.when(verb, '/good', suppliedData).respond({status: 'good'});
|
||||
$httpBackend.expect(verb, '/good', suppliedData);
|
||||
$httpBackend.when(verb, url, suppliedData).respond({status: 'good'});
|
||||
$httpBackend.expect(verb, url, suppliedData);
|
||||
apiMethod('/good', suppliedData).success(function (data) {
|
||||
called.data = data;
|
||||
});
|
||||
|
@ -39,9 +43,10 @@
|
|||
|
||||
function testBadCall(apiMethod, verb) {
|
||||
var called = {};
|
||||
var url = WEBROOT + 'bad';
|
||||
var suppliedData = verb === 'GET' ? undefined : 'some complicated data';
|
||||
$httpBackend.when(verb, '/bad', suppliedData).respond(500, '');
|
||||
$httpBackend.expect(verb, '/bad', suppliedData);
|
||||
$httpBackend.when(verb, url, suppliedData).respond(500, '');
|
||||
$httpBackend.expect(verb, url, suppliedData);
|
||||
apiMethod('/bad', suppliedData).error(function () {
|
||||
called.called = true;
|
||||
});
|
||||
|
@ -89,8 +94,24 @@
|
|||
testBadCall(api.delete, 'DELETE');
|
||||
});
|
||||
|
||||
describe('Upload.upload() call', function () {
|
||||
var Upload;
|
||||
describe('WEBROOT handling', function() {
|
||||
it('respects WEBROOT by default', function() {
|
||||
var expectedUrl = WEBROOT + 'good';
|
||||
$httpBackend.when('GET', expectedUrl).respond(200, '');
|
||||
$httpBackend.expect('GET', expectedUrl);
|
||||
api.get('/good');
|
||||
});
|
||||
|
||||
it('ignores WEBROOT with external = true flag', function() {
|
||||
var expectedUrl = '/good';
|
||||
$httpBackend.when('GET', expectedUrl).respond(200, '');
|
||||
$httpBackend.expect('GET', expectedUrl);
|
||||
api.get('/good', {external: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upload service', function () {
|
||||
var Upload, file;
|
||||
var called = {};
|
||||
|
||||
beforeEach(inject(function ($injector) {
|
||||
|
@ -98,22 +119,64 @@
|
|||
spyOn(Upload, 'upload').and.callFake(function (config) {
|
||||
called.config = config;
|
||||
});
|
||||
spyOn(Upload, 'http').and.callFake(function (config) {
|
||||
called.config = config;
|
||||
});
|
||||
file = new File(['part'], 'filename.sample');
|
||||
}));
|
||||
|
||||
it('is used when there is a File() blob inside data', function () {
|
||||
var file = new File(['part'], 'filename.sample');
|
||||
|
||||
it('upload() is used when there is a File() blob inside data', function () {
|
||||
api.post('/good', {first: file, second: 'the data'});
|
||||
expect(Upload.upload).toHaveBeenCalled();
|
||||
expect(called.config.data).toEqual({first: file, second: 'the data'});
|
||||
});
|
||||
|
||||
it('is NOT used in case there are no File() blobs inside data', function() {
|
||||
it('upload() is NOT used when a File() blob is passed as data', function () {
|
||||
api.post('/good', file);
|
||||
expect(Upload.upload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('upload() is NOT used in case there are no File() blobs inside data', function() {
|
||||
testGoodCall(api.post, 'POST', {second: 'the data'});
|
||||
expect(Upload.upload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
it('upload() respects WEBROOT by default', function() {
|
||||
api.post('/good', {first: file});
|
||||
expect(called.config.url).toEqual(WEBROOT + 'good');
|
||||
});
|
||||
|
||||
it('upload() ignores WEBROOT with external = true flag', function() {
|
||||
api.post('/good', {first: file}, {external: true});
|
||||
expect(called.config.url).toEqual('/good');
|
||||
});
|
||||
|
||||
it('http() is used when a File() blob is passed as data', function () {
|
||||
api.post('/good', file);
|
||||
expect(Upload.http).toHaveBeenCalled();
|
||||
expect(called.config.data).toEqual(file);
|
||||
});
|
||||
|
||||
it('http() is NOT used when there is a File() blob inside data', function () {
|
||||
api.post('/good', {first: file, second: 'the data'});
|
||||
expect(Upload.http).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('http() is NOT used when no File() blobs are passed at all', function() {
|
||||
testGoodCall(api.post, 'POST', {second: 'the data'});
|
||||
expect(Upload.http).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('http() respects WEBROOT by default', function() {
|
||||
api.post('/good', file);
|
||||
expect(called.config.url).toEqual(WEBROOT + 'good');
|
||||
});
|
||||
|
||||
it('http() ignores WEBROOT with external = true flag', function() {
|
||||
api.post('/good', file, {external: true});
|
||||
expect(called.config.url).toEqual('/good');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
from django.conf import settings
|
||||
from django.views import generic
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api.rest import urls
|
||||
from openstack_dashboard.api.rest import utils as rest_utils
|
||||
|
||||
|
@ -38,7 +39,13 @@ class Settings(generic.View):
|
|||
Examples of settings: OPENSTACK_HYPERVISOR_FEATURES
|
||||
"""
|
||||
url_regex = r'settings/$'
|
||||
SPECIALS = {
|
||||
'HORIZON_IMAGES_UPLOAD_MODE': api.glance.get_image_upload_mode()
|
||||
}
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
return {k: getattr(settings, k, None) for k in settings_allowed}
|
||||
plain_settings = {k: getattr(settings, k, None) for k
|
||||
in settings_allowed if k not in self.SPECIALS}
|
||||
plain_settings.update(self.SPECIALS)
|
||||
return plain_settings
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
"""API for the glance service.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views import generic
|
||||
from six.moves import zip as izip
|
||||
|
||||
|
@ -115,6 +117,10 @@ class ImageProperties(generic.View):
|
|||
)
|
||||
|
||||
|
||||
class UploadObjectForm(forms.Form):
|
||||
data = forms.FileField(required=False)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Images(generic.View):
|
||||
"""API for Glance images.
|
||||
|
@ -162,8 +168,26 @@ class Images(generic.View):
|
|||
'has_prev_data': has_prev_data,
|
||||
}
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
# note: not an AJAX request - the body will be raw file content mixed with
|
||||
# metadata
|
||||
@csrf_exempt
|
||||
def post(self, request):
|
||||
form = UploadObjectForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
raise rest_utils.AjaxError(500, 'Invalid request')
|
||||
|
||||
data = form.clean()
|
||||
meta = create_image_metadata(request.POST)
|
||||
meta['data'] = data['data']
|
||||
|
||||
image = api.glance.image_create(request, **meta)
|
||||
return rest_utils.CreatedResponse(
|
||||
'/api/glance/images/%s' % image.name,
|
||||
image.to_dict()
|
||||
)
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def put(self, request):
|
||||
"""Create an Image.
|
||||
|
||||
Create an Image using the parameters supplied in the POST
|
||||
|
@ -193,10 +217,13 @@ class Images(generic.View):
|
|||
"""
|
||||
meta = create_image_metadata(request.DATA)
|
||||
|
||||
if request.DATA.get('import_data'):
|
||||
meta['copy_from'] = request.DATA.get('image_url')
|
||||
if request.DATA.get('image_url'):
|
||||
if request.DATA.get('import_data'):
|
||||
meta['copy_from'] = request.DATA.get('image_url')
|
||||
else:
|
||||
meta['location'] = request.DATA.get('image_url')
|
||||
else:
|
||||
meta['location'] = request.DATA.get('image_url')
|
||||
meta['data'] = request.DATA.get('data')
|
||||
|
||||
image = api.glance.image_create(request, **meta)
|
||||
return rest_utils.CreatedResponse(
|
||||
|
@ -326,7 +353,7 @@ def handle_unknown_properties(data, meta):
|
|||
'container_format', 'min_disk', 'min_ram', 'name',
|
||||
'properties', 'kernel', 'ramdisk',
|
||||
'tags', 'import_data', 'source',
|
||||
'image_url', 'source_type',
|
||||
'image_url', 'source_type', 'data',
|
||||
'checksum', 'created_at', 'deleted', 'is_copying',
|
||||
'deleted_at', 'is_public', 'virtual_size',
|
||||
'status', 'size', 'owner', 'id', 'updated_at']
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
'ngSanitize',
|
||||
'schemaForm',
|
||||
'smart-table',
|
||||
'ngFileUpload',
|
||||
'ui.bootstrap'
|
||||
];
|
||||
|
||||
|
|
|
@ -110,6 +110,11 @@
|
|||
|
||||
function submit() {
|
||||
var finalModel = angular.extend({}, model.image, model.metadata);
|
||||
if (finalModel.source_type === 'url') {
|
||||
delete finalModel.data;
|
||||
} else {
|
||||
delete finalModel.image_url;
|
||||
}
|
||||
return glance.createImage(finalModel).then(onCreateImage);
|
||||
}
|
||||
|
||||
|
|
|
@ -119,6 +119,86 @@
|
|||
id: '2', prop1: '11', prop3: '3'});
|
||||
});
|
||||
|
||||
it('does not pass location to create image if source_type is NOT url', function() {
|
||||
var image = {name: 'Test', source_type: 'file-direct', image_url: 'http://somewhere',
|
||||
data: {name: 'test_file'}
|
||||
};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
|
||||
expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test',
|
||||
source_type: 'file-direct', data: {name: 'test_file'}});
|
||||
});
|
||||
|
||||
it('does not pass file to create image if source_type is url', function() {
|
||||
var image = {name: 'Test', source_type: 'url', image_url: 'http://somewhere',
|
||||
data: {name: 'test_file'}
|
||||
};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
|
||||
expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test',
|
||||
source_type: 'url', image_url: 'http://somewhere'});
|
||||
});
|
||||
|
||||
it('does not pass location to create image if source_type is NOT url', function() {
|
||||
var image = {name: 'Test', source_type: 'file-direct', image_url: 'http://somewhere',
|
||||
data: {name: 'test_file'}
|
||||
};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
|
||||
expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test',
|
||||
source_type: 'file-direct', data: {name: 'test_file'}});
|
||||
});
|
||||
|
||||
it('does not pass file to create image if source_type is url', function() {
|
||||
var image = {name: 'Test', source_type: 'url', image_url: 'http://somewhere',
|
||||
data: {name: 'test_file'}
|
||||
};
|
||||
|
||||
spyOn($scope, '$emit').and.callThrough();
|
||||
spyOn(glanceAPI, 'createImage').and.callThrough();
|
||||
spyOn(wizardModalService, 'modal').and.callThrough();
|
||||
|
||||
service.initScope($scope);
|
||||
service.perform();
|
||||
$scope.$emit(events.IMAGE_CHANGED, image);
|
||||
|
||||
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
|
||||
modalArgs.submit();
|
||||
|
||||
expect(glanceAPI.createImage).toHaveBeenCalledWith({ name: 'Test',
|
||||
source_type: 'url', image_url: 'http://somewhere'});
|
||||
});
|
||||
|
||||
it('should raise event even if update meta data fails', function() {
|
||||
var image = { name: 'Test', id: '2' };
|
||||
var failedPromise = function() {
|
||||
|
|
|
@ -46,14 +46,16 @@
|
|||
) {
|
||||
var ctrl = this;
|
||||
|
||||
settings.getSettings().then(getConfiguredFormats);
|
||||
settings.getSettings().then(getConfiguredFormatsAndModes);
|
||||
ctrl.validationRules = validationRules;
|
||||
ctrl.imageFormats = imageFormats;
|
||||
ctrl.diskFormats = [];
|
||||
ctrl.prepareUpload = prepareUpload;
|
||||
|
||||
ctrl.image = {
|
||||
source_type: 'url',
|
||||
image_url: '',
|
||||
data: {},
|
||||
is_copying: true,
|
||||
protected: false,
|
||||
min_disk: 0,
|
||||
|
@ -73,6 +75,10 @@
|
|||
{ label: gettext('No'), value: false }
|
||||
];
|
||||
|
||||
ctrl.imageSourceOptions = [
|
||||
{ label: gettext('URL'), value: 'url' }
|
||||
];
|
||||
|
||||
ctrl.imageVisibilityOptions = [
|
||||
{ label: gettext('Public'), value: 'public'},
|
||||
{ label: gettext('Private'), value: 'private' }
|
||||
|
@ -82,6 +88,7 @@
|
|||
ctrl.ramdiskImages = [];
|
||||
|
||||
ctrl.setFormats = setFormats;
|
||||
ctrl.isLocalFileUpload = isLocalFileUpload;
|
||||
|
||||
init();
|
||||
|
||||
|
@ -93,18 +100,32 @@
|
|||
|
||||
///////////////////////////
|
||||
|
||||
function getConfiguredFormats(response) {
|
||||
function prepareUpload(file) {
|
||||
ctrl.image.data = file;
|
||||
}
|
||||
|
||||
function getConfiguredFormatsAndModes(response) {
|
||||
var settingsFormats = response.OPENSTACK_IMAGE_FORMATS;
|
||||
var uploadMode = response.HORIZON_IMAGES_UPLOAD_MODE;
|
||||
var dupe = angular.copy(imageFormats);
|
||||
angular.forEach(dupe, function stripUnknown(name, key) {
|
||||
if (settingsFormats.indexOf(key) === -1) {
|
||||
delete dupe[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (uploadMode !== 'off') {
|
||||
ctrl.imageSourceOptions.splice(0, 0, {
|
||||
label: gettext('File'), value: 'file-' + uploadMode
|
||||
});
|
||||
}
|
||||
ctrl.imageFormats = dupe;
|
||||
}
|
||||
|
||||
function isLocalFileUpload() {
|
||||
var type = ctrl.image.source_type;
|
||||
return (type === 'file-legacy' || type === 'file-direct');
|
||||
}
|
||||
|
||||
// emits new data to parent listeners
|
||||
function watchImageCollection(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
});
|
||||
});
|
||||
|
||||
describe('getConfiguredFormats', function() {
|
||||
describe('getConfiguredFormatsAndModes', function() {
|
||||
|
||||
it('uses the settings for the source of allowed image formats', function() {
|
||||
var ctrl = createController();
|
||||
|
@ -178,6 +178,55 @@
|
|||
};
|
||||
expect(ctrl.imageFormats).toEqual(expectation);
|
||||
});
|
||||
|
||||
describe('upload mode', function() {
|
||||
var urlSourceOption = { label: gettext('URL'), value: 'url' };
|
||||
|
||||
it('set to "off" disables local file upload', function() {
|
||||
var ctrl = createController();
|
||||
settingsCall.resolve({
|
||||
OPENSTACK_IMAGE_FORMATS: [],
|
||||
HORIZON_IMAGES_UPLOAD_MODE: 'off'
|
||||
});
|
||||
$timeout.flush();
|
||||
expect(ctrl.imageSourceOptions).toEqual([urlSourceOption]);
|
||||
});
|
||||
|
||||
it('set to a non-"off" value enables local file upload', function() {
|
||||
var ctrl = createController();
|
||||
var fileSourceOption = { label: gettext('File'), value: 'file-sample' };
|
||||
settingsCall.resolve({
|
||||
OPENSTACK_IMAGE_FORMATS: [],
|
||||
HORIZON_IMAGES_UPLOAD_MODE: 'sample'
|
||||
});
|
||||
$timeout.flush();
|
||||
expect(ctrl.imageSourceOptions).toEqual([fileSourceOption, urlSourceOption]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLocalFileUpload()', function() {
|
||||
var ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
ctrl = createController();
|
||||
});
|
||||
|
||||
it('returns true for source-type == "file-direct"', function() {
|
||||
ctrl.image = {source_type: 'file-direct'};
|
||||
expect(ctrl.isLocalFileUpload()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for source-type == "file-legacy"', function() {
|
||||
ctrl.image = {source_type: 'file-legacy'};
|
||||
expect(ctrl.isLocalFileUpload()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for any else source-type', function() {
|
||||
ctrl.image = {source_type: 'url'};
|
||||
expect(ctrl.isLocalFileUpload()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -52,7 +52,46 @@
|
|||
|
||||
<div class="selected-source">
|
||||
<div class="row form-group">
|
||||
<div class="col-xs-6 col-sm-6" ng-if="ctrl.image.source_type === 'url'">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group">
|
||||
<label class="control-label required">
|
||||
<translate>Source Type</translate>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<div class="btn-group">
|
||||
<label class="btn btn-default btn-toggle"
|
||||
ng-repeat="option in ctrl.imageSourceOptions"
|
||||
ng-model="ctrl.image.source_type"
|
||||
btn-radio="option.value">{$ ::option.label $}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row form-group" ng-if="ctrl.isLocalFileUpload()">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group required">
|
||||
<label class="control-label" for="imageForm-image_url">
|
||||
<translate>File</translate><span class="hz-icon-required fa fa-asterisk"></span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" ng-model="image_file"
|
||||
ngf-select="ctrl.prepareUpload(image_file)"
|
||||
name="image_file" ng-required="true"
|
||||
id="imageForm-image_file" translate>Browse...</button>
|
||||
</span>
|
||||
<input type="text" class="form-control" readonly ng-model="image_file.name">
|
||||
</div>
|
||||
<p class="help-block alert alert-danger"
|
||||
ng-show="imageForm.image_file.$invalid && imageForm.image_file.$dirty">
|
||||
<translate>A local file should be selected.</translate>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row form-group" ng-if="ctrl.image.source_type === 'url'">
|
||||
<div class="col-xs-6 col-sm-6">
|
||||
<div class="form-group required"
|
||||
ng-class="{'has-error':imageForm.image_url.$invalid && imageForm.image_url.$dirty}">
|
||||
<label class="control-label" for="imageForm-image_url">
|
||||
|
|
|
@ -135,10 +135,38 @@
|
|||
* @returns {Object} The result of the API call
|
||||
*/
|
||||
function createImage(image) {
|
||||
return apiService.post('/api/glance/images/', image)
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to create the image.'));
|
||||
});
|
||||
var localFile;
|
||||
var method = image.source_type === 'file-legacy' ? 'post' : 'put';
|
||||
if (image.source_type === 'file-direct' && 'data' in image) {
|
||||
localFile = image.data;
|
||||
image = angular.extend({}, image);
|
||||
image.data = localFile.name;
|
||||
}
|
||||
|
||||
function onImageQueued(response) {
|
||||
var image = response.data;
|
||||
if ('upload_url' in image) {
|
||||
return apiService.put(image.upload_url, localFile, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'X-Auth-Token': image.token_id
|
||||
},
|
||||
external: true
|
||||
}).then(
|
||||
function success() { return response; },
|
||||
onError
|
||||
);
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
function onError() {
|
||||
toastService.add('error', gettext('Unable to create the image.'));
|
||||
}
|
||||
|
||||
return apiService[method]('/api/glance/images/', image)
|
||||
.then(onImageQueued, onError);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -63,18 +63,6 @@
|
|||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "createImage",
|
||||
"method": "post",
|
||||
"path": "/api/glance/images/",
|
||||
"data": {
|
||||
name: '1'
|
||||
},
|
||||
"error": "Unable to create the image.",
|
||||
"testInput": [
|
||||
{name: '1'}
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "updateImage",
|
||||
"method": "patch",
|
||||
|
@ -184,6 +172,114 @@
|
|||
expect(service.deleteImage("whatever", true)).toBe("promise");
|
||||
});
|
||||
|
||||
describe('createImage', function() {
|
||||
var $q, $rootScope, imageQueuedPromise;
|
||||
|
||||
beforeEach(inject(function(_$q_, _$rootScope_) {
|
||||
$q = _$q_;
|
||||
$rootScope = _$rootScope_;
|
||||
imageQueuedPromise = $q.defer();
|
||||
spyOn(apiService, 'put').and.returnValue(imageQueuedPromise.promise);
|
||||
}));
|
||||
|
||||
it('shows error message when arguments are insufficient', function() {
|
||||
spyOn(toastService, 'add');
|
||||
|
||||
service.createImage.apply(null, [{name: 1}]);
|
||||
|
||||
expect(apiService.put).toHaveBeenCalledWith('/api/glance/images/', {name: 1});
|
||||
imageQueuedPromise.reject();
|
||||
$rootScope.$apply();
|
||||
expect(toastService.add).toHaveBeenCalledWith('error', "Unable to create the image.");
|
||||
});
|
||||
|
||||
describe('external upload of a local file', function() {
|
||||
var fakeFile = {name: 'test file'};
|
||||
var imageData = {
|
||||
name: 'test', source_type: 'file-direct', diskFormat: 'iso', data: fakeFile
|
||||
};
|
||||
var queuedImage = {
|
||||
'name': imageData.name,
|
||||
'upload_url': 'http://sample.com',
|
||||
'token_id': 'my token'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
apiService.put.and.returnValues(
|
||||
imageQueuedPromise.promise,
|
||||
{then: angular.noop});
|
||||
service.createImage(imageData);
|
||||
});
|
||||
|
||||
it('does not send the file itself during the first call', function() {
|
||||
var passedImageData = angular.extend({}, imageData, {data: fakeFile.name});
|
||||
expect(apiService.put.calls.argsFor(0)).toEqual(['/api/glance/images/', passedImageData]);
|
||||
});
|
||||
|
||||
it('second call is not made until the image is created', function() {
|
||||
expect(apiService.put.calls.count()).toBe(1);
|
||||
|
||||
imageQueuedPromise.resolve({data: queuedImage});
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(apiService.put.calls.count()).toBe(2);
|
||||
});
|
||||
|
||||
it('second call is not started if the initial image creation fails', function() {
|
||||
imageQueuedPromise.reject();
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(apiService.put.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('external upload uses data from initial image creation', function() {
|
||||
imageQueuedPromise.resolve({data: queuedImage});
|
||||
$rootScope.$apply();
|
||||
|
||||
expect(apiService.put).toHaveBeenCalledWith(
|
||||
queuedImage.upload_url,
|
||||
fakeFile,
|
||||
{
|
||||
headers: {
|
||||
'X-Auth-Token': queuedImage.token_id,
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
external: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('proxied (AKA legacy) upload of a local file', function() {
|
||||
var fakeFile = {name: 'test file'};
|
||||
var imageData = {
|
||||
name: 'test', source_type: 'file-legacy', diskFormat: 'iso', data: fakeFile
|
||||
};
|
||||
var queuedImage = {
|
||||
'name': imageData.name
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
var q = $q.defer();
|
||||
q.resolve({data: queuedImage});
|
||||
spyOn(apiService, 'post').and.returnValue(q.promise);
|
||||
service.createImage(imageData);
|
||||
});
|
||||
|
||||
it('emits one POST and not PUTs', function() {
|
||||
expect(apiService.post.calls.count()).toBe(1);
|
||||
expect(apiService.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends the file itself during the POST call', function() {
|
||||
expect(apiService.post).toHaveBeenCalledWith('/api/glance/images/', imageData);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.openstack-service-api', []);
|
||||
.module('horizon.app.core.openstack-service-api', ['ngFileUpload']);
|
||||
}());
|
||||
|
|
|
@ -154,7 +154,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
'kernel_id': 'kernel',
|
||||
}}
|
||||
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content.decode('utf-8'),
|
||||
'{"name": "testimage"}')
|
||||
|
@ -190,7 +190,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
'kernel_id': 'kernel',
|
||||
}}
|
||||
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content.decode('utf-8'),
|
||||
'{"name": "testimage"}')
|
||||
|
@ -226,7 +226,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
'kernel_id': 'kernel',
|
||||
}}
|
||||
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content.decode('utf-8'),
|
||||
'{"name": "testimage"}')
|
||||
|
@ -244,7 +244,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
|
||||
''')
|
||||
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 400)
|
||||
self.assertEqual(response.content.decode('utf-8'),
|
||||
'"invalid visibility option: verybad"')
|
||||
|
@ -270,7 +270,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
'min_ram': 0,
|
||||
'properties': {}
|
||||
}
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response['location'], '/api/glance/images/testimage')
|
||||
gc.image_create.assert_called_once_with(request, **metadata)
|
||||
|
@ -297,7 +297,7 @@ class ImagesRestTestCase(test.TestCase):
|
|||
'min_ram': 0,
|
||||
'properties': {'arbitrary': 'property', 'another': 'prop'}
|
||||
}
|
||||
response = glance.Images().post(request)
|
||||
response = glance.Images().put(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response['location'], '/api/glance/images/testimage')
|
||||
gc.image_create.assert_called_once_with(request, **metadata)
|
||||
|
|
Loading…
Reference in New Issue