Merge "[NG] Support local file upload in Create Image workflow"

This commit is contained in:
Jenkins 2016-08-08 23:01:16 +00:00 committed by Gerrit Code Review
commit 72ad1e3fd5
15 changed files with 505 additions and 53 deletions

View File

@ -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'];

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,6 @@
'ngSanitize',
'schemaForm',
'smart-table',
'ngFileUpload',
'ui.bootstrap'
];

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,5 +17,5 @@
'use strict';
angular
.module('horizon.app.core.openstack-service-api', []);
.module('horizon.app.core.openstack-service-api', ['ngFileUpload']);
}());

View File

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