[PATCH v3 0/5] Create VMs Asynchronously

v3 Changes: Instead of adding a description key to the task, just append the action desciptor to the target_uri. If a guest has a large disk, and uses a filesystem that requires preallocation, it can take several minutes to create a VM. During that time, kimchi is tied up by the VM creation. This patch changes the VMs Collection to be an AsyncCollection. Another change required for this was to create a more granular way to query tasks. Currently it is not possible (using the API) to query tasks for the same collection or resource type that may have different operations. For example, VM cloning is also an asynchronous operation. For the guests tab, the UI was querying all running tasks and displaying them with the Cloning label. This picked up VMs that were being created as well. For more information about how the tasks can be queried, see the updated API doc and the UI change. Christy Perez (5): Granular Task Queries: Backend Granular task query test updates Granular Task Queries: UI Create guests asynchronously: Backend Async vm creation test updates docs/API.md | 16 ++++++++ src/kimchi/control/vms.py | 4 +- src/kimchi/mockmodel.py | 7 ++-- src/kimchi/model/debugreports.py | 4 +- src/kimchi/model/host.py | 4 +- src/kimchi/model/storagepools.py | 2 +- src/kimchi/model/storagevolumes.py | 12 +++--- src/kimchi/model/vms.py | 33 +++++++++++++--- src/kimchi/model/vmsnapshots.py | 5 ++- tests/test_authorization.py | 23 +++++++----- tests/test_mockmodel.py | 12 ++++-- tests/test_model.py | 60 +++++++++++++++++++---------- tests/test_model_storagevolume.py | 2 +- tests/test_rest.py | 77 ++++++++++++++++++++++++++++---------- ui/js/src/kimchi.guest_main.js | 4 +- 15 files changed, 188 insertions(+), 77 deletions(-) -- 2.1.0

