[PATCH V3 0/5] vm ticket in backend

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> V2 -> V3: do not make ticket as sub-resource of a VM V1 -> V2: make ticket as sub-resource of a VM A ticket is the credential to access VM. Only who get the ticket can access a VM. test this patch set: set the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ \ -X PUT -d '{"ticket": {"passwd": "abcd"}}' get the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ ShaoHe Feng (5): vm ticket in backend: update API.md vm ticket in backend: update controller and API.json vm ticket in backend: update model vm ticket in backend: update mockmodel vm ticket in backend: update test case docs/API.md | 7 +++++ src/kimchi/API.json | 16 +++++++++++ src/kimchi/control/vms.py | 3 ++- src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 14 ++++++++++ src/kimchi/model/vms.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_model.py | 15 +++++++++++ tests/test_rest.py | 12 +++++++++ 8 files changed, 136 insertions(+), 1 deletion(-) -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> A ticket is the credential to access VM. Only who get the ticket can access a VM. test this patch set: set the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ \ -X PUT -d '{"ticket": {"passwd": "abcd"}}' get the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- docs/API.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/API.md b/docs/API.md index 1a228df..aebf563 100644 --- a/docs/API.md +++ b/docs/API.md @@ -94,6 +94,9 @@ the following general conventions: * port: The real port number of the graphics, vnc or spice. Users can use this port to connect to the vm with general vnc/spice clients. + * ticket: A ticket represents credentials to access VM. + * password: the password of a ticket. + * expire: lifetime of a ticket. * users: A list of system users who have permission to access the VM. Default is: empty (i.e. only root-users may access). * groups: A list of system groups whose users have permission to access @@ -107,6 +110,10 @@ the following general conventions: will take effect in next reboot) * memory: New amount of memory (MB) for this VM (if VM is running, new value will take effect in next reboot) + * ticket: A ticket represents credentials to access VM. + * password *(optional)*: the password of a ticket. + * expire *(optional)*: lifetime of a ticket. + * **POST**: *See Virtual Machine Actions* **Actions (POST):** -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> verify parameters when update the ticket of a VM. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- src/kimchi/API.json | 16 ++++++++++++++++ src/kimchi/control/vms.py | 3 ++- src/kimchi/i18n.py | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 3616509..4b432a2 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -235,6 +235,22 @@ "error": "KCHVM0026E" } }, + "ticket": { + "description": "A ticket represents credentials to access VM", + "type": "object", + "properties": { + "password": { + "description": "the password of a ticket.", + "type": "string", + "error": "KCHVM0031E" + }, + "size": { + "description": "lifetime of a ticket.", + "type": "number", + "error": "KCHVM0032E" + } + } + }, "cpus": { "description": "The new number of virtual CPUs for the VM", "type": "integer", diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index cf427fa..8e86cdc 100644 --- a/src/kimchi/control/vms.py +++ b/src/kimchi/control/vms.py @@ -32,7 +32,8 @@ def __init__(self, model): class VM(Resource): def __init__(self, model, ident): super(VM, self).__init__(model, ident) - self.update_params = ["name", "users", "groups", "cpus", "memory"] + self.update_params = ["name", "users", "groups", "cpus", "memory", + "ticket"] self.screenshot = VMScreenShot(model, ident) self.uri_fmt = '/vms/%s' for ident, node in sub_nodes.items(): diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 0c76145..39dd623 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -89,6 +89,8 @@ "KCHVM0028E": _("Group(s) '%(groups)s' do not exist"), "KCHVM0029E": _("Unable to shutdown virtual machine %(name)s. Details: %(err)s"), "KCHVM0030E": _("Unable to get access metadata of virtual machine %(name)s. Details: %(err)s"), + "KCHVM0031E": _("password of a vm ticket must be a string."), + "KCHVM0032E": _("expire of a vm ticket must be a number."), "KCHVMIF0001E": _("Interface %(iface)s does not exist in virtual machine %(name)s"), "KCHVMIF0002E": _("Network %(network)s specified for virtual machine %(name)s does not exist"), -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> set ticket as attribute of vm info. vm lookup will get ticket. API user should check the expire of ticket. vm update method can edit ticket. when set a ticket, it will take effect on both persistent and live configure. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 8c0dcb1..2681e95 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -19,7 +19,10 @@ from lxml.builder import E import lxml.etree as ET +from lxml import etree, objectify import os +import random +import string import time import uuid from xml.etree import ElementTree @@ -246,6 +249,12 @@ def __init__(self, **kargs): self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs) def update(self, name, params): + if 'ticket' in params: + password = params['ticket'].get("passwd") + password = password if password is not None else "".join( + random.sample(string.ascii_letters + string.digits, 8)) + params['ticket']['passwd'] = password + dom = self.get_vm(name, self.conn) dom = self._static_vm_update(dom, params) self._live_vm_update(dom, params) @@ -304,6 +313,47 @@ def vm_update_os_metadata(dom, params): os_elem = E.os({"distro": distro, "version": version}) set_metadata_node(dom, os_elem) + def _set_ticket(self, xml, params, flag=0): + DEFAULT_VALID_TO = 30 + password = params.get("passwd") + expire = params.get("expire") + root = objectify.fromstring(xml) + graphic = root.devices.find("graphics") + graphic.attrib['passwd'] = password + to = graphic.attrib.get('passwdValidTo') + if to is not None: + if (time.mktime(time.strptime(to, '%Y-%m-%dT%H:%M:%S')) + - time.time() <= 0): + expire = expire if expire is not None else DEFAULT_VALID_TO + + if expire is not None: + expire_time = time.gmtime(time.time() + float(expire)) + valid_to = time.strftime('%Y-%m-%dT%H:%M:%S', expire_time) + graphic.attrib['passwdValidTo'] = valid_to + + return root if flag == 0 else graphic + + def _set_persistent_ticket(self, dom, xml, params): + if 'ticket' not in params: + # store the password for copy + ticket = self._get_ticket(dom) + if ticket['passwd'] is None: + return xml + else: + params['ticket'] = ticket + node = self._set_ticket(xml, params['ticket']) + return ET.tostring(node, encoding="utf-8") + + def _set_live_ticket(self, dom, params): + if 'ticket' not in params: + return + if dom.isActive(): + xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE) + flag = libvirt.VIR_DOMAIN_XML_SECURE + node = self._set_ticket(xml, params['ticket'], flag) + dom.updateDeviceFlags(etree.tostring(node), + libvirt.VIR_DOMAIN_AFFECT_LIVE) + def _static_vm_update(self, dom, params): state = DOM_STATE_MAP[dom.info()[0]] old_xml = new_xml = dom.XMLDesc(0) @@ -319,6 +369,8 @@ def _static_vm_update(self, dom, params): xpath = VM_STATIC_UPDATE_PARAMS[key] new_xml = xmlutils.xml_item_update(new_xml, xpath, val) + new_xml = self._set_persistent_ticket(dom, new_xml, params) + conn = self.conn.get() try: if 'name' in params: @@ -340,11 +392,26 @@ def _static_vm_update(self, dom, params): def _live_vm_update(self, dom, params): self._vm_update_access_metadata(dom, params) + self._set_live_ticket(dom, params) def _has_video(self, dom): dom = ElementTree.fromstring(dom.XMLDesc(0)) return dom.find('devices/video') is not None + def _get_ticket(self, dom): + xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE) + root = objectify.fromstring(xml) + graphic = root.devices.find("graphics") + passwd = graphic.attrib.get('passwd') + ticket = {"passwd": passwd} + valid_to = graphic.attrib.get('passwdValidTo') + ticket['expire'] = None + if valid_to is not None: + to = time.mktime(time.strptime(valid_to, '%Y-%m-%dT%H:%M:%S')) + ticket['expire'] = to - time.mktime(time.gmtime()) + + return ticket + def lookup(self, name): dom = self.get_vm(name, self.conn) info = dom.info() @@ -394,6 +461,7 @@ def lookup(self, name): 'graphics': {"type": graphics_type, "listen": graphics_listen, "port": graphics_port}, + 'ticket': self._get_ticket(dom), 'users': users, 'groups': groups, 'access': 'full' -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> Support lookup and update ticket for vm. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 0e45d1e..2fa722d 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -88,6 +88,15 @@ def reset(self): def _static_vm_update(self, dom, params): state = dom.info['state'] + if 'ticket' in params: + ticket = params.pop('ticket') + passwd = ticket.get('passwd') + dom.ticket["passwd"] = passwd if passwd is not None else "".join( + random.sample(string.ascii_letters + string.digits, 8)) + expire = ticket.get('expire') + dom.ticket["ValidTo"] = expire if expire is None else round( + time.time()) + expire + for key, val in params.items(): if key == 'name': if state == 'running' or params['name'] in self.vms_get_list(): @@ -129,6 +138,10 @@ def vm_lookup(self, name): vm.info['screenshot'] = self.vmscreenshot_lookup(name) else: vm.info['screenshot'] = None + vm.info['ticket'] = {"passwd": vm.ticket["passwd"]} + validTo = vm.ticket["ValidTo"] + vm.info['ticket']["expire"] = (validTo - round(time.time()) + if validTo is not None else None) return vm.info def vm_delete(self, name): @@ -1033,6 +1046,7 @@ def __init__(self, uuid, name, template_info): self.networks = template_info['networks'] ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} + self.ticket = {"passwd": "123456", "ValidTo": None} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) stats = {'cpu_utilization': 20, -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> update test_model and test_rest Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- tests/test_model.py | 15 +++++++++++++++ tests/test_rest.py | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 30daafa..d773c00 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -702,10 +702,25 @@ def test_vm_edit(self): vms = inst.vms_get_list() self.assertTrue('kimchi-vm1' in vms) + # update vm ticket when vm is not running + inst.vm_update(u'kimchi-vm1', + {"ticket": {"passwd": "123456"}}) + inst.vm_start('kimchi-vm1') rollback.prependDefer(self._rollback_wrapper, inst.vm_poweroff, 'kimchi-vm1') + vm_info = inst.vm_lookup(u'kimchi-vm1') + self.assertEquals('123456', vm_info['ticket']["passwd"]) + self.assertEquals(None, vm_info['ticket']["expire"]) + + # update vm ticket when vm is running + inst.vm_update(u'kimchi-vm1', + {"ticket": {"passwd": "abcdef", "expire": 20}}) + vm_info = inst.vm_lookup(u'kimchi-vm1') + self.assertEquals('abcdef', vm_info['ticket']["passwd"]) + self.assertGreaterEqual(20, vm_info['ticket']['expire']) + info = inst.vm_lookup('kimchi-vm1') self.assertEquals('running', info['state']) diff --git a/tests/test_rest.py b/tests/test_rest.py index 54209ef..6f93941 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -218,9 +218,21 @@ def test_edit_vm(self): resp = self.request('/vms/vm-1', req, 'PUT') self.assertEquals(200, resp.status) + req = json.dumps({"ticket": {'passwd': "abcdef"}}) + resp = self.request('/vms/vm-1', req, 'PUT') + info = json.loads(resp.read()) + self.assertEquals('abcdef', info["ticket"]["passwd"]) + self.assertEquals(None, info["ticket"]["expire"]) + resp = self.request('/vms/vm-1/poweroff', '{}', 'POST') self.assertEquals(200, resp.status) + req = json.dumps({"ticket": {'passwd': "123456", "expire": 20}}) + resp = self.request('/vms/vm-1', req, 'PUT') + info = json.loads(resp.read()) + self.assertEquals('123456', info["ticket"]["passwd"]) + self.assertGreaterEqual(20, info["ticket"]["expire"]) + req = json.dumps({'name': 12}) resp = self.request('/vms/vm-1', req, 'PUT') self.assertEquals(400, resp.status) -- 1.9.3

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 07/21/2014 12:51 PM, shaohef@linux.vnet.ibm.com wrote:
From: ShaoHe Feng <shaohef@linux.vnet.ibm.com>
V2 -> V3: do not make ticket as sub-resource of a VM
V1 -> V2: make ticket as sub-resource of a VM
A ticket is the credential to access VM. Only who get the ticket can access a VM.
test this patch set: set the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ \ -X PUT -d '{"ticket": {"passwd": "abcd"}}'
get the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/
ShaoHe Feng (5): vm ticket in backend: update API.md vm ticket in backend: update controller and API.json vm ticket in backend: update model vm ticket in backend: update mockmodel vm ticket in backend: update test case
docs/API.md | 7 +++++ src/kimchi/API.json | 16 +++++++++++ src/kimchi/control/vms.py | 3 ++- src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 14 ++++++++++ src/kimchi/model/vms.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_model.py | 15 +++++++++++ tests/test_rest.py | 12 +++++++++ 8 files changed, 136 insertions(+), 1 deletion(-)

