[PATCH V4 0/3] Edit guest MAC address

V4: - Disable MAC changes in a running system. V3: - Updated API.json to filter mac address. - Improved UI code. V2: - Updated API.json to reflect only mac address as required parameter. - Removed input text filters. - Updated error message. This patchset implements a new feature in Kimchi, it allows users to edit a guest MAC address. I have a fork of kimchi in my github, so if you prefer to use their diff tool (syntax highlight) you can check this link out: https://github.com/kimchi-project/kimchi/compare/master...jrziviani:guest_ma... Jose Ricardo Ziviani (3): Implement backend code to edit MAC address of a guest Implement frontend code to edit MAC address of a guest Update test cases to reflect MAC address update changes src/kimchi/API.json | 22 ++++++++-------- src/kimchi/i18n.py | 5 +++- src/kimchi/model/vmifaces.py | 52 +++++++++++++++++++++++++------------ tests/test_model.py | 13 +++++++--- tests/test_rest.py | 12 +++++---- ui/js/src/kimchi.guest_edit_main.js | 44 +++++++++++++++++++++---------- ui/pages/guest-edit.html.tmpl | 6 +++-- 7 files changed, 100 insertions(+), 54 deletions(-) -- 1.9.1

Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- src/kimchi/API.json | 22 +++++++++---------- src/kimchi/i18n.py | 5 ++++- src/kimchi/model/vmifaces.py | 52 +++++++++++++++++++++++++++++--------------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/kimchi/API.json b/src/kimchi/API.json index a6330ae..b9a41eb 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -393,6 +393,12 @@ "type": "string", "pattern": "^ne2k_pci|i82551|i82557b|i82559er|rtl8139|e1000|pcnet|virtio$", "error": "KCHVMIF0006E" + }, + "mac": { + "description": "Network Interface Card MAC address", + "type": "string", + "pattern": "(^$)|^(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$)", + "error": "KCHVMIF0010E" } } }, @@ -400,19 +406,13 @@ "type": "object", "error": "KCHVMIF0008E", "properties": { - "network": { - "description": "the name of one available network", - "minLength": 1, + "mac": { + "description": "Network Interface Card MAC address", "type": "string", - "error": "KCHVMIF0005E" - }, - "model": { - "description": "model of emulated network interface card", - "enum": ["ne2k_pci", "i82551", "i82557b", "i82559er", "rtl8139", "e1000", "pcnet", "virtio", "spapr-vlan"], - "error": "KCHVMIF0006E" + "pattern": "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$", + "error": "KCHVMIF0010E" } - }, - "additionalProperties": false + } }, "templates_create": { "type": "object", diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 9f169ab..d5e93fa 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -126,7 +126,10 @@ messages = { "KCHVMIF0005E": _("Network name for virtual machine interface must be a string"), "KCHVMIF0006E": _("Invalid network model card specified for virtual machine interface"), "KCHVMIF0007E": _("Specify type and network to add a new virtual machine interface"), - "KCHVMIF0008E": _("Specify type and network to update a virtual machine interface"), + "KCHVMIF0008E": _("MAC Address must respect this format FF:FF:FF:FF:FF:FF"), + "KCHVMIF0009E": _("MAC Address %(mac)s already exists in virtual machine %(name)s"), + "KCHVMIF0010E": _("Invalid MAC Address"), + "KCHVMIF0011E": _("Cannot change MAC address of a running virtual machine"), "KCHTMPL0001E": _("Template %(name)s already exists"), "KCHTMPL0003E": _("Network '%(network)s' specified for template %(template)s does not exist"), diff --git a/src/kimchi/model/vmifaces.py b/src/kimchi/model/vmifaces.py index 8bf6b3d..50edde2 100644 --- a/src/kimchi/model/vmifaces.py +++ b/src/kimchi/model/vmifaces.py @@ -22,7 +22,8 @@ import random import libvirt from lxml import etree, objectify -from kimchi.exception import InvalidParameter, MissingParameter, NotFoundError +from kimchi.exception import InvalidParameter, MissingParameter +from kimchi.exception import NotFoundError, InvalidOperation from kimchi.model.config import CapabilitiesModel from kimchi.model.vms import DOM_STATE_MAP, VMModel from kimchi.xmlutils.interface import get_iface_xml @@ -57,10 +58,19 @@ class VMIfacesModel(object): macs = (iface.mac.get('address') for iface in self.get_vmifaces(vm, self.conn)) - while True: - params['mac'] = VMIfacesModel.random_mac() - if params['mac'] not in macs: - break + # user defined customized mac address + if 'mac' in params and params['mac']: + # make sure it is unique + if params['mac'] in macs: + raise InvalidParameter('KCHVMIF0009E', + {'name': vm, 'mac': params['mac']}) + + # otherwise choose a random mac address + else: + while True: + params['mac'] = VMIfacesModel.random_mac() + if params['mac'] not in macs: + break dom = VMModel.get_vm(vm, self.conn) @@ -147,22 +157,30 @@ class VMIfaceModel(object): if iface is None: raise NotFoundError("KCHVMIF0001E", {'name': vm, 'iface': mac}) + # mac address is a required parameter + if 'mac' not in params: + raise MissingParameter('KCHVMIF0008E') + + # new mac address must be unique + if self._get_vmiface(vm, params['mac']) is not None: + raise InvalidParameter('KCHVMIF0009E', + {'name': vm, 'mac': params['mac']}) + flags = 0 if dom.isPersistent(): flags |= libvirt.VIR_DOMAIN_AFFECT_CONFIG - if DOM_STATE_MAP[dom.info()[0]] != "shutoff": - flags |= libvirt.VIR_DOMAIN_AFFECT_LIVE - - if iface.attrib['type'] == 'network' and 'network' in params: - iface.source.attrib['network'] = params['network'] - xml = etree.tostring(iface) - dom.updateDeviceFlags(xml, flags=flags) + # cannot change mac address in a running system + if DOM_STATE_MAP[dom.info()[0]] != "shutoff": + raise InvalidOperation('KCHVMIF0011E') - if 'model' in params: - iface.model.attrib["type"] = params['model'] - xml = etree.tostring(iface) + # remove the current nic + xml = etree.tostring(iface) + dom.detachDeviceFlags(xml, flags=flags) - dom.updateDeviceFlags(xml, flags=flags) + # add the nic with the desired mac address + iface.mac.attrib['address'] = params['mac'] + xml = etree.tostring(iface) + dom.attachDeviceFlags(xml, flags=flags) - return mac + return [vm, params['mac']] -- 1.9.1

Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- ui/js/src/kimchi.guest_edit_main.js | 44 +++++++++++++++++++++++++------------ ui/pages/guest-edit.html.tmpl | 6 +++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/ui/js/src/kimchi.guest_edit_main.js b/ui/js/src/kimchi.guest_edit_main.js index 387dbac..7671e43 100644 --- a/ui/js/src/kimchi.guest_edit_main.js +++ b/ui/js/src/kimchi.guest_edit_main.js @@ -164,6 +164,7 @@ kimchi.guest_edit_main = function() { }).click(function(evt){ evt.preventDefault(); addItem({ + id: -1, mac: "", network: "", type: "network", @@ -171,12 +172,17 @@ kimchi.guest_edit_main = function() { editMode: "" }); }); - var toggleEdit = function(item, on){ - $("label", item).toggleClass("hide", on); - $("select", item).toggleClass("hide", !on); + var toggleEdit = function(item, on, itemId){ + $("#label-mac-" + itemId, item).toggleClass("hide", on); + $("#edit-mac-" + itemId, item).toggleClass("hide", !on); + $("#label-network-" + itemId, item).toggleClass("hide", false); + $("select", item).toggleClass("hide", true); $(".action-area", item).toggleClass("hide"); }; var addItem = function(data) { + if (data.id == -1) { + data.id = $('#form-guest-edit-interface > .body').children().size() + } var itemNode = $.parseHTML(kimchi.substitute($('#interface-tmpl').html(),data)); $(".body", "#form-guest-edit-interface").append(itemNode); $("select", itemNode).append(networkOptions); @@ -184,12 +190,12 @@ kimchi.guest_edit_main = function() { $("select", itemNode).val(data.network); } $(".edit", itemNode).button({ - disabled: true, + disabled: kimchi.thisVMState === "running", icons: { primary: "ui-icon-pencil" }, text: false }).click(function(evt){ evt.preventDefault(); - toggleEdit($(this).parent().parent(), true); + toggleEdit($(this).closest('div'), true, data.id); }); $(".delete", itemNode).button({ icons: { primary: "ui-icon-trash" }, @@ -209,21 +215,30 @@ kimchi.guest_edit_main = function() { var item = $(this).parent().parent(); var interface = { network: $("select", item).val(), - type: "network" + type: "network", + mac: $(":text", item).val() }; - var postUpdate = function(){ - $("label", item).text(interface.network); - toggleEdit(item, false); + var postUpdate = function(mac){ + $("#label-network-" + data.id, item).text(interface.network); + $("#label-mac-" + data.id, item).text(mac); + $("#edit-mac-" + data.id, item).val(mac); + toggleEdit(item, false, data.id); }; if(item.prop("id")==""){ kimchi.createGuestInterface(kimchi.selectedGuest, interface, function(data){ item.prop("id", data.mac); - postUpdate(); + postUpdate(data.mac); }); }else{ - kimchi.updateGuestInterface(kimchi.selectedGuest, item.prop("id"), interface, function(){ - postUpdate(); - }); + if (item.prop('id') == interface.mac) { + toggleEdit(item, false, data.id); + } else { + kimchi.updateGuestInterface(kimchi.selectedGuest, item.prop('id'), + interface, function(data){ + item.prop("id", data.mac); + postUpdate(data.mac); + }); + } } }); $(".cancel", itemNode).button({ @@ -232,7 +247,7 @@ kimchi.guest_edit_main = function() { }).click(function(evt){ evt.preventDefault(); var item = $(this).parent().parent(); - $("label", item).text()==="" ? item.remove() : toggleEdit(item, false); + $("label", item).text()==="" ? item.remove() : toggleEdit(item, false, data.id); }); }; var networkOptions = ""; @@ -245,6 +260,7 @@ kimchi.guest_edit_main = function() { for(var i=0;i<data.length;i++){ data[i].viewMode = ""; data[i].editMode = "hide"; + data[i].id = i; addItem(data[i]); } }); diff --git a/ui/pages/guest-edit.html.tmpl b/ui/pages/guest-edit.html.tmpl index 24282d8..389f3c5 100644 --- a/ui/pages/guest-edit.html.tmpl +++ b/ui/pages/guest-edit.html.tmpl @@ -227,14 +227,16 @@ <script id="interface-tmpl" type="text/html"> <div class="item" id="{mac}"> <span class="cell"> - <label class="{viewMode}">{network}</label> + <label id="label-network-{id}" class="{viewMode}">{network}</label> <select class="{editMode}"></select> </span> <span class="cell"> <span>{type}</span> </span> <span class="cell"> - <span>{mac}</span> + <label id="label-mac-{id}" class="{viewMode}">{mac}</label> + <input class="{editMode}" type="text" + id="edit-mac-{id}" name="{mac}" value="{mac}" /> </span> <span class="action-area {editMode}"> <button class="save"></button><button class="cancel"></button> -- 1.9.1

Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- tests/test_model.py | 13 +++++++++---- tests/test_rest.py | 12 +++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 88c020e..7a7b97d 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -368,12 +368,17 @@ class ModelTests(unittest.TestCase): self.assertEquals("test-network", iface['network']) # update vm interface - iface_args = {"network": "default", - "model": "e1000"} + newMacAddr = '54:50:e3:44:8a:af' + iface_args = {"mac": newMacAddr} inst.vmiface_update(vm_name, mac, iface_args) + iface = inst.vmiface_lookup(vm_name, newMacAddr) + self.assertEquals(newMacAddr, iface['mac']) + + # undo mac address change + iface_args = {"mac": mac} + inst.vmiface_update(vm_name, newMacAddr, iface_args) iface = inst.vmiface_lookup(vm_name, mac) - self.assertEquals("default", iface['network']) - self.assertEquals("e1000", iface["model"]) + self.assertEquals(mac, iface['mac']) @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_disk(self): diff --git a/tests/test_rest.py b/tests/test_rest.py index 914b602..d917d8f 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -724,13 +724,15 @@ class RestTests(unittest.TestCase): self.assertEquals('network', iface['type']) # update vm interface - req = json.dumps({"network": "default", "model": "virtio"}) + newMacAddr = '54:50:e3:44:8a:af' + req = json.dumps({"network": "default", "model": "virtio", + "type": "network", "mac": newMacAddr}) resp = self.request('/vms/test-vm/ifaces/%s' % iface['mac'], req, 'PUT') - self.assertEquals(200, resp.status) - update_iface = json.loads(resp.read()) - self.assertEquals(u'virtio', update_iface['model']) - self.assertEquals('default', update_iface['network']) + self.assertEquals(303, resp.status) + iface = json.loads(self.request('/vms/test-vm/ifaces/%s' % + newMacAddr).read()) + self.assertEquals(newMacAddr, iface['mac']) # detach network interface from vm resp = self.request('/vms/test-vm/ifaces/%s' % iface['mac'], -- 1.9.1
participants (2)
-
Aline Manera
-
Jose Ricardo Ziviani