[PATCH 0/6 v7] Asyn VM creation

V6 -> V7: - Update test cases - Update UI to reuse the clone box for pending VM creation Aline Manera (1): Create VMs Asynchronously: UI Christy Perez (5): Append clone to target_uri for vm clone task Tests for new clone target_uri UI changes for new clone target_uri Create VMs asynchronously: Backend Create VMs Asynchronously: Tests src/kimchi/control/vms.py | 4 +-- src/kimchi/model/vms.py | 33 ++++++++++++++++++----- tests/test_authorization.py | 25 +++++++++++------- tests/test_mockmodel.py | 14 +++++++--- tests/test_model.py | 49 +++++++++++++++++++++++----------- tests/test_rest.py | 60 +++++++++++++++++++++++++++++++----------- tests/test_server.py | 26 ++++++++---------- ui/css/theme-default/list.css | 8 +++--- ui/js/src/kimchi.guest_main.js | 32 ++++++++++++++++++---- ui/pages/guest.html.tmpl | 4 +-- ui/pages/i18n.json.tmpl | 1 + 11 files changed, 176 insertions(+), 80 deletions(-) -- 2.1.0

From: Christy Perez <christy@linux.vnet.ibm.com> So that we can differentiate between clone and create tasks for VMs. Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a6ca27b..5f8c3da 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -229,9 +229,8 @@ class VMModel(object): 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) -- 2.1.0

From: Christy Perez <christy@linux.vnet.ibm.com> Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- tests/test_model.py | 4 ++-- tests/test_rest.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index bd195b5..ad0dccd 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -829,9 +829,9 @@ class ModelTests(unittest.TestCase): # 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_rest.py b/tests/test_rest.py index 686d54c..65c3db5 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -292,7 +292,7 @@ class RestTests(unittest.TestCase): 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', '{}') -- 2.1.0

From: Christy Perez <christy@linux.vnet.ibm.com> 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 b66177c..c712bb4 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

From: Christy Perez <christy@linux.vnet.ibm.com> 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, 26 insertions(+), 6 deletions(-) diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index 6352a26..a40b56e 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 5f8c3da..f0182b9 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -79,11 +79,10 @@ class VMsModel(object): self.conn = kargs['conn'] self.objstore = kargs['objstore'] self.caps = CapabilitiesModel(**kargs) + self.task = TaskModel(**kargs) 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() name = get_vm_name(params.get('name'), t_name, vm_list) # incoming text, from js json, is unicode, do not need decode @@ -102,7 +101,26 @@ class VMsModel(object): raise InvalidOperation("KCHVM0005E") t.validate() + data = {'name': name, 'template': t, + 'graphics': params.get('graphics', {})} + taskid = add_task(u'/vms/%s' % name, self._create_task, + self.objstore, data) + 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 = str(uuid.uuid4()) + 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: @@ -117,6 +135,7 @@ class VMsModel(object): # 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) @@ -128,6 +147,7 @@ class VMsModel(object): graphics=graphics, volumes=vol_list) + cb('Defining new VM') try: conn.defineXML(xml.encode('utf-8')) except libvirt.libvirtError as e: @@ -138,10 +158,10 @@ class VMsModel(object): 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