You can filter tasks by their state (running) and/or a target_uri. If you want to see all cloning VM tasks, you can only query for running tasks with /vm in the target_uri. This patch adds an action keyword to the target_uris for existing tasks. This will make it possible to query other types of tasks for the same resource. Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- docs/API.md | 16 ++++++++++++++++ src/kimchi/mockmodel.py | 7 ++++--- src/kimchi/model/debugreports.py | 4 ++-- src/kimchi/model/host.py | 4 ++-- src/kimchi/model/storagepools.py | 2 +- src/kimchi/model/storagevolumes.py | 12 +++++++----- src/kimchi/model/vms.py | 5 ++--- src/kimchi/model/vmsnapshots.py | 5 +++-- 8 files changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3f7925f..1f8e769 100644 --- a/docs/API.md +++ b/docs/API.md @@ -651,6 +651,22 @@ server. * failed: The task failed * message: Human-readable details about the Task status * target_uri: Resource URI related to the Task + The target uri may end with an action descriptor: + * VM task descriptions: + * clone + * create + * VM Snapshot description + * create + * Storage descriptions: + * create + * clone + * Pool descriptions: + * create + * scan + * Debugreport descriptions: + * create + * Host SW Update descriptions: + * update * **POST**: *See Task Actions* **Actions (POST):** diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 413ac5d..2b1b214 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -244,7 +244,7 @@ def _get_volume_path(self, pool, vol): return MockModel._libvirt_get_vol_path(pool, vol) def _gen_debugreport_file(self, name): - return add_task('/debugreports/%s' % name, self._create_log, + return add_task('/debugreports/%s/create' % name, self._create_log, self.objstore, name) def _create_log(self, cb, name): @@ -333,7 +333,8 @@ def _mock_packageupdate_lookup(self, pkg_name): return self._mock_swupdate.pkgs[pkg_name] def _mock_host_swupdate(self, args=None): - task_id = add_task('/host/swupdate', self._mock_swupdate.doUpdate, + task_id = add_task('/host/swupdate/update', + self._mock_swupdate.doUpdate, self.objstore) return self.task_lookup(task_id) @@ -385,7 +386,7 @@ def _mock_vm_clone(self, name): def _mock_vmsnapshots_create(self, vm_name, params): name = params.get('name', unicode(int(time.time()))) params = {'vm_name': vm_name, 'name': name} - taskid = add_task(u'/vms/%s/snapshots/%s' % (vm_name, name), + taskid = add_task(u'/vms/%s/snapshots/%s/create' % (vm_name, name), self._vmsnapshots_create_task, self.objstore, params) return self.task_lookup(taskid) diff --git a/src/kimchi/model/debugreports.py b/src/kimchi/model/debugreports.py index 5f74da8..19b4de5 100644 --- a/src/kimchi/model/debugreports.py +++ b/src/kimchi/model/debugreports.py @@ -62,8 +62,8 @@ def _gen_debugreport_file(self, name): gen_cmd = self.get_system_report_tool() if gen_cmd is not None: - return add_task('/debugreports/%s' % name, gen_cmd, self.objstore, - name) + return add_task('/debugreports/%s/create' % name, gen_cmd, + self.objstore, name) raise OperationFailed("KCHDR0002E") diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 4419bb3..f429415 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -115,8 +115,8 @@ def swupdate(self, *name): raise OperationFailed('KCHPKGUPD0001E') kimchi_log.debug('Host is going to be updated.') - taskid = add_task('/host/swupdate', swupdate.doUpdate, self.objstore, - None) + taskid = add_task('/host/swupdate/update', swupdate.doUpdate, + self.objstore, None) return self.task.lookup(taskid) def shutdown(self, args=None): diff --git a/src/kimchi/model/storagepools.py b/src/kimchi/model/storagepools.py index b85f3b4..094c841 100644 --- a/src/kimchi/model/storagepools.py +++ b/src/kimchi/model/storagepools.py @@ -167,7 +167,7 @@ def _do_deep_scan(self, params): params['path'] = self.scanner.scan_dir_prepare(params['name']) scan_params['pool_path'] = params['path'] - task_id = add_task('/storagepools/%s' % ISO_POOL_NAME, + task_id = add_task('/storagepools/%s/scan' % ISO_POOL_NAME, self.scanner.start_scan, self.objstore, scan_params) # Record scanning-task/storagepool mapping for future querying try: diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py index 0480496..9f93f10 100644 --- a/src/kimchi/model/storagevolumes.py +++ b/src/kimchi/model/storagevolumes.py @@ -116,8 +116,10 @@ def create(self, pool_name, params): raise InvalidParameter('KCHVOL0001E', {'name': name}) params['pool'] = pool_name - targeturi = '/storagepools/%s/storagevolumes/%s' % (pool_name, name) - taskid = add_task(targeturi, create_func, self.objstore, params) + targeturi = '/storagepools/%s/storagevolumes/%s/create' \ + % (pool_name, name) + taskid = add_task(targeturi, create_func, self.objstore, + params) return self.task.lookup(taskid) def _create_volume_with_file(self, cb, params): @@ -413,9 +415,9 @@ def clone(self, pool, name, new_pool=None, new_name=None): 'name': name, 'new_pool': new_pool, 'new_name': new_name} - taskid = add_task(u'/storagepools/%s/storagevolumes/%s' % - (pool, new_name), self._clone_task, self.objstore, - params) + taskid = add_task(u'/storagepools/%s/storagevolumes/%s/clone' % + (pool, new_name), self._clone_task, + self.objstore, params) return self.task.lookup(taskid) def _clone_task(self, cb, params): diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 4c5f443..1ae841d 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -332,9 +332,8 @@ def clone(self, name): new_name = get_next_clone_name(current_vm_names, name) # create a task with the actual clone function - taskid = add_task(u'/vms/%s' % new_name, self._clone_task, - self.objstore, - {'name': name, 'new_name': new_name}) + taskid = add_task(u'/vms/%s/clone' % new_name, self._clone_task, + self.objstore, {'name': name, 'new_name': new_name}) return self.task.lookup(taskid) diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index 3a92cdc..c4cc663 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -72,8 +72,9 @@ def create(self, vm_name, params={}): name = params.get('name', unicode(int(time.time()))) task_params = {'vm_name': vm_name, 'name': name} - taskid = add_task(u'/vms/%s/snapshots/%s' % (vm_name, name), - self._create_task, self.objstore, task_params) + taskid = add_task(u'/vms/%s/snapshots/%s/create' % (vm_name, name), + self._create_task, self.objstore, + task_params) return self.task.lookup(taskid) def _create_task(self, cb, params): -- 2.1.0

Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- tests/test_model.py | 6 +++--- tests/test_model_storagevolume.py | 2 +- tests/test_rest.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index f80f1c9..cf55549 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -160,7 +160,7 @@ def test_vm_lifecycle(self): self.assertEquals(snap, current_snap) task = inst.vmsnapshots_create(u'kimchi-vm') - snap_name = task['target_uri'].split('/')[-1] + snap_name = task['target_uri'].split('/')[-2] rollback.prependDefer(inst.vmsnapshot_delete, u'kimchi-vm', snap_name) inst.task_wait(task['id']) @@ -1142,9 +1142,9 @@ def test_vm_clone(self): # and make sure both of them complete successfully task1 = inst.vm_clone(name) task2 = inst.vm_clone(name) - clone1_name = task1['target_uri'].split('/')[-1] + clone1_name = task1['target_uri'].split('/')[-2] rollback.prependDefer(inst.vm_delete, clone1_name) - clone2_name = task2['target_uri'].split('/')[-1] + clone2_name = task2['target_uri'].split('/')[-2] rollback.prependDefer(inst.vm_delete, clone2_name) inst.task_wait(task1['id']) task1 = inst.task_lookup(task1['id']) diff --git a/tests/test_model_storagevolume.py b/tests/test_model_storagevolume.py index a3c3ce3..11fd90d 100644 --- a/tests/test_model_storagevolume.py +++ b/tests/test_model_storagevolume.py @@ -124,7 +124,7 @@ def _task_lookup(taskid): resp = self.request(vol_uri + '/clone', '{}', 'POST') self.assertEquals(202, resp.status) task = json.loads(resp.read()) - cloned_vol_name = task['target_uri'].split('/')[-1] + cloned_vol_name = task['target_uri'].split('/')[-2] rollback.prependDefer(model.storagevolume_delete, pool_name, cloned_vol_name) wait_task(_task_lookup, task['id']) diff --git a/tests/test_rest.py b/tests/test_rest.py index 812afb7..c759a52 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -370,7 +370,7 @@ def test_vm_lifecycle(self): wait_task(self._task_lookup, task['id']) task = json.loads(self.request('/tasks/%s' % task['id'], '{}').read()) self.assertEquals('finished', task['status']) - clone_vm_name = task['target_uri'].split('/')[-1] + clone_vm_name = task['target_uri'].split('/')[-2] self.assertTrue(re.match(u'test-vm-clone-\d+', clone_vm_name)) resp = self.request('/vms/test-vm', '{}') @@ -423,7 +423,7 @@ def test_vm_lifecycle(self): resp = self.request('/vms/test-vm/snapshots', '{}', 'POST') self.assertEquals(202, resp.status) task = json.loads(resp.read()) - snap_name = task['target_uri'].split('/')[-1] + snap_name = task['target_uri'].split('/')[-2] wait_task(self._task_lookup, task['id']) resp = self.request('/tasks/%s' % task['id'], '{}', 'GET') task = json.loads(resp.read()) @@ -1724,7 +1724,7 @@ def test_upload(self): task = r.json() wait_task(self._task_lookup, task['id'], 15) uri = '/storagepools/default-pool/storagevolumes/%s' - resp = self.request(uri % task['target_uri'].split('/')[-1]) + resp = self.request(uri % task['target_uri'].split('/')[-2]) self.assertEquals(200, resp.status) -- 2.1.0

Update the clone tasks GET call. Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- ui/js/src/kimchi.guest_main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/src/kimchi.guest_main.js b/ui/js/src/kimchi.guest_main.js index 21caf1b..e21b51f 100644 --- a/ui/js/src/kimchi.guest_main.js +++ b/ui/js/src/kimchi.guest_main.js @@ -203,10 +203,10 @@ kimchi.listVmsAuto = function() { } var getCloningGuests = function(){ var guests = []; - kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/*'), function(tasks) { + kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/.+/clone'), function(tasks) { for(var i=0;i<tasks.length;i++){ var guestUri = tasks[i].target_uri; - var guestName = guestUri.substring(guestUri.lastIndexOf('/')+1, guestUri.length); + var guestName = guestUri.split('/')[2] guests.push($.extend({}, kimchi.sampleGuestObject, {name: guestName, isCloning: true})); if(kimchi.trackingTasks.indexOf(tasks[i].id)==-1) kimchi.trackTask(tasks[i].id, null, function(err){ -- 2.1.0

Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- src/kimchi/control/vms.py | 4 ++-- src/kimchi/model/vms.py | 28 +++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index 5068b7c..8a31dc0 100644 --- a/src/kimchi/control/vms.py +++ b/src/kimchi/control/vms.py @@ -17,13 +17,13 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from kimchi.control.base import Collection, Resource +from kimchi.control.base import AsyncCollection, Resource from kimchi.control.utils import internal_redirect, UrlSubNode from kimchi.control.vm import sub_nodes @UrlSubNode('vms', True) -class VMs(Collection): +class VMs(AsyncCollection): def __init__(self, model): super(VMs, self).__init__(model) self.resource = VM diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 1ae841d..e5b0aa7 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -80,6 +80,7 @@ def __init__(self, **kargs): self.conn = kargs['conn'] self.objstore = kargs['objstore'] self.caps = CapabilitiesModel(**kargs) + self.task = TaskModel(**kargs) @staticmethod def _update_guests_stats(names, conn): @@ -183,7 +184,6 @@ def _get_disk_io_rate(vm_uuid, dom, seconds): 'diskWrKB': diskWrKB}) def create(self, params): - conn = self.conn.get() t_name = template_name_from_uri(params['template']) vm_uuid = str(uuid.uuid4()) vm_list = self.get_list() @@ -205,6 +205,26 @@ def create(self, params): t.validate() + taskid = add_task(u'/vms/%s/create' % name, + self._create_task, self.objstore, + {'vm_uuid': vm_uuid, 'template': t, 'name': name}) + + return self.task.lookup(taskid) + + def _create_task(self, cb, params): + """ + params: A dict with the following values: + - vm_uuid: The UUID of the VM being created + - template: The template being used to create the VM + - name: The name for the new VM + """ + + vm_uuid = params['vm_uuid'] + t = params['template'] + name = params['name'] + conn = self.conn.get() + + cb('Storing VM icon') # Store the icon for displaying later icon = t.info.get('icon') if icon: @@ -219,6 +239,7 @@ def create(self, params): # If storagepool is SCSI, volumes will be LUNs and must be passed by # the user from UI or manually. + cb('Provisioning storage for new VM') vol_list = [] if t._get_storage_type() not in ["iscsi", "scsi"]: vol_list = t.fork_vm_storage(vm_uuid) @@ -231,6 +252,7 @@ def create(self, params): graphics=graphics, volumes=vol_list) + cb('Defining new VM') try: conn.defineXML(xml.encode('utf-8')) except libvirt.libvirtError as e: @@ -241,10 +263,10 @@ def create(self, params): raise OperationFailed("KCHVM0007E", {'name': name, 'err': e.get_error_message()}) + cb('Updating VM metadata') VMModel.vm_update_os_metadata(VMModel.get_vm(name, self.conn), t.info, self.caps.metadata_support) - - return name + cb('OK', True) def get_list(self): return VMsModel.get_vms(self.conn) -- 2.1.0

Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- tests/test_authorization.py | 23 +++++++++------ tests/test_mockmodel.py | 12 ++++++-- tests/test_model.py | 54 ++++++++++++++++++++++++---------- tests/test_rest.py | 71 ++++++++++++++++++++++++++++++++++----------- 4 files changed, 115 insertions(+), 45 deletions(-) diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 4fcc496..eae837c 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -26,7 +26,7 @@ import kimchi.mockmodel from iso_gen import construct_fake_iso from utils import get_free_port, patch_auth, request -from utils import run_server +from utils import run_server, wait_task test_server = None @@ -118,19 +118,24 @@ def test_nonroot_access(self): # Non-root users can only get vms authorized to them model.templates_create({'name': u'test', 'cdrom': fake_iso}) - model.vms_create({'name': u'test-me', 'template': '/templates/test'}) + task_info = model.vms_create({'name': u'test-me', + 'template': '/templates/test'}) + wait_task(model.task_lookup, task_info['id']) + model.vm_update(u'test-me', {'users': [kimchi.mockmodel.fake_user.keys()[0]], 'groups': []}) - model.vms_create({'name': u'test-usera', - 'template': '/templates/test'}) + task_info = model.vms_create({'name': u'test-usera', + 'template': '/templates/test'}) + wait_task(model.task_lookup, task_info['id']) non_root = list(set(model.users_get_list()) - set(['root']))[0] model.vm_update(u'test-usera', {'users': [non_root], 'groups': []}) - model.vms_create({'name': u'test-groupa', - 'template': '/templates/test'}) + task_info = model.vms_create({'name': u'test-groupa', + 'template': '/templates/test'}) + wait_task(model.task_lookup, task_info['id']) a_group = model.groups_get_list()[0] model.vm_update(u'test-groupa', {'groups': [a_group]}) @@ -143,9 +148,9 @@ def test_nonroot_access(self): self.assertEquals(403, resp.status) # Create a vm using mockmodel directly to test Resource access - model.vms_create({'name': 'kimchi-test', - 'template': '/templates/test'}) - + task_info = model.vms_create({'name': 'kimchi-test', + 'template': '/templates/test'}) + wait_task(model.task_lookup, task_info['id']) resp = self.request('/vms/kimchi-test', '{}', 'PUT') self.assertEquals(403, resp.status) resp = self.request('/vms/kimchi-test', '{}', 'DELETE') diff --git a/tests/test_mockmodel.py b/tests/test_mockmodel.py index 29354aa..443df42 100644 --- a/tests/test_mockmodel.py +++ b/tests/test_mockmodel.py @@ -169,7 +169,9 @@ def test_screenshot_refresh(self): req = json.dumps({'name': 'test', 'cdrom': fake_iso}) request(host, ssl_port, '/templates', req, 'POST') req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) - request(host, ssl_port, '/vms', req, 'POST') + resp = request(host, ssl_port, '/vms', req, 'POST') + task = json.loads(resp.read()) + wait_task(model.task_lookup, task['id']) # Test screenshot refresh for running vm request(host, ssl_port, '/vms/test-vm/start', '{}', 'POST') @@ -197,7 +199,9 @@ def test_vm_list_sorted(self): def add_vm(name): # Create a VM req = json.dumps({'name': name, 'template': '/templates/test'}) - request(host, ssl_port, '/vms', req, 'POST') + task = json.loads(request(host, ssl_port, '/vms', req, + 'POST').read()) + wait_task(model.task_lookup, task['id']) vms = [u'abc', u'bca', u'cab', u'xba'] for vm in vms: @@ -209,7 +213,9 @@ def add_vm(name): def test_vm_info(self): model.templates_create({'name': u'test', 'cdrom': fake_iso}) - model.vms_create({'name': u'test-vm', 'template': '/templates/test'}) + task = model.vms_create({'name': u'test-vm', + 'template': '/templates/test'}) + wait_task(model.task_lookup, task['id']) vms = model.vms_get_list() self.assertEquals(2, len(vms)) self.assertIn(u'test-vm', vms) diff --git a/tests/test_model.py b/tests/test_model.py index cf55549..69fd01a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -117,8 +117,11 @@ def test_vm_lifecycle(self): rollback.prependDefer(inst.template_delete, 'test') params = {'name': 'kimchi-vm', 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) rollback.prependDefer(inst.vm_delete, 'kimchi-vm') + inst.task_wait(task['id'], 10) + task = inst.task_lookup(task['id']) + self.assertEquals('finished', task['status']) vms = inst.vms_get_list() self.assertTrue('kimchi-vm' in vms) @@ -235,7 +238,8 @@ def test_image_based_template(self): session.store('template', tmpl_name, tmpl_info) params = {'name': 'kimchi-vm', 'template': '/templates/img-tmpl'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-vm') vms = inst.vms_get_list() @@ -254,7 +258,8 @@ def test_vm_graphics(self): inst.templates_create(params) with RollbackContext() as rollback: params = {'name': 'kimchi-vnc', 'template': '/templates/test'} - inst.vms_create(params) + task1 = inst.vms_create(params) + inst.task_wait(task1['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-vnc') info = inst.vm_lookup('kimchi-vnc') @@ -264,7 +269,8 @@ def test_vm_graphics(self): graphics = {'type': 'spice', 'listen': '127.0.0.1'} params = {'name': 'kimchi-spice', 'template': '/templates/test', 'graphics': graphics} - inst.vms_create(params) + task2 = inst.vms_create(params) + inst.task_wait(task2['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-spice') info = inst.vm_lookup('kimchi-spice') @@ -281,7 +287,8 @@ def test_vm_ifaces(self): inst.templates_create(params) rollback.prependDefer(inst.template_delete, 'test') params = {'name': 'kimchi-ifaces', 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-ifaces') # Create a network @@ -387,7 +394,8 @@ def _attach_disk(expect_bus='virtio'): inst.templates_create(params) rollback.prependDefer(inst.template_delete, 'test') params = {'name': vm_name, 'template': '/templates/test'} - inst.vms_create(params) + task1 = inst.vms_create(params) + inst.task_wait(task1['id']) rollback.prependDefer(inst.vm_delete, vm_name) prev_count = len(inst.vmstorages_get_list(vm_name)) @@ -430,7 +438,8 @@ def _attach_disk(expect_bus='virtio'): rollback.prependDefer(inst.template_delete, 'old_distro_template') params = {'name': vm_name, 'template': '/templates/old_distro_template'} - inst.vms_create(params) + task2 = inst.vms_create(params) + inst.task_wait(task2['id']) rollback.prependDefer(inst.vm_delete, vm_name) # Attach will choose IDE bus for old distro @@ -451,7 +460,8 @@ def test_vm_cdrom(self): inst.templates_create(params) rollback.prependDefer(inst.template_delete, 'test') params = {'name': vm_name, 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, vm_name) prev_count = len(inst.vmstorages_get_list(vm_name)) @@ -549,7 +559,8 @@ def test_vm_storage_provisioning(self): rollback.prependDefer(inst.template_delete, 'test') params = {'name': 'test-vm-1', 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'test-vm-1') vm_info = inst.vm_lookup(params['name']) @@ -601,12 +612,14 @@ def test_template_storage_customise(self): inst.template_update('test', params) params = {'name': 'test-vm-1', 'template': '/templates/test'} + # @TODO: What is invalid here? Rewrite test for that. self.assertRaises(InvalidParameter, inst.vms_create, params) inst.storagepool_activate(pool) rollback.prependDefer(inst.storagepool_deactivate, pool) - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'test-vm-1') vm_info = inst.vm_lookup(params['name']) disk_path = '/tmp/kimchi-images/%s-0.img' % vm_info['uuid'] @@ -789,7 +802,10 @@ def test_template_update(self): 'new-test', params) params = {'name': 'some-vm', 'template': '/templates/new-test'} - self.assertEquals('some-vm', inst.vms_create(params)) + task = inst.vms_create(params) + inst.task_wait(task['id']) + vm_name = task['target_uri'].split('/')[-1] + self.assertEquals('some-vm', vm_name) rollback.prependDefer(inst.vm_delete, 'some-vm') iface_args = {'type': 'network', 'network': u'kīмсhī-пet'} @@ -808,10 +824,12 @@ def test_vm_edit(self): with RollbackContext() as rollback: params_1 = {'name': 'kimchi-vm1', 'template': '/templates/test'} params_2 = {'name': 'kimchi-vm2', 'template': '/templates/test'} - inst.vms_create(params_1) + task1 = inst.vms_create(params_1) + inst.task_wait(task1['id']) rollback.prependDefer(utils.rollback_wrapper, inst.vm_delete, 'kimchi-vm1') - inst.vms_create(params_2) + task2 = inst.vms_create(params_2) + inst.task_wait(task2['id']) rollback.prependDefer(utils.rollback_wrapper, inst.vm_delete, 'kimchi-vm2') @@ -1091,11 +1109,13 @@ def test_delete_running_vm(self): rollback.prependDefer(inst.template_delete, 'test') params = {'name': u'kīмсhī-∨м', 'template': u'/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(utils.rollback_wrapper, inst.vm_delete, u'kīмсhī-∨м') inst.vm_start(u'kīмсhī-∨м') + self.assertEquals(inst.vm_lookup(u'kīмсhī-∨м')['state'], 'running') rollback.prependDefer(utils.rollback_wrapper, inst.vm_poweroff, u'kīмсhī-∨м') @@ -1114,7 +1134,8 @@ def test_vm_list_sorted(self): rollback.prependDefer(inst.template_delete, 'test') params = {'name': 'kimchi-vm', 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-vm') vms = inst.vms_get_list() @@ -1186,7 +1207,8 @@ def test_use_test_host(self): params = {'name': 'kimchi-vm', 'template': '/templates/test'} - inst.vms_create(params) + task = inst.vms_create(params) + inst.task_wait(task['id']) rollback.prependDefer(inst.vm_delete, 'kimchi-vm') vms = inst.vms_get_list() diff --git a/tests/test_rest.py b/tests/test_rest.py index c759a52..f46bd56 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -190,7 +190,9 @@ def test_get_vms(self): req = json.dumps({'name': name, 'template': '/templates/test', 'users': test_users, 'groups': test_groups}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) vms = json.loads(self.request('/vms').read()) self.assertEquals(11, len(vms)) @@ -208,7 +210,9 @@ def test_edit_vm(self): req = json.dumps({'name': 'vm-1', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) vm = json.loads(self.request('/vms/vm-1').read()) self.assertEquals('vm-1', vm['name']) @@ -325,7 +329,9 @@ def test_vm_lifecycle(self): # Create a VM req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + self.assertEquals(202, resp.status) # Verify the VM vm = json.loads(self.request('/vms/test-vm').read()) @@ -479,7 +485,9 @@ def test_vm_graphics(self): # Create a VM with default args req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Verify the VM vm = json.loads(self.request('/vms/test-vm').read()) self.assertEquals('127.0.0.1', vm['graphics']['listen']) @@ -493,7 +501,9 @@ def test_vm_graphics(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test', 'graphics': graphics}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Verify the VM vm = json.loads(self.request('/vms/test-vm').read()) self.assertEquals('127.0.0.1', vm['graphics']['listen']) @@ -507,7 +517,9 @@ def test_vm_graphics(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test', 'graphics': graphics}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Verify the VM vm = json.loads(self.request('/vms/test-vm').read()) self.assertEquals('fe00::0', vm['graphics']['listen']) @@ -521,7 +533,9 @@ def test_vm_graphics(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test', 'graphics': graphics}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Verify the VM vm = json.loads(self.request('/vms/test-vm').read()) self.assertEquals('127.0.0.1', vm['graphics']['listen']) @@ -563,7 +577,9 @@ def test_vm_storage_devices(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Delete the VM rollback.prependDefer(self.request, '/vms/test-vm', '{}', 'DELETE') @@ -708,7 +724,9 @@ def test_vm_iface(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Delete the VM rollback.prependDefer(self.request, '/vms/test-vm', '{}', 'DELETE') @@ -782,7 +800,10 @@ def test_vm_customise_storage(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test', 'storagepool': '/storagepools/alt'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + resp = self.request('/vms/test-vm', {}, 'GET') vm_info = json.loads(resp.read()) # Test template not changed after vm customise its pool @@ -835,7 +856,9 @@ def test_scsi_fc_storage(self): req = json.dumps({'name': 'test-vm', 'template': '/templates/test_fc_pool'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Start the VM resp = self.request('/vms/test-vm/start', '{}', 'POST') @@ -886,8 +909,11 @@ def test_template_customise_storage(self): # Create a VM req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + resp = self.request('/vms/test-vm', {}, 'GET') vm = json.loads(resp.read()) - self.assertEquals(201, resp.status) + self.assertEquals(200, resp.status) # Verify the volume was created vol_uri = '/storagepools/alt/storagevolumes/%s-0.img' % vm['uuid'] @@ -969,8 +995,10 @@ def test_unnamed_vms(self): # Create 5 unnamed vms from this template for i in xrange(1, 6): req = json.dumps({'template': '/templates/test'}) - vm = json.loads(self.request('/vms', req, 'POST').read()) - self.assertEquals('test-vm-%i' % i, vm['name']) + task = json.loads(self.request('/vms', req, 'POST').read()) + wait_task(self._task_lookup, task['id']) + resp = self.request('/vms/test-vm-%i' % i, {}, 'GET') + self.assertEquals(resp.status, 200) count = len(json.loads(self.request('/vms').read())) self.assertEquals(6, count) @@ -1002,7 +1030,10 @@ def test_create_vm_with_img_based_template(self): self.assertEquals(201, resp.status) req = json.dumps({'template': '/templates/test'}) - json.loads(self.request('/vms', req, 'POST').read()) + resp = self.request('/vms', req, 'POST') + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Test storage volume created with backing store of base file resp = json.loads( @@ -1318,6 +1349,8 @@ def test_screenshot_refresh(self): resp = self.request('/templates', req, 'POST') req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) # Test screenshot for shut-off state vm resp = self.request('/vms/test-vm/screenshot') @@ -1647,10 +1680,14 @@ def test_get_param(self): # Create a VM req = json.dumps({'name': 'test-vm1', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + self.assertEquals(202, resp.status) req = json.dumps({'name': 'test-vm2', 'template': '/templates/test'}) resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) resp = request(host, ssl_port, '/vms') self.assertEquals(200, resp.status) -- 2.1.0

ping On 03/04/2015 03:50 PM, Christy Perez wrote:
v3 Changes: Instead of adding a description key to the task, just append the action desciptor to the target_uri.
If a guest has a large disk, and uses a filesystem that requires preallocation, it can take several minutes to create a VM. During that time, kimchi is tied up by the VM creation.
This patch changes the VMs Collection to be an AsyncCollection.
Another change required for this was to create a more granular way to query tasks. Currently it is not possible (using the API) to query tasks for the same collection or resource type that may have different operations. For example, VM cloning is also an asynchronous operation. For the guests tab, the UI was querying all running tasks and displaying them with the Cloning label. This picked up VMs that were being created as well. For more information about how the tasks can be queried, see the updated API doc and the UI change.
Christy Perez (5): Granular Task Queries: Backend Granular task query test updates Granular Task Queries: UI Create guests asynchronously: Backend Async vm creation test updates
docs/API.md | 16 ++++++++ src/kimchi/control/vms.py | 4 +- src/kimchi/mockmodel.py | 7 ++-- src/kimchi/model/debugreports.py | 4 +- src/kimchi/model/host.py | 4 +- src/kimchi/model/storagepools.py | 2 +- src/kimchi/model/storagevolumes.py | 12 +++--- src/kimchi/model/vms.py | 33 +++++++++++++--- src/kimchi/model/vmsnapshots.py | 5 ++- tests/test_authorization.py | 23 +++++++----- tests/test_mockmodel.py | 12 ++++-- tests/test_model.py | 60 +++++++++++++++++++---------- tests/test_model_storagevolume.py | 2 +- tests/test_rest.py | 77 ++++++++++++++++++++++++++++---------- ui/js/src/kimchi.guest_main.js | 4 +- 15 files changed, 188 insertions(+), 77 deletions(-)
participants (1)
-
Christy Perez