The tests are failing: ====================================================================== ERROR: test_vm_info (test_model.ModelTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/alinefm/kimchi/tests/test_model.py", line 72, in test_vm_info info = inst.vm_lookup('test') File "/home/alinefm/kimchi/src/kimchi/model/vms.py", line 464, in lookup 'ticket': self._get_ticket(dom), File "/home/alinefm/kimchi/src/kimchi/model/vms.py", line 405, in _get_ticket passwd = graphic.attrib.get('passwd') AttributeError: 'NoneType' object has no attribute 'attrib' ====================================================================== FAIL: test_vm_info (test_mockmodel.MockModelTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/alinefm/kimchi/tests/test_mockmodel.py", line 149, in test_vm_info self.assertEquals(keys, set(info.keys())) AssertionError: Items in the second set but not the first: 'ticket' ---------------------------------------------------------------------- Ran 159 tests in 266.996s On 07/21/2014 12:51 PM, shaohef@linux.vnet.ibm.com wrote:
From: ShaoHe Feng <shaohef@linux.vnet.ibm.com>
V2 -> V3: do not make ticket as sub-resource of a VM
V1 -> V2: make ticket as sub-resource of a VM
A ticket is the credential to access VM. Only who get the ticket can access a VM.
test this patch set: set the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/ \ -X PUT -d '{"ticket": {"passwd": "abcd"}}'
get the ticket $ sudo curl -k -u <user>:<password> -H "Content-Type: application/json" -H \ "Accept: application/json" https://localhost:8001/vms/test-vm-8/
ShaoHe Feng (5): vm ticket in backend: update API.md vm ticket in backend: update controller and API.json vm ticket in backend: update model vm ticket in backend: update mockmodel vm ticket in backend: update test case
docs/API.md | 7 +++++ src/kimchi/API.json | 16 +++++++++++ src/kimchi/control/vms.py | 3 ++- src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 14 ++++++++++ src/kimchi/model/vms.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_model.py | 15 +++++++++++ tests/test_rest.py | 12 +++++++++ 8 files changed, 136 insertions(+), 1 deletion(-)
participants (2)
-
Aline Manera
-
shaohef@linux.vnet.ibm.com