From: Christy Perez <christy@linux.vnet.ibm.com> Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- tests/test_authorization.py | 25 +++++++++++-------- tests/test_mockmodel.py | 14 +++++++---- tests/test_model.py | 45 ++++++++++++++++++++++++----------- tests/test_rest.py | 58 ++++++++++++++++++++++++++++++++++----------- tests/test_server.py | 26 +++++++++----------- 5 files changed, 111 insertions(+), 57 deletions(-) diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 4fcc496..959dcf8 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -1,7 +1,7 @@ # # Project Kimchi # -# Copyright IBM, Corp. 2014 +# Copyright IBM, Corp. 2014-2015 # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -26,7 +26,7 @@ from functools import partial 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 @@ class AuthorizationTests(unittest.TestCase): # 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 @@ class AuthorizationTests(unittest.TestCase): 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 aa48dd1..52972f0 100644 --- a/tests/test_mockmodel.py +++ b/tests/test_mockmodel.py @@ -25,7 +25,7 @@ import unittest import kimchi.mockmodel -from utils import get_free_port, patch_auth, request, run_server +from utils import get_free_port, patch_auth, request, run_server, wait_task from kimchi.osinfo import get_template_default @@ -66,7 +66,9 @@ class MockModelTests(unittest.TestCase): 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') @@ -94,7 +96,9 @@ class MockModelTests(unittest.TestCase): 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: @@ -106,7 +110,9 @@ class MockModelTests(unittest.TestCase): 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 ad0dccd..88c020e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -129,8 +129,11 @@ class ModelTests(unittest.TestCase): 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) @@ -267,7 +270,8 @@ class ModelTests(unittest.TestCase): 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() @@ -286,7 +290,8 @@ class ModelTests(unittest.TestCase): 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') @@ -296,7 +301,8 @@ class ModelTests(unittest.TestCase): 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') @@ -325,7 +331,8 @@ class ModelTests(unittest.TestCase): for vm_name in ['kimchi-ifaces', 'kimchi-ifaces-running']: 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) ifaces = inst.vmifaces_get_list(vm_name) @@ -422,7 +429,8 @@ class ModelTests(unittest.TestCase): 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)) @@ -465,7 +473,8 @@ class ModelTests(unittest.TestCase): 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) # Need to check the right disk_bus for old distro @@ -486,7 +495,8 @@ class ModelTests(unittest.TestCase): 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)) @@ -575,7 +585,8 @@ class ModelTests(unittest.TestCase): 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']) @@ -596,10 +607,12 @@ class ModelTests(unittest.TestCase): 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') @@ -778,11 +791,13 @@ class ModelTests(unittest.TestCase): 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ī-∨м') @@ -801,7 +816,8 @@ class ModelTests(unittest.TestCase): 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() @@ -873,7 +889,8 @@ class ModelTests(unittest.TestCase): 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 65c3db5..8826424 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -112,7 +112,9 @@ class RestTests(unittest.TestCase): 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)) @@ -130,7 +132,9 @@ class RestTests(unittest.TestCase): 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']) @@ -247,7 +251,9 @@ class RestTests(unittest.TestCase): # 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()) @@ -429,7 +435,9 @@ class RestTests(unittest.TestCase): # 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']) @@ -443,7 +451,9 @@ class RestTests(unittest.TestCase): 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']) @@ -457,7 +467,9 @@ class RestTests(unittest.TestCase): 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']) @@ -471,7 +483,9 @@ class RestTests(unittest.TestCase): 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']) @@ -513,7 +527,9 @@ class RestTests(unittest.TestCase): 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') @@ -658,7 +674,9 @@ class RestTests(unittest.TestCase): 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') @@ -738,7 +756,10 @@ class RestTests(unittest.TestCase): 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 @@ -791,7 +812,9 @@ class RestTests(unittest.TestCase): 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') @@ -816,8 +839,10 @@ class RestTests(unittest.TestCase): # 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) @@ -849,7 +874,10 @@ class RestTests(unittest.TestCase): 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( @@ -939,6 +967,8 @@ class RestTests(unittest.TestCase): 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') diff --git a/tests/test_server.py b/tests/test_server.py index 42bebbe..a4d31d0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -263,29 +263,25 @@ class ServerTests(unittest.TestCase): mockiso = '/tmp/mock.iso' open('/tmp/mock.iso', 'w').close() - req = json.dumps({'name': 'test', 'cdrom': mockiso}) + # Create 2 different templates + req = json.dumps({'name': 'test-tmpl1', 'cdrom': mockiso}) self.request('/templates', req, 'POST') - # Create a VM - req = json.dumps({'name': 'test-vm1', 'template': '/templates/test'}) - resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) - req = json.dumps({'name': 'test-vm2', 'template': '/templates/test'}) - resp = self.request('/vms', req, 'POST') - self.assertEquals(201, resp.status) + req = json.dumps({'name': 'test-tmpl2', 'cdrom': mockiso}) + self.request('/templates', req, 'POST') # Remove mock iso os.unlink(mockiso) - resp = self.request('/vms') + # Get the templates + resp = self.request('/templates') self.assertEquals(200, resp.status) res = json.loads(resp.read()) - self.assertEquals(3, len(res)) + self.assertEquals(2, len(res)) - # FIXME: control/base.py also allows filter by regex so it is returning - # 2 vms when querying for 'test-vm1': 'test' and 'test-vm1' - resp = self.request('/vms?name=test-vm1') + # Get a specific template + resp = self.request('/templates?name=test-tmpl1') self.assertEquals(200, resp.status) res = json.loads(resp.read()) - self.assertEquals(2, len(res)) - self.assertIn('test-vm1', [r['name'] for r in res]) + self.assertEquals(1, len(res)) + self.assertEquals('test-tmpl1', res[0]['name']) -- 2.1.0

Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> Signed-off-by: Aline Manera <alinefm@linux.vnet.ibm.com> --- ui/css/theme-default/list.css | 8 ++++---- ui/js/src/kimchi.guest_main.js | 28 +++++++++++++++++++++++++--- ui/pages/guest.html.tmpl | 4 ++-- ui/pages/i18n.json.tmpl | 1 + 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/ui/css/theme-default/list.css b/ui/css/theme-default/list.css index 7b32ea6..e51912d 100644 --- a/ui/css/theme-default/list.css +++ b/ui/css/theme-default/list.css @@ -1,7 +1,7 @@ /* * Project Kimchi * - * Copyright IBM, Corp. 2013-2014 + * Copyright IBM, Corp. 2013-2015 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -289,11 +289,11 @@ padding-left: 10px; } -.guest-clone { +.guest-pending { margin: 10px; } -.guest-clone .icon { +.guest-pending .icon { background: url('../../images/theme-default/kimchi-loading15x15.gif') no-repeat; display: inline-block; width: 20px; @@ -301,7 +301,7 @@ vertical-align: middle; } -.guest-clone .text { +.guest-pending .text { color: #666666; margin-left: 5px; text-shadow: -1px -1px 1px #CCCCCC, 1px 1px 1px #FFFFFF; diff --git a/ui/js/src/kimchi.guest_main.js b/ui/js/src/kimchi.guest_main.js index c712bb4..260e907 100644 --- a/ui/js/src/kimchi.guest_main.js +++ b/ui/js/src/kimchi.guest_main.js @@ -201,6 +201,21 @@ kimchi.listVmsAuto = function() { if (kimchi.vmTimeout) { clearTimeout(kimchi.vmTimeout); } + var getCreatingGuests = function(){ + var guests = []; + kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/[^/]+$'), function(tasks) { + for(var i=0;i<tasks.length;i++){ + var guestUri = tasks[i].target_uri; + var guestName = guestUri.split('/')[2] + guests.push($.extend({}, kimchi.sampleGuestObject, {name: guestName, isCreating: true})); + if(kimchi.trackingTasks.indexOf(tasks[i].id)==-1) + kimchi.trackTask(tasks[i].id, null, function(err){ + kimchi.message.error(err.message); + }, null); + } + }, null, true); + return guests; + }; var getCloningGuests = function(){ var guests = []; kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/.+/clone'), function(tasks) { @@ -219,6 +234,7 @@ kimchi.listVmsAuto = function() { kimchi.listVMs(function(result, textStatus, jqXHR) { if (result && textStatus=="success") { result = getCloningGuests().concat(result); + result = getCreatingGuests().concat(result); if(result.length) { var listHtml = ''; var guestTemplate = kimchi.guestTemplate; @@ -281,7 +297,7 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { imgLoad.attr('src',load_src); //Link the stopped tile to the start action, the running tile to open the console - if(!vmObject.isCloning){ + if(!(vmObject.isCloning || vmObject.isCreating)){ if (vmRunningBool) { liveTile.off("click", kimchi.vmstart); liveTile.on("click", kimchi.openVmConsole); @@ -329,7 +345,7 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { } //Setup action event handlers - if(!vmObject.isCloning){ + if(!(vmObject.isCloning || vmObject.isCreating)){ guestActions.find("[name=vm-start]").on({click : kimchi.vmstart}); guestActions.find("[name=vm-poweroff]").on({click : kimchi.vmpoweroff}); if (vmRunningBool) { //If the guest is not running, do not enable reset @@ -362,8 +378,14 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { }else{ guestActions.find('.btn').attr('disabled', true); - result.find('.guest-clone').removeClass('hide-content'); $('.popover', guestActions.find("div[name=actionmenu]")).remove(); + + result.find('.guest-pending').removeClass('hide-content'); + pendingText = result.find('.guest-pending .text') + if(vmObject.isCloning) + pendingText.text(i18n['KCHAPI6009M']); + else + pendingText.text(i18n['KCHAPI6008M']); } return result; diff --git a/ui/pages/guest.html.tmpl b/ui/pages/guest.html.tmpl index 17d41ac..8896ac5 100644 --- a/ui/pages/guest.html.tmpl +++ b/ui/pages/guest.html.tmpl @@ -26,8 +26,8 @@ <div class="guest-general"> <h2 class="title" title="{name}">{name}</h2> </div> - <div class="guest-clone hide-content"> - <span class="icon"></span><span class="text">$_("Cloning")...</span> + <div class="guest-pending hide-content"> + <span class="icon"></span><span class="text"></span> </div> </div> <div name="cpu_utilization" class="sortable"> diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index a6e3f5b..675d9a6 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -48,6 +48,7 @@ "KCHAPI6006M": "$_("Warning")", "KCHAPI6007M": "$_("Save")", "KCHAPI6008M": "$_("Creating...")", + "KCHAPI6009M": "$_("Cloning...")", "KCHGRD6001M": "$_("Loading...")", "KCHGRD6002M": "$_("An error occurred while retrieving system information.")", -- 2.1.0

Reviewed-By: Christy Perez <christy@linux.vnet.ibm.com> Tested-By: Christy Perez <christy@linux.vnet.ibm.com> Thanks Aline! On 04/29/2015 04:36 PM, Aline Manera wrote:
Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> Signed-off-by: Aline Manera <alinefm@linux.vnet.ibm.com> --- ui/css/theme-default/list.css | 8 ++++---- ui/js/src/kimchi.guest_main.js | 28 +++++++++++++++++++++++++--- ui/pages/guest.html.tmpl | 4 ++-- ui/pages/i18n.json.tmpl | 1 + 4 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/ui/css/theme-default/list.css b/ui/css/theme-default/list.css index 7b32ea6..e51912d 100644 --- a/ui/css/theme-default/list.css +++ b/ui/css/theme-default/list.css @@ -1,7 +1,7 @@ /* * Project Kimchi * - * Copyright IBM, Corp. 2013-2014 + * Copyright IBM, Corp. 2013-2015 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -289,11 +289,11 @@ padding-left: 10px; }
-.guest-clone { +.guest-pending { margin: 10px; }
-.guest-clone .icon { +.guest-pending .icon { background: url('../../images/theme-default/kimchi-loading15x15.gif') no-repeat; display: inline-block; width: 20px; @@ -301,7 +301,7 @@ vertical-align: middle; }
-.guest-clone .text { +.guest-pending .text { color: #666666; margin-left: 5px; text-shadow: -1px -1px 1px #CCCCCC, 1px 1px 1px #FFFFFF; diff --git a/ui/js/src/kimchi.guest_main.js b/ui/js/src/kimchi.guest_main.js index c712bb4..260e907 100644 --- a/ui/js/src/kimchi.guest_main.js +++ b/ui/js/src/kimchi.guest_main.js @@ -201,6 +201,21 @@ kimchi.listVmsAuto = function() { if (kimchi.vmTimeout) { clearTimeout(kimchi.vmTimeout); } + var getCreatingGuests = function(){ + var guests = []; + kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/[^/]+$'), function(tasks) { + for(var i=0;i<tasks.length;i++){ + var guestUri = tasks[i].target_uri; + var guestName = guestUri.split('/')[2] + guests.push($.extend({}, kimchi.sampleGuestObject, {name: guestName, isCreating: true})); + if(kimchi.trackingTasks.indexOf(tasks[i].id)==-1) + kimchi.trackTask(tasks[i].id, null, function(err){ + kimchi.message.error(err.message); + }, null); + } + }, null, true); + return guests; + }; var getCloningGuests = function(){ var guests = []; kimchi.getTasksByFilter('status=running&target_uri='+encodeURIComponent('^/vms/.+/clone'), function(tasks) { @@ -219,6 +234,7 @@ kimchi.listVmsAuto = function() { kimchi.listVMs(function(result, textStatus, jqXHR) { if (result && textStatus=="success") { result = getCloningGuests().concat(result); + result = getCreatingGuests().concat(result); if(result.length) { var listHtml = ''; var guestTemplate = kimchi.guestTemplate; @@ -281,7 +297,7 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { imgLoad.attr('src',load_src);
//Link the stopped tile to the start action, the running tile to open the console - if(!vmObject.isCloning){ + if(!(vmObject.isCloning || vmObject.isCreating)){ if (vmRunningBool) { liveTile.off("click", kimchi.vmstart); liveTile.on("click", kimchi.openVmConsole); @@ -329,7 +345,7 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { }
//Setup action event handlers - if(!vmObject.isCloning){ + if(!(vmObject.isCloning || vmObject.isCreating)){ guestActions.find("[name=vm-start]").on({click : kimchi.vmstart}); guestActions.find("[name=vm-poweroff]").on({click : kimchi.vmpoweroff}); if (vmRunningBool) { //If the guest is not running, do not enable reset @@ -362,8 +378,14 @@ kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) {
}else{ guestActions.find('.btn').attr('disabled', true); - result.find('.guest-clone').removeClass('hide-content'); $('.popover', guestActions.find("div[name=actionmenu]")).remove(); + + result.find('.guest-pending').removeClass('hide-content'); + pendingText = result.find('.guest-pending .text') + if(vmObject.isCloning) + pendingText.text(i18n['KCHAPI6009M']); + else + pendingText.text(i18n['KCHAPI6008M']); }
return result; diff --git a/ui/pages/guest.html.tmpl b/ui/pages/guest.html.tmpl index 17d41ac..8896ac5 100644 --- a/ui/pages/guest.html.tmpl +++ b/ui/pages/guest.html.tmpl @@ -26,8 +26,8 @@ <div class="guest-general"> <h2 class="title" title="{name}">{name}</h2> </div> - <div class="guest-clone hide-content"> - <span class="icon"></span><span class="text">$_("Cloning")...</span> + <div class="guest-pending hide-content"> + <span class="icon"></span><span class="text"></span> </div> </div> <div name="cpu_utilization" class="sortable"> diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index a6e3f5b..675d9a6 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -48,6 +48,7 @@ "KCHAPI6006M": "$_("Warning")", "KCHAPI6007M": "$_("Save")", "KCHAPI6008M": "$_("Creating...")", + "KCHAPI6009M": "$_("Cloning...")",
"KCHGRD6001M": "$_("Loading...")", "KCHGRD6002M": "$_("An error occurred while retrieving system information.")",

Reviewed-By: Christy Perez <christy@linux.vnet.ibm.com> Tested-By: Christy Perez <christy@linux.vnet.ibm.com> On 04/29/2015 04:36 PM, Aline Manera wrote:
V6 -> V7: - Update test cases - Update UI to reuse the clone box for pending VM creation
Aline Manera (1): Create VMs Asynchronously: UI
Christy Perez (5): Append clone to target_uri for vm clone task Tests for new clone target_uri UI changes for new clone target_uri Create VMs asynchronously: Backend Create VMs Asynchronously: Tests
src/kimchi/control/vms.py | 4 +-- src/kimchi/model/vms.py | 33 ++++++++++++++++++----- tests/test_authorization.py | 25 +++++++++++------- tests/test_mockmodel.py | 14 +++++++--- tests/test_model.py | 49 +++++++++++++++++++++++----------- tests/test_rest.py | 60 +++++++++++++++++++++++++++++++----------- tests/test_server.py | 26 ++++++++---------- ui/css/theme-default/list.css | 8 +++--- ui/js/src/kimchi.guest_main.js | 32 ++++++++++++++++++---- ui/pages/guest.html.tmpl | 4 +-- ui/pages/i18n.json.tmpl | 1 + 11 files changed, 176 insertions(+), 80 deletions(-)
participants (2)
-
Aline Manera
-
Christy Perez