[PATCH 0/3 V4] Storage volume upload UI

V3 - V4: - Rebase V2 - V3: - Adjust error handlers to work with Chrome V1 - V2: - Deal with non-existing file and read permission errors Aline Manera (3): Storage volume upload: Keep the task tracking to update the UI Storage volume upload: Let the 'format' parameter be an empty string Enable storage volume upload on UI src/kimchi/API.json | 2 +- src/kimchi/mockmodel.py | 3 +- src/kimchi/model/storagevolumes.py | 20 ++++-- tests/test_model_storagevolume.py | 2 +- ui/js/src/kimchi.api.js | 25 +++++-- ui/js/src/kimchi.storagepool_add_volume_main.js | 96 +++++++++++++++++++++---- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 8 files changed, 126 insertions(+), 28 deletions(-) -- 2.1.0

When creating a new storage volume for upload we should keep the task tracking to allow updating the UI properly. That way the task will be only finished on upload error or when it is finished. Signed-off-by: Aline Manera <alinefm@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 3 ++- src/kimchi/model/storagevolumes.py | 20 ++++++++++++++------ tests/test_model_storagevolume.py | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 19dfd1e..aaf1af2 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -308,7 +308,7 @@ class MockModel(Model): return self._model_storagevolume_lookup(pool, vol) - def _mock_storagevolume_doUpload(self, vol, offset, data, data_size): + def _mock_storagevolume_doUpload(self, cb, vol, offset, data, data_size): vol_path = vol.path() # MockModel does not create the storage volume as a file @@ -325,6 +325,7 @@ class MockModel(Model): fd.write(data) except Exception, e: os.remove(vol_path) + cb('', False) raise OperationFailed("KCHVOL0029E", {"err": e.message}) def _mock_partitions_get_list(self): diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index 9165946..f8b3263 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -155,12 +155,14 @@ class StorageVolumesModel(object): name) vol_path = vol_info['path'] - if params.get('upload', False): - upload_volumes[vol_path] = {'lock': threading.Lock(), 'offset': 0} - set_disk_used_by(self.objstore, vol_info['path'], []) - cb('', True) + if params.get('upload', False): + upload_volumes[vol_path] = {'lock': threading.Lock(), + 'offset': 0, 'cb': cb} + cb('ready for upload') + else: + cb('OK', True) def _create_volume_with_url(self, cb, params): pool_name = params['pool'] @@ -459,7 +461,7 @@ class StorageVolumeModel(object): cb('OK', True) - def doUpload(self, vol, offset, data, data_size): + def doUpload(self, cb, vol, offset, data, data_size): try: st = self.conn.get().newStream(0) vol.upload(st, offset, data_size) @@ -467,6 +469,8 @@ class StorageVolumeModel(object): st.finish() except Exception as e: st and st.abort() + cb('', False) + try: vol.delete(0) except Exception as e: @@ -489,17 +493,21 @@ class StorageVolumeModel(object): if vol_data is None: raise OperationFailed("KCHVOL0027E", {"vol": vol_path}) + cb = vol_data['cb'] lock = vol_data['lock'] with lock: offset = vol_data['offset'] if (offset + chunk_size) > vol_capacity: raise OperationFailed("KCHVOL0028E") - self.doUpload(vol, offset, chunk_data, chunk_size) + cb('%s/%s' % (offset, vol_capacity)) + self.doUpload(cb, vol, offset, chunk_data, chunk_size) + cb('%s/%s' % (offset + chunk_size, vol_capacity)) vol_data['offset'] += chunk_size if vol_data['offset'] == vol_capacity: del upload_volumes[vol_path] + cb('OK', True) class IsoVolumesModel(object): diff --git a/tests/test_model_storagevolume.py b/tests/test_model_storagevolume.py index b6aaffb..5e76d3d 100644 --- a/tests/test_model_storagevolume.py +++ b/tests/test_model_storagevolume.py @@ -168,7 +168,7 @@ def _do_volume_test(self, model, host, ssl_port, pool_name): task_id = json.loads(resp.read())['id'] wait_task(_task_lookup, task_id) status = json.loads(self.request('/tasks/%s' % task_id).read()) - self.assertEquals('finished', status['status']) + self.assertEquals('ready for upload', status['message']) # Upload volume content url = 'https://%s:%s' % (host, ssl_port) + uri + '/' + filename -- 2.1.0

When creating a new storage volume for upload, we may not know the right format for it. So let it empty and then libvirt will update it accordindly. Signed-off-by: Aline Manera <alinefm@linux.vnet.ibm.com> --- src/kimchi/API.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kimchi/API.json b/src/kimchi/API.json index b9a41eb..e8da7d9 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -216,7 +216,7 @@ "format": { "description": "The format of the volume", "type": "string", - "pattern": "^(bochs|cloop|cow|dmg|qcow|qcow2|qed|raw|vmdk|vpc)$", + "pattern": "^(|bochs|cloop|cow|dmg|qcow|qcow2|qed|raw|vmdk|vpc)$", "error": "KCHVOL0015E" }, "url": { -- 2.1.0

Also update the code to request the new upload API. Signed-off-by: Aline Manera <alinefm@linux.vnet.ibm.com> --- ui/js/src/kimchi.api.js | 25 +++++-- ui/js/src/kimchi.storagepool_add_volume_main.js | 96 +++++++++++++++++++++---- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/ui/js/src/kimchi.api.js b/ui/js/src/kimchi.api.js index 5c36418..9207d7e 100644 --- a/ui/js/src/kimchi.api.js +++ b/ui/js/src/kimchi.api.js @@ -1243,14 +1243,29 @@ var kimchi = { }, /** - * Add a volume to a given storage pool. + * Create a new volume with capacity */ - uploadVolumeToSP: function(settings, suc, err) { - var fd = settings['formData']; - var sp = encodeURIComponent(settings['sp']); + createVolumeWithCapacity: function(poolName, settings, suc, err) { kimchi.requestJSON({ - url : 'storagepools/' + sp + '/storagevolumes', + url : 'storagepools/' + encodeURIComponent(poolName) + '/storagevolumes', type : 'POST', + contentType : "application/json", + data : JSON.stringify(settings), + dataType : "json", + success : suc, + error : err + }); + }, + + /** + * Upload volume content + */ + uploadVolumeToSP: function(poolName, volumeName, settings, suc, err) { + var url = 'storagepools/' + encodeURIComponent(poolName) + '/storagevolumes/' + encodeURIComponent(volumeName); + var fd = settings['formData']; + kimchi.requestJSON({ + url : url, + type : 'PUT', data : fd, processData : false, contentType : false, diff --git a/ui/js/src/kimchi.storagepool_add_volume_main.js b/ui/js/src/kimchi.storagepool_add_volume_main.js index 590ccde..c56f68c 100644 --- a/ui/js/src/kimchi.storagepool_add_volume_main.js +++ b/ui/js/src/kimchi.storagepool_add_volume_main.js @@ -1,7 +1,7 @@ /* * Project Kimchi * - * Copyright IBM, Corp. 2014 + * Copyright IBM, Corp. 2014-2015 * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -79,18 +79,90 @@ kimchi.sp_add_volume_main = function() { }; var uploadFile = function() { + var chunkSize = 8 * 1024 * 1024; // 8MB + var uploaded = 0; + var blobFile = $(localFileBox)[0].files[0]; - var fileName = blobFile.name; - var fd = new FormData(); - fd.append('name', fileName); - fd.append('file', blobFile); - kimchi.uploadVolumeToSP({ - sp: kimchi.selectedSP, - formData: fd - }, function(result) { - kimchi.window.close(); - kimchi.topic('kimchi/storageVolumeAdded').publish(); - }, onError); + + var createUploadVol = function() { + kimchi.createVolumeWithCapacity(kimchi.selectedSP, { + name: blobFile.name, + format: '', + capacity: blobFile.size, + upload: true + }, function(result) { + kimchi.window.close(); + trackVolCreation(result.id); + }, onError); + }; + + var uploadRequest = function(blob) { + var fd = new FormData(); + fd.append('chunk', blob); + fd.append('chunk_size', blob.size); + + kimchi.uploadVolumeToSP(kimchi.selectedSP, blobFile.name, { + formData: fd + }, function(result) { + if (uploaded < blobFile.size) + setTimeout(doUpload, 500); + }, onError); + + uploaded += blob.size + }; + + // Check file exists and has read permission + try { + var blob = blobFile.slice(0, 20); + var reader = new FileReader(); + reader.onloadend = function(e) { + if (e.loaded == 0) + kimchi.message.error.code('KCHAPI6008E'); + else + createUploadVol(); + }; + + reader.readAsBinaryString(blob); + } catch (err) { + kimchi.message.error.code('KCHAPI6008E'); + return; + } + + var doUpload = function() { + try { + var blob = blobFile.slice(uploaded, uploaded + chunkSize); + var reader = new FileReader(); + reader.onloadend = function(e) { + if (e.loaded == 0) + kimchi.message.error.code('KCHAPI6009E'); + else + uploadRequest(blob); + }; + + reader.readAsBinaryString(blob); + } catch (err) { + kimchi.message.error.code('KCHAPI6009E'); + return; + } + } + + var trackVolCreation = function(taskid) { + var onTaskResponse = function(result) { + var taskStatus = result['status']; + var taskMsg = result['message']; + if (taskStatus == 'running') { + if (taskMsg != 'ready for upload') { + setTimeout(function() { + trackVolCreation(taskid); + }, 2000); + } else { + kimchi.topic('kimchi/storageVolumeAdded').publish(); + doUpload(); + } + } + }; + kimchi.getTask(taskid, onTaskResponse, onError); + }; }; $(addButton).on('click', function(event) { diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index 675d9a6..f705613 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -39,6 +39,8 @@ "KCHAPI6004E": "$_("This is not a valid URL.")", "KCHAPI6005E": "$_("No such data available.")", "KCHAPI6007E": "$_("Can not contact the host system. Verify the host system is up and that you have network connectivity to it. HTTP request response %1. ")", + "KCHAPI6008E": "$_("Unable to read file.")", + "KCHAPI6009E": "$_("Error while uploading file.")", "KCHAPI6001M": "$_("Delete Confirmation")", "KCHAPI6002M": "$_("OK")", diff --git a/ui/pages/storagepool-add-volume.html.tmpl b/ui/pages/storagepool-add-volume.html.tmpl index b5f365f..048f1ed 100644 --- a/ui/pages/storagepool-add-volume.html.tmpl +++ b/ui/pages/storagepool-add-volume.html.tmpl @@ -1,7 +1,7 @@ #* * Project Kimchi * - * Copyright IBM, Corp. 2014 + * Copyright IBM, Corp. 2014-2015 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ </div> <div class="form-section"> <h2> - <input type="radio" id="volume-type-upload" class="volume-type" name="volumeType" value="upload" disabled/> + <input type="radio" id="volume-type-upload" class="volume-type" name="volumeType" value="upload"/> <label for="volume-type-upload"> $_("Upload a file") </label> -- 2.1.0

Reviewed-by: Daniel Barboza <dhbarboza82@gmail.com> ps: there is a typo in the commit msg of patch 2/3 that you might want to fix before pushing to master: "libvirt will update it **accordindly**" On 05/29/2015 12:24 PM, Aline Manera wrote:
V3 - V4: - Rebase
V2 - V3: - Adjust error handlers to work with Chrome
V1 - V2: - Deal with non-existing file and read permission errors
Aline Manera (3): Storage volume upload: Keep the task tracking to update the UI Storage volume upload: Let the 'format' parameter be an empty string Enable storage volume upload on UI
src/kimchi/API.json | 2 +- src/kimchi/mockmodel.py | 3 +- src/kimchi/model/storagevolumes.py | 20 ++++-- tests/test_model_storagevolume.py | 2 +- ui/js/src/kimchi.api.js | 25 +++++-- ui/js/src/kimchi.storagepool_add_volume_main.js | 96 +++++++++++++++++++++---- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 8 files changed, 126 insertions(+), 28 deletions(-)
participants (2)
-
Aline Manera
-
Daniel Henrique Barboza