[PATCH 0/3 V2] Storage volume upload UI

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 | 73 ++++++++++++++++++++++--- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 8 files changed, 107 insertions(+), 24 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 c81cabb..d73871d 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 dc807e4..664b128 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -155,9 +155,6 @@ class StorageVolumesModel(object): name) vol_path = vol_info['path'] - if params.get('upload', False): - upload_volumes[vol_path] = {'lock': threading.Lock(), 'offset': 0} - try: with self.objstore as session: session.store('storagevolume', vol_path, {'ref_cnt': 0}) @@ -167,7 +164,12 @@ class StorageVolumesModel(object): kimchi_log.warning('Unable to store storage volume id in ' 'objectstore due error: %s', e.message) - 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'] @@ -462,7 +464,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) @@ -470,6 +472,8 @@ class StorageVolumeModel(object): st.finish() except Exception as e: st and st.abort() + cb('', False) + try: vol.delete(0) except Exception as e: @@ -492,17 +496,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 fea1de1..8f0e878 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 a6330ae..d92ae9b 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 | 73 ++++++++++++++++++++++--- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 4 files changed, 89 insertions(+), 15 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..b826831 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,17 +79,74 @@ kimchi.sp_add_volume_main = function() { }; var uploadFile = function() { + var chunkSize = 8 * 1024 * 1024; // 8MB + var uploaded = 0; + var blobFile = $(localFileBox)[0].files[0]; + + // Check file exists and has read permission + try { + var blob = blobFile.slice(0, 1); + var reader = new FileReader(); + reader.readAsBinaryString(blob); + } catch (err) { + kimchi.message.error.code('KCHAPI6008E'); + return; + } + + var fileSize = blobFile.size; var fileName = blobFile.name; - var fd = new FormData(); - fd.append('name', fileName); - fd.append('file', blobFile); - kimchi.uploadVolumeToSP({ - sp: kimchi.selectedSP, - formData: fd + + var doUpload = function() { + try { + var blob = blobFile.slice(uploaded, uploaded + chunkSize); + var reader = new FileReader(); + reader.readAsBinaryString(blob); + + var fd = new FormData(); + fd.append('chunk', blob); + fd.append('chunk_size', blob.size); + } catch (err) { + kimchi.message.error.code('KCHAPI6009E'); + return; + } + + kimchi.uploadVolumeToSP(kimchi.selectedSP, fileName, { + formData: fd + }, function(result) { + if (uploaded < fileSize) + setTimeout(doUpload, 1000); + }, onError); + + uploaded += blob.size + } + + 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); + }; + + kimchi.createVolumeWithCapacity(kimchi.selectedSP, { + name: fileName, + format: '', + capacity: fileSize, + upload: true }, function(result) { kimchi.window.close(); - kimchi.topic('kimchi/storageVolumeAdded').publish(); + trackVolCreation(result.id); }, onError); }; 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

I have found a few errors when testing this patchset: - If I select a file which doesn't exist, I get the error message " KCHVOL0020E: Storage volume capacity must be an integer number.", which makes no sense in this context. This errors seems to happen only when using Chrome but not Firefox. - If I select a file which doesn't have read permissions, I get two errors: "Can not contact the host system. Verify the host system is up and that you have network connectivity to it. HTTP request response rejected." "Undefined" And a "ghost" storage volume is listed inside the storage pool. And I can't find any special error message in the logs. On 21-05-2015 18:08, Aline Manera wrote:
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 | 73 ++++++++++++++++++++++--- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 8 files changed, 107 insertions(+), 24 deletions(-)

On 28/05/2015 15:02, Crístian Deives wrote:
I have found a few errors when testing this patchset:
- If I select a file which doesn't exist, I get the error message " KCHVOL0020E: Storage volume capacity must be an integer number.", which makes no sense in this context. This errors seems to happen only when using Chrome but not Firefox.
- If I select a file which doesn't have read permissions, I get two errors:
"Can not contact the host system. Verify the host system is up and that you have network connectivity to it. HTTP request response rejected."
"Undefined"
And a "ghost" storage volume is listed inside the storage pool. And I can't find any special error message in the logs.
Is that also Chrome specific issue?
On 21-05-2015 18:08, Aline Manera wrote:
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 | 73 ++++++++++++++++++++++--- ui/pages/i18n.json.tmpl | 2 + ui/pages/storagepool-add-volume.html.tmpl | 4 +- 8 files changed, 107 insertions(+), 24 deletions(-)

On 28-05-2015 14:11, Aline Manera wrote:
- If I select a file which doesn't have read permissions, I get two errors:
"Can not contact the host system. Verify the host system is up and that you have network connectivity to it. HTTP request response rejected."
"Undefined"
And a "ghost" storage volume is listed inside the storage pool. And I can't find any special error message in the logs.
Is that also Chrome specific issue?
Yes. When I try it on Firefox, I get: "KCHAPI6008E: Unable to read file."
participants (2)
-
Aline Manera
-
Crístian Deives