[PATCH v2] [Kimchi 0/5] CPU Hot plug/unplug feature

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> ** DEPENDS ON 'CPU topology setup enhancements' PATCH SET ** v3: - rebased the patch set with the 'CPU topology setup enhancements' changes - added new test on test_rest.py v2: - standardized i18n messages to use CPUs instead of vCPUs This patch set implements CPU Hot plug/unplug capabilities in Kimchi. To test it, simply add/remove CPUs in a running guest via the 'Edit' menu. Note that all restrictions on the current CPU value (can't exceed max number of CPUs, must be a number that makes sense in the topology if one is set) still applies. For Power systems, the hot unplug requires additional software running in the guest to work: powerpc-utils, ppc64-diag and librtas. The service 'rtas_err' must be running too. Daniel Henrique Barboza (5): CPU Hot plug/unplug: i18n changes CPU Hot plug/unplug: model changes CPU Hot plug/unplug: test changes CPU Hot plug/unplug: ui changes CPU Hot plug/unplug: test_rest.py changes i18n.py | 3 + model/vms.py | 66 ++++++++++++++++++--- tests/test_model.py | 112 ++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 33 +++++++++-- ui/js/src/kimchi.guest_edit_main.js | 44 +++++++++----- ui/pages/guest-edit.html.tmpl | 5 +- ui/pages/help/en_US/guests.dita | 8 +++ 7 files changed, 242 insertions(+), 29 deletions(-) -- 2.7.4

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds new i18n messages to be used by the new CPU Hotplug backend. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- i18n.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/i18n.py b/i18n.py index 453ede8..838cfcb 100644 --- a/i18n.py +++ b/i18n.py @@ -362,6 +362,9 @@ messages = { "KCHCPUINF0008E": _("Parameter 'cpu_info' expects an object with fields among: 'vcpus', 'maxvcpus', 'topology'."), "KCHCPUINF0009E": _("Parameter 'topology' expects an object with fields among: 'sockets', 'cores', 'threads'."), + "KCHCPUHOTP0001E": _("Unable to update Max CPU or CPU topology when guest is running."), + "KCHCPUHOTP0002E": _("Unable to hot plug/unplug CPUs. Details: %(err)s"), + "KCHLVMS0001E": _("Invalid volume group name parameter: %(name)s."), "KCHCONN0001E": _("Unable to establish connection with libvirt. Please check your libvirt URI which is often defined in /etc/libvirt/libvirt.conf"), -- 2.7.4

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch makes changes on model/vms.py to support the hotplug of new CPUs in a running VM. For CPU Hot unplug in Power hypervisors there is a need for guest software to be installed: * guest must have packages: powerpc-utils, ppc64-diag, librtas * guest must have service rtas_err.service running Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/vms.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/model/vms.py b/model/vms.py index bff7ed2..d703c89 100644 --- a/model/vms.py +++ b/model/vms.py @@ -82,7 +82,8 @@ DOM_STATE_MAP = {0: 'nostate', 7: 'pmsuspended'} # update parameters which are updatable when the VM is online -VM_ONLINE_UPDATE_PARAMS = ['graphics', 'groups', 'memory', 'users'] +VM_ONLINE_UPDATE_PARAMS = ['cpu_info', 'graphics', 'groups', + 'memory', 'users'] # update parameters which are updatable when the VM is offline VM_OFFLINE_UPDATE_PARAMS = ['cpu_info', 'graphics', 'groups', 'memory', @@ -1028,18 +1029,33 @@ class VMModel(object): unit='Kib')) return ET.tostring(root, encoding="utf-8") - def _update_cpu_info(self, new_xml, dom, new_info): + def get_vm_cpu_cores(self, vm_xml): + return xpath_get_text(vm_xml, XPATH_TOPOLOGY + '/@cores')[0] + + def get_vm_cpu_sockets(self, vm_xml): + return xpath_get_text(vm_xml, XPATH_TOPOLOGY + '/@sockets')[0] + + def get_vm_cpu_threads(self, vm_xml): + return xpath_get_text(vm_xml, XPATH_TOPOLOGY + '/@threads')[0] + + def get_vm_cpu_topology(self, dom): topology = {} if self.has_topology(dom): - sockets = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@sockets')[0] - cores = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@cores')[0] - threads = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@threads')[0] + sockets = int(self.get_vm_cpu_sockets(dom.XMLDesc(0))) + cores = int(self.get_vm_cpu_cores(dom.XMLDesc(0))) + threads = int(self.get_vm_cpu_threads(dom.XMLDesc(0))) + topology = { - 'sockets': int(sockets), - 'cores': int(cores), - 'threads': int(threads), + 'sockets': sockets, + 'cores': cores, + 'threads': threads, } + return topology + + def _update_cpu_info(self, new_xml, dom, new_info): + topology = self.get_vm_cpu_topology(dom) + # if current is not defined in vcpu, vcpus is equal to maxvcpus xml_maxvcpus = xpath_get_text(new_xml, 'vcpu') maxvcpus = int(xml_maxvcpus[0]) @@ -1064,6 +1080,40 @@ class VMModel(object): if (('memory' in params) and ('current' in params['memory'])): self._update_memory_live(dom, params) + if 'vcpus' in params.get('cpu_info', {}): + self.cpu_hotplug_precheck(dom, params) + vcpus = params['cpu_info'].get('vcpus') + self.update_cpu_live(dom, vcpus) + + def cpu_hotplug_precheck(self, dom, params): + + if (('maxvcpus' in params['cpu_info']) or + ('topology' in params['cpu_info'])): + raise InvalidParameter('KCHCPUHOTP0001E') + + topology = self.get_vm_cpu_topology(dom) + + xml_maxvcpus = xpath_get_text(dom.XMLDesc(0), 'vcpu') + maxvcpus = int(xml_maxvcpus[0]) + vcpus = params['cpu_info'].get('vcpus') + + cpu_info = { + 'maxvcpus': maxvcpus, + 'vcpus': vcpus, + 'topology': topology, + } + + cpu_model = CPUInfoModel(conn=self.conn) + cpu_model.check_cpu_info(cpu_info) + + def update_cpu_live(self, dom, vcpus): + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE | \ + libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + dom.setVcpusFlags(vcpus, flags) + except libvirt.libvirtError as e: + raise OperationFailed('KCHCPUHOTP0002E', {'err': e.message}) + def _get_mem_dev_total_size(self, xml): root = ET.fromstring(xml) totMemDevs = 0 -- 2.7.4

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds new tests in test_model.py to verify this new feature. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- tests/test_model.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 4bc82ad..f3da835 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -22,6 +22,7 @@ import __builtin__ as builtins import base64 import grp +import libvirt import json import lxml.etree as ET import mock @@ -1415,6 +1416,117 @@ class ModelTests(unittest.TestCase): inst.vm_update(u'пeω-∨м', {"bootmenu": False}) self.assertEquals("no", inst.vm_lookup(u'пeω-∨м')['bootmenu']) + def test_get_vm_cpu_cores(self): + xml = """<domain type='kvm'>\ +<cpu><topology sockets='3' cores='2' threads='8'/></cpu>\ +</domain>""" + inst = model.Model(None, objstore_loc=self.tmp_store) + self.assertEqual('2', inst.vm_get_vm_cpu_cores(xml)) + + def test_get_vm_cpu_sockets(self): + xml = """<domain type='kvm'>\ +<cpu><topology sockets='3' cores='2' threads='8'/></cpu>\ +</domain>""" + inst = model.Model(None, objstore_loc=self.tmp_store) + self.assertEqual('3', inst.vm_get_vm_cpu_sockets(xml)) + + def test_get_vm_cpu_threads(self): + xml = """<domain type='kvm'>\ +<cpu><topology sockets='3' cores='2' threads='8'/></cpu>\ +</domain>""" + inst = model.Model(None, objstore_loc=self.tmp_store) + self.assertEqual('8', inst.vm_get_vm_cpu_threads(xml)) + + @mock.patch('wok.plugins.kimchi.model.vms.VMModel.has_topology') + def test_get_vm_cpu_topology(self, mock_has_topology): + class FakeDom(): + def XMLDesc(self, flag): + return """<domain type='kvm'>\ +<cpu><topology sockets='3' cores='2' threads='8'/></cpu>\ +</domain>""" + + def name(self): + return 'fakedom' + + mock_has_topology.return_value = True + expected_topology = {'sockets': 3, 'cores': 2, 'threads': 8} + + inst = model.Model(None, objstore_loc=self.tmp_store) + self.assertEqual(expected_topology, + inst.vm_get_vm_cpu_topology(FakeDom())) + + @mock.patch('wok.plugins.kimchi.model.vms.VMModel.has_topology') + def test_get_vm_cpu_topology_blank(self, mock_has_topology): + class FakeDom(): + def XMLDesc(self, flag): + return """<domain type='kvm'></domain>""" + + def name(self): + return 'fakedom' + + mock_has_topology.return_value = False + expected_topology = {} + + inst = model.Model(None, objstore_loc=self.tmp_store) + self.assertEqual(expected_topology, + inst.vm_get_vm_cpu_topology(FakeDom())) + + def test_vm_cpu_hotplug_invalidparam_fail(self): + inst = model.Model(None, objstore_loc=self.tmp_store) + + with self.assertRaisesRegexp(InvalidParameter, 'KCHCPUHOTP0001E'): + params = {"cpu_info": {"vcpus": 1, 'maxvcpus': 4}} + inst.vm_cpu_hotplug_precheck('', params) + + @mock.patch('wok.plugins.kimchi.model.vms.VMModel.has_topology') + def test_vm_cpu_hotplug_abovemax_fail(self, mock_has_topology): + class FakeDom(): + def XMLDesc(self, flag): + return """<domain type='kvm'>\ +<vcpu placement='static' current='1'>8</vcpu><\ +/domain>""" + + def name(self): + return 'fakedom' + + mock_has_topology.return_value = False + inst = model.Model(None, objstore_loc=self.tmp_store) + + with self.assertRaisesRegexp(InvalidParameter, 'KCHCPUINF0001E'): + params = {"cpu_info": {"vcpus": 16}} + inst.vm_cpu_hotplug_precheck(FakeDom(), params) + + @mock.patch('wok.plugins.kimchi.model.vms.VMModel.has_topology') + @mock.patch('wok.plugins.kimchi.model.vms.VMModel.get_vm_cpu_topology') + def test_vm_cpu_hotplug_topology_mismatch_fail(self, mock_topology, + mock_has_topology): + class FakeDom(): + def XMLDesc(self, flag): + return """<domain type='kvm'>\ +<vcpu placement='static' current='8'>48</vcpu><\ +/domain>""" + + def name(self): + return 'fakedom' + + mock_has_topology.return_value = True + mock_topology.return_value = {'sockets': 3, 'cores': 2, 'threads': 8} + + inst = model.Model(None, objstore_loc=self.tmp_store) + + with self.assertRaisesRegexp(InvalidParameter, 'KCHCPUINF0005E'): + params = {"cpu_info": {"vcpus": 10}} + inst.vm_cpu_hotplug_precheck(FakeDom(), params) + + def test_vm_cpu_hotplug_error(self): + class FakeDom(): + def setVcpusFlags(self, vcpu, flags): + raise libvirt.libvirtError('') + + inst = model.Model(None, objstore_loc=self.tmp_store) + with self.assertRaisesRegexp(OperationFailed, 'KCHCPUHOTP0002E'): + inst.vm_update_cpu_live(FakeDom(), '') + def test_get_interfaces(self): inst = model.Model('test:///default', objstore_loc=self.tmp_store) -- 2.7.4

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch makes the following changes to enable CPU hotplug/unplug on a running/paused guest: - if the guest is running, lock all fields but the current CPU in the Edit Guest dialog. A message is displayed informing the user that on a running or paused guest the only CPU setting that can be changed is the current CPU value; - in the form validation processorSubmit function, discard every info but the current CPU value if the guest is running or paused. Changes in guests.dita were also made to add information about the guest support for this feature. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- ui/js/src/kimchi.guest_edit_main.js | 44 ++++++++++++++++++++++++------------- ui/pages/guest-edit.html.tmpl | 5 ++++- ui/pages/help/en_US/guests.dita | 8 +++++++ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/ui/js/src/kimchi.guest_edit_main.js b/ui/js/src/kimchi.guest_edit_main.js index f06e0fa..7667fad 100644 --- a/ui/js/src/kimchi.guest_edit_main.js +++ b/ui/js/src/kimchi.guest_edit_main.js @@ -1032,7 +1032,17 @@ kimchi.guest_edit_main = function() { setupPermission(); setupPCIDevice(); setupSnapshot(); + kimchi.init_processor_tab(guest.cpu_info, $(saveButton)); + if ((kimchi.thisVMState === "running") || (kimchi.thisVMState === "paused")) { + $('#guest-edit-max-processor-textbox').attr("disabled", true); + $('#sockets').attr("disabled", true); + $('#cores').attr("disabled", true); + $('#threads').attr("disabled", true); + + $("#topology-checkbox").hide(); + $("#settings-readonly-help").removeClass('hidden'); + } wok.topic('kimchi/vmCDROMAttached').subscribe(onAttached); wok.topic('kimchi/vmCDROMReplaced').subscribe(onReplaced); @@ -1176,22 +1186,26 @@ kimchi.guest_edit_main = function() { if (maxCpu >= cpu) { maxCpuFinal = maxCpu; } - if ($("input:checkbox", "#form-edit-processor").prop("checked")) { - data['cpu_info'] = { - vcpus: cpu, - maxvcpus: maxCpuFinal, - topology: { - sockets: parseInt($("#sockets").val()), - cores: parseInt($("#cores").val()), - threads: parseInt($("#threads").val()) - } - }; + if (kimchi.thisVMState === 'running' || kimchi.thisVMState === 'paused') { + data['cpu_info'] = {vcpus: cpu}; } else { - data['cpu_info'] = { - vcpus: cpu, - maxvcpus: maxCpuFinal, - topology: {} - }; + if ($("input:checkbox", "#form-edit-processor").prop("checked")) { + data['cpu_info'] = { + vcpus: cpu, + maxvcpus: maxCpuFinal, + topology: { + sockets: parseInt($("#sockets").val()), + cores: parseInt($("#cores").val()), + threads: parseInt($("#threads").val()) + } + }; + } else { + data['cpu_info'] = { + vcpus: cpu, + maxvcpus: maxCpuFinal, + topology: {} + }; + } } kimchi.updateVM(kimchi.selectedGuest, data, function() { diff --git a/ui/pages/guest-edit.html.tmpl b/ui/pages/guest-edit.html.tmpl index ce63471..2ee5bda 100644 --- a/ui/pages/guest-edit.html.tmpl +++ b/ui/pages/guest-edit.html.tmpl @@ -197,6 +197,9 @@ $_("Current CPU must be equal or lower than the Maximum CPU value. If a topology is set, it must be also be a multiple of the 'threads' value.") </p> </div> + <div id="settings-readonly-help" class="hidden"> + <b>$_("Unable to edit maximum CPU or CPU topology when editing a running or paused virtual machine.")</b> + </div> <div id="guest-max-processor-panel" class="form-group"> <label for="guest-edit-max-processor-textbox">$_("Max CPU")</label> <input id="guest-edit-max-processor-textbox" class="form-control" name="max-processor" type="number" min="1" /> @@ -206,7 +209,7 @@ </p> </div> </div> - <div class="manual form-group"> + <div class="manual form-group" id="topology-checkbox"> <input type="checkbox" class="wok-checkbox" id="cpus-check" /> <label for="cpus-check">$_("Manually set CPU topology")</label> </div> diff --git a/ui/pages/help/en_US/guests.dita b/ui/pages/help/en_US/guests.dita index 45cf7da..5c71694 100644 --- a/ui/pages/help/en_US/guests.dita +++ b/ui/pages/help/en_US/guests.dita @@ -77,6 +77,14 @@ create a template.</li> can be edited only while guest is stopped. Others will take effect in next boot.</shortdesc> <csbody> +<ol><li><uicontrol>If guest is off: </uicontrol>CPU and Memory new values +takes effect after next boot.</li> +<li><uicontrol>If guest is running: </uicontrol>Guest must support +hotplug/hotunplug of CPU and Memory devices, otherwise changes in these fields will not take effect. For Power systems, the guest must have:<ul> +<li>Latest versions of packages <uicontrol>powerpc-utils, ppc64-diag</uicontrol> and <uicontrol>librtas</uicontrol> +must be installed.</li> +<li>Service <uicontrol>rtas_err</uicontrol> must be running.</li></ul> +</li></ol> <dl><dlentry> <dt>General</dt> <dd>Displays information about your guest, including name, CPUs, memory, -- 2.7.4

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch changes test_test.py by: - adds a new test called 'test_edit_vm_cpuhotplug' to test the CPU Hotplug feature; - removes the CPU hotplug test from 'test_edit_vm'. This test was meant to fail because CPU hotplug wasn't possible until now. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- tests/test_rest.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/test_rest.py b/tests/test_rest.py index f6b81a6..f25a693 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -123,6 +123,34 @@ class RestTests(unittest.TestCase): self.assertEquals([], vm['users']) self.assertEquals([], vm['groups']) + def test_edit_vm_cpuhotplug(self): + req = json.dumps({'name': 'template_cpuhotplug', + 'source_media': {'type': 'disk', 'path': fake_iso}}) + resp = self.request('/plugins/kimchi/templates', req, 'POST') + self.assertEquals(201, resp.status) + + req = json.dumps( + {'name': 'vm-cpuhotplug', + 'template': '/plugins/kimchi/templates/template_cpuhotplug'} + ) + resp = self.request('/plugins/kimchi/vms', req, 'POST') + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + + req = json.dumps({'cpu_info': {'maxvcpus': 5, 'vcpus': 1}}) + resp = self.request('/plugins/kimchi/vms/vm-cpuhotplug', + req, 'PUT') + self.assertEquals(200, resp.status) + + resp = self.request('/plugins/kimchi/vms/vm-cpuhotplug/start', + '{}', 'POST') + self.assertEquals(200, resp.status) + + req = json.dumps({'cpu_info': {'vcpus': 5}}) + resp = self.request('/plugins/kimchi/vms/vm-cpuhotplug', req, 'PUT') + self.assertEquals(200, resp.status) + def test_edit_vm(self): req = json.dumps({'name': 'test', 'source_media': {'type': 'disk', 'path': fake_iso}}) @@ -168,11 +196,6 @@ class RestTests(unittest.TestCase): resp = self.request('/plugins/kimchi/vms/vm-1', req, 'PUT') self.assertEquals(400, resp.status) - # Unable to do CPU hotplug - req = json.dumps({'cpu_info': {'vcpus': 5}}) - resp = self.request('/plugins/kimchi/vms/vm-1', req, 'PUT') - self.assertEquals(400, resp.status) - # Check if there is support to memory hotplug, once vm is running resp = self.request('/plugins/kimchi/config/capabilities').read() conf = json.loads(resp) -- 2.7.4
participants (2)
-
Aline Manera
-
dhbarboza82@gmail.com