[PATCH v2] [Kimchi 0/9] Virt-Viewer launcher backend

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> v2: - fixed typo and capital letters in i18n.py - fixed 'double dict' error in virtviewer module - added tests with virtual machine containing utf-8 characters This patch set adds the Virt-Viewer launcher backend to Kimchi. This feature consists of a new resource located in: **URI:** /plugins/kimchi/vms/*:name*/snapshots/current that retrieves a download link to a .vv file to be used by a Virt-Viewer compatible desktop app to connect to the remote virtual machine. This backend takes cares of handling firewall rules to allow the connection to be succesfull. Note that no firewall port will be opened unless a download is made - if the user decides to use noVNC or spice-html5 instead Kimchi will not touch the host firewall. Example: [danielhb@arthas kimchi]$ curl -u root -H "Content-Type: application/json" -H "Accept: application/json" -X GET "http://localhost:8010/plugins/kimchi/vms/OpenSUSE-Leap-42.1/virtviewerfile" Enter host password for user 'root': [virt-viewer] type=vnc host=localhost port=5904 After this call, port 5904 was opened in the host to allow for a virt-viewer connection. When shutting down the virtual machine or WoK, a cleanup is made to close any ports left opened. Daniel Henrique Barboza (9): Virt-Viewer launcher: docs and i18n changes Virt-Viewer launcher: Makefile and config changes Virt-Viewer launcher: control/vms.py and model/vms.py changes Virt-Viewer launcher: virtviewerfile module Virt-Viewer launcher: test changes Virt-Viewer launcher: adding FirewallManager class Virt-Viewer launcher: test changes for firewall manager Virt-Viewer launcher: libvirt events to control firewall Virt-Viewer launcher: changes after adding libvirt event listening Makefile.am | 2 + config.py.in | 10 ++- control/vms.py | 12 +++ docs/API.md | 6 ++ i18n.py | 2 + model/virtviewerfile.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++ model/vms.py | 9 +- tests/test_config.py.in | 6 ++ tests/test_model.py | 228 +++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 503 insertions(+), 6 deletions(-) create mode 100644 model/virtviewerfile.py -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds the documentation and i18n changes for the new Virt-Viewer launcher API. URI: plugins/kimchi/vms/*:name*/virtviewerfile 'Retrieve the Virt-Viewer launcher file to connect to the graphics of this virtual machine. Virtual machine must be running.' Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- docs/API.md | 6 ++++++ i18n.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/API.md b/docs/API.md index 84ef7c9..78c0574 100644 --- a/docs/API.md +++ b/docs/API.md @@ -286,6 +286,12 @@ Represents a snapshot of the Virtual Machine's primary monitor. **URI:** /plugins/kimchi/vms/*:name*/snapshots/current * **GET**: Retrieve current snapshot information for the virtual machine. +### Sub-resource: Virt-Viewer File +**URI:** /plugins/kimchi/vms/*:name*/virtviewerfile +* **GET**: Retrieve the Virt-Viewer launcher file to connect to the + graphics of this virtual machine. Virtual machine must + be running. + ### Collection: Templates **URI:** /plugins/kimchi/templates diff --git a/i18n.py b/i18n.py index 08a3ac8..5dc47b6 100644 --- a/i18n.py +++ b/i18n.py @@ -131,6 +131,8 @@ messages = { "KCHVM0080E": _("Cannot update Maximum Memory when guest is running."), "KCHVM0081E": _("Impossible to create %(dir)s directory."), "KCHVM0082E": _("Either the guest %(name)s did not start to listen to the serial or it is not configured to use the serial console."), + "KCHVM0083E": _("Unable to retrieve Virt Viewer file for stopped virtual machine %(name)s"), + "KCHVM0084E": _("Error occured while retrieving the Virt Viewer file for virtual machine %(name)s : %(err)s"), "KCHVMHDEV0001E": _("VM %(vmid)s does not contain directly assigned host device %(dev_name)s."), "KCHVMHDEV0002E": _("The host device %(dev_name)s is not allowed to directly assign to VM."), -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> The Virt-Viewer API will store the generated launcher files into the 'data/virtviewerfiles' directory. This patch adds this new dir to Makefile.am and config.py.in files. tests/test_config.py.in was also changed to reflect this new directory. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- Makefile.am | 2 ++ config.py.in | 10 +++++++++- tests/test_config.py.in | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 1b46773..eabf6ae 100644 --- a/Makefile.am +++ b/Makefile.am @@ -119,6 +119,7 @@ install-deb: install mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virtviewerfiles deb: contrib/make-deb.sh @@ -168,6 +169,7 @@ install-data-local: mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virviewerfiles uninstall-local: @if test -f $(DESTDIR)/etc/systemd/system/wokd.service.d/kimchi.conf; then \ diff --git a/config.py.in b/config.py.in index 937f1ec..a887670 100644 --- a/config.py.in +++ b/config.py.in @@ -57,6 +57,10 @@ def get_screenshot_path(): return os.path.join(PluginPaths('kimchi').state_dir, 'screenshots') +def get_virtviewerfiles_path(): + return os.path.join(PluginPaths('kimchi').state_dir, 'virtviewerfiles') + + def get_config(): plugin_conf = PluginPaths('kimchi').conf_file return Parser().dict_from_file(plugin_conf) @@ -152,7 +156,11 @@ class KimchiConfig(PluginConfig): '/data/screenshots': {'tools.nocache.on': False, 'tools.staticdir.dir': get_screenshot_path(), - 'tools.staticdir.on': True}} + 'tools.staticdir.on': True}, + '/data/virtviewerfiles': {'tools.nocache.on': False, + 'tools.staticdir.dir': + get_virtviewerfiles_path(), + 'tools.staticdir.on': True}} for uri, data in static_config.iteritems(): custom_config[uri] = {'tools.nocache.on': True, 'tools.wokauth.on': True} diff --git a/tests/test_config.py.in b/tests/test_config.py.in index a6728e8..48f5579 100644 --- a/tests/test_config.py.in +++ b/tests/test_config.py.in @@ -24,6 +24,7 @@ from wok.config import CACHEEXPIRES from wok.config import Paths, PluginPaths from wok.plugins.kimchi.config import get_screenshot_path +from wok.plugins.kimchi.config import get_virtviewerfiles_path from wok.plugins.kimchi.config import KimchiConfig, KimchiPaths @@ -151,6 +152,11 @@ class ConfigTests(unittest.TestCase): 'tools.staticdir.dir': get_screenshot_path(), 'tools.staticdir.on': True }, + '/data/virtviewerfiles': { + 'tools.nocache.on': False, + 'tools.staticdir.dir': get_virtviewerfiles_path(), + 'tools.staticdir.on': True + }, '/ui/config/tab-ext.xml': { 'tools.nocache.on': True, 'tools.staticfile.on': True, -- 2.5.5

On Jul 08 12:10PM, dhbarboza82@gmail.com wrote:
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
The Virt-Viewer API will store the generated launcher files into the 'data/virtviewerfiles' directory. This patch adds this new dir to Makefile.am and config.py.in files.
tests/test_config.py.in was also changed to reflect this new directory.
Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- Makefile.am | 2 ++ config.py.in | 10 +++++++++- tests/test_config.py.in | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/Makefile.am b/Makefile.am index 1b46773..eabf6ae 100644 --- a/Makefile.am +++ b/Makefile.am @@ -119,6 +119,7 @@ install-deb: install mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virtviewerfiles
deb: contrib/make-deb.sh @@ -168,6 +169,7 @@ install-data-local: mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virviewerfiles
Missing 't' in the name of the directory.
uninstall-local: @if test -f $(DESTDIR)/etc/systemd/system/wokd.service.d/kimchi.conf; then \ diff --git a/config.py.in b/config.py.in index 937f1ec..a887670 100644 --- a/config.py.in +++ b/config.py.in @@ -57,6 +57,10 @@ def get_screenshot_path(): return os.path.join(PluginPaths('kimchi').state_dir, 'screenshots')
+def get_virtviewerfiles_path(): + return os.path.join(PluginPaths('kimchi').state_dir, 'virtviewerfiles') + + def get_config(): plugin_conf = PluginPaths('kimchi').conf_file return Parser().dict_from_file(plugin_conf) @@ -152,7 +156,11 @@ class KimchiConfig(PluginConfig): '/data/screenshots': {'tools.nocache.on': False, 'tools.staticdir.dir': get_screenshot_path(), - 'tools.staticdir.on': True}} + 'tools.staticdir.on': True}, + '/data/virtviewerfiles': {'tools.nocache.on': False, + 'tools.staticdir.dir': + get_virtviewerfiles_path(), + 'tools.staticdir.on': True}} for uri, data in static_config.iteritems(): custom_config[uri] = {'tools.nocache.on': True, 'tools.wokauth.on': True} diff --git a/tests/test_config.py.in b/tests/test_config.py.in index a6728e8..48f5579 100644 --- a/tests/test_config.py.in +++ b/tests/test_config.py.in @@ -24,6 +24,7 @@ from wok.config import CACHEEXPIRES from wok.config import Paths, PluginPaths
from wok.plugins.kimchi.config import get_screenshot_path +from wok.plugins.kimchi.config import get_virtviewerfiles_path from wok.plugins.kimchi.config import KimchiConfig, KimchiPaths
@@ -151,6 +152,11 @@ class ConfigTests(unittest.TestCase): 'tools.staticdir.dir': get_screenshot_path(), 'tools.staticdir.on': True }, + '/data/virtviewerfiles': { + 'tools.nocache.on': False, + 'tools.staticdir.dir': get_virtviewerfiles_path(), + 'tools.staticdir.on': True + }, '/ui/config/tab-ext.xml': { 'tools.nocache.on': True, 'tools.staticfile.on': True, -- 2.5.5
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
-- Paulo Ricardo Paz Vital Linux Technology Center, IBM Systems http://www.ibm.com/linux/ltc/

On 07/08/2016 03:36 PM, Paulo Ricardo Paz Vital wrote:
On Jul 08 12:10PM, dhbarboza82@gmail.com wrote:
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
The Virt-Viewer API will store the generated launcher files into the 'data/virtviewerfiles' directory. This patch adds this new dir to Makefile.am and config.py.in files.
tests/test_config.py.in was also changed to reflect this new directory.
Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- Makefile.am | 2 ++ config.py.in | 10 +++++++++- tests/test_config.py.in | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/Makefile.am b/Makefile.am index 1b46773..eabf6ae 100644 --- a/Makefile.am +++ b/Makefile.am @@ -119,6 +119,7 @@ install-deb: install mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virtviewerfiles
deb: contrib/make-deb.sh @@ -168,6 +169,7 @@ install-data-local: mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/ws-tokens mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/screenshots mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/isos + mkdir -p $(DESTDIR)/$(localstatedir)/lib/kimchi/virviewerfiles Missing 't' in the name of the directory. Good eye!
uninstall-local: @if test -f $(DESTDIR)/etc/systemd/system/wokd.service.d/kimchi.conf; then \ diff --git a/config.py.in b/config.py.in index 937f1ec..a887670 100644 --- a/config.py.in +++ b/config.py.in @@ -57,6 +57,10 @@ def get_screenshot_path(): return os.path.join(PluginPaths('kimchi').state_dir, 'screenshots')
+def get_virtviewerfiles_path(): + return os.path.join(PluginPaths('kimchi').state_dir, 'virtviewerfiles') + + def get_config(): plugin_conf = PluginPaths('kimchi').conf_file return Parser().dict_from_file(plugin_conf) @@ -152,7 +156,11 @@ class KimchiConfig(PluginConfig): '/data/screenshots': {'tools.nocache.on': False, 'tools.staticdir.dir': get_screenshot_path(), - 'tools.staticdir.on': True}} + 'tools.staticdir.on': True}, + '/data/virtviewerfiles': {'tools.nocache.on': False, + 'tools.staticdir.dir': + get_virtviewerfiles_path(), + 'tools.staticdir.on': True}} for uri, data in static_config.iteritems(): custom_config[uri] = {'tools.nocache.on': True, 'tools.wokauth.on': True} diff --git a/tests/test_config.py.in b/tests/test_config.py.in index a6728e8..48f5579 100644 --- a/tests/test_config.py.in +++ b/tests/test_config.py.in @@ -24,6 +24,7 @@ from wok.config import CACHEEXPIRES from wok.config import Paths, PluginPaths
from wok.plugins.kimchi.config import get_screenshot_path +from wok.plugins.kimchi.config import get_virtviewerfiles_path from wok.plugins.kimchi.config import KimchiConfig, KimchiPaths
@@ -151,6 +152,11 @@ class ConfigTests(unittest.TestCase): 'tools.staticdir.dir': get_screenshot_path(), 'tools.staticdir.on': True }, + '/data/virtviewerfiles': { + 'tools.nocache.on': False, + 'tools.staticdir.dir': get_virtviewerfiles_path(), + 'tools.staticdir.on': True + }, '/ui/config/tab-ext.xml': { 'tools.nocache.on': True, 'tools.staticfile.on': True, -- 2.5.5
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> control/vms.py: - added a new resource called VMVirtViewerFile that returns an URL to download the virt-viewer launcher file. - added a new URL called 'virtviewerfile' to allow access to the new VMVirtViewerFile resource. model/vms.py: - turned get_graphics() into a staticmethod to be used outside of the vms.py module. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- control/vms.py | 12 ++++++++++++ model/vms.py | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/control/vms.py b/control/vms.py index bbf0fa4..645cb40 100644 --- a/control/vms.py +++ b/control/vms.py @@ -65,6 +65,7 @@ class VM(Resource): super(VM, self).__init__(model, ident) self.role_key = 'guests' self.screenshot = VMScreenShot(model, ident) + self.virtviewerfile = VMVirtViewerFile(model, ident) self.uri_fmt = '/vms/%s' for ident, node in sub_nodes.items(): setattr(self, ident, node(model, self.ident)) @@ -103,3 +104,14 @@ class VMScreenShot(Resource): self.lookup() internal_uri = self.info.replace('plugins/kimchi', '') raise internal_redirect(internal_uri) + + +class VMVirtViewerFile(Resource): + def __init__(self, model, ident): + super(VMVirtViewerFile, self).__init__(model, ident) + self.role_key = 'guests' + + @property + def data(self): + internal_uri = self.info.replace('plugins/kimchi', '') + raise internal_redirect(internal_uri) diff --git a/model/vms.py b/model/vms.py index 6a309fc..e0b0a37 100644 --- a/model/vms.py +++ b/model/vms.py @@ -1154,7 +1154,7 @@ class VMModel(object): state = DOM_STATE_MAP[info[0]] screenshot = None # (type, listen, port, passwd, passwdValidTo) - graphics = self._vm_get_graphics(name) + graphics = self.get_graphics(name, self.conn) graphics_port = graphics[2] graphics_port = graphics_port if state == 'running' else None try: @@ -1426,8 +1426,9 @@ class VMModel(object): return True - def _vm_get_graphics(self, name): - dom = self.get_vm(name, self.conn) + @staticmethod + def get_graphics(name, conn): + dom = VMModel.get_vm(name, conn) xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE) expr = "/domain/devices/graphics/@type" @@ -1492,7 +1493,7 @@ class VMModel(object): def connect(self, name): # (type, listen, port, passwd, passwdValidTo) - graphics_port = self._vm_get_graphics(name)[2] + graphics_port = self.get_graphics(name, self.conn)[2] if graphics_port is not None: websocket.add_proxy_token(name.encode('utf-8'), graphics_port) else: -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This new module contains functions to create virt viewer files and a new model called VMVirtViewerFileModel that will return a link to the new generated script file in the lookup() method. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/virtviewerfile.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 model/virtviewerfile.py diff --git a/model/virtviewerfile.py b/model/virtviewerfile.py new file mode 100644 index 0000000..9b2a9bd --- /dev/null +++ b/model/virtviewerfile.py @@ -0,0 +1,100 @@ +# +# Project Kimchi +# +# Copyright IBM Corp, 2016 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import cherrypy +import libvirt +import os + +from wok.config import config as wok_config +from wok.exception import NotFoundError, OperationFailed +from wok.plugins.kimchi import config as kimchi_config +from wok.plugins.kimchi.model.vms import VMModel + + +def write_virt_viewer_file(params): + file_template = """\ +[virt-viewer] +type=%(type)s +host=%(host)s +port=%(graphics_port)s +""" + file_contents = file_template % params + + if params.get('graphics_passwd'): + file_contents += 'password=%s\n' % params['graphics_passwd'] + + try: + with open(params.get('path'), 'w') as vv_file: + vv_file.write(file_contents) + except Exception: + raise + + +def _get_request_host(): + host = cherrypy.request.headers.get('Host') + if not host: + host = wok_config.get("server", "host") + host = host.split(':')[0] + return host + + +def create_virt_viewer_file(vm_name, graphics_info): + graphics_type = graphics_info[0] + graphics_port = graphics_info[2] + graphics_passwd = graphics_info[3] + + try: + host = _get_request_host() + + default_dir = kimchi_config.get_virtviewerfiles_path() + file_path = os.path.join(default_dir, '%s-access.vv' % vm_name) + + file_params = { + 'type': graphics_type, + 'graphics_port': graphics_port, + 'graphics_passwd': graphics_passwd, + 'host': host, + 'path': file_path + } + write_virt_viewer_file(file_params) + return file_path + + except Exception as e: + raise OperationFailed("KCHVM0084E", + {'name': vm_name, 'err': e.message}) + + +class VMVirtViewerFileModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _check_if_vm_running(self, name): + dom = VMModel.get_vm(name, self.conn) + d_info = dom.info() + if d_info[0] != libvirt.VIR_DOMAIN_RUNNING: + raise NotFoundError("KCHVM0083E", {'name': name}) + + def lookup(self, name): + self._check_if_vm_running(name) + graphics_info = VMModel.get_graphics(name, self.conn) + file_path = create_virt_viewer_file(name, graphics_info) + + return 'plugins/kimchi/data/virtviewerfiles/%s' %\ + os.path.basename(file_path) -- 2.5.5

On Jul 08 12:10PM, dhbarboza82@gmail.com wrote:
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
This new module contains functions to create virt viewer files and a new model called VMVirtViewerFileModel that will return a link to the new generated script file in the lookup() method.
Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/virtviewerfile.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 model/virtviewerfile.py
diff --git a/model/virtviewerfile.py b/model/virtviewerfile.py new file mode 100644 index 0000000..9b2a9bd --- /dev/null +++ b/model/virtviewerfile.py @@ -0,0 +1,100 @@ +# +# Project Kimchi +# +# Copyright IBM Corp, 2016 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import cherrypy +import libvirt +import os + +from wok.config import config as wok_config +from wok.exception import NotFoundError, OperationFailed +from wok.plugins.kimchi import config as kimchi_config +from wok.plugins.kimchi.model.vms import VMModel + + +def write_virt_viewer_file(params): + file_template = """\ +[virt-viewer] +type=%(type)s +host=%(host)s +port=%(graphics_port)s +""" + file_contents = file_template % params + + if params.get('graphics_passwd'): + file_contents += 'password=%s\n' % params['graphics_passwd'] + + try: + with open(params.get('path'), 'w') as vv_file: + vv_file.write(file_contents) + except Exception: + raise + + +def _get_request_host(): + host = cherrypy.request.headers.get('Host') + if not host: + host = wok_config.get("server", "host") + host = host.split(':')[0] + return host + + +def create_virt_viewer_file(vm_name, graphics_info): + graphics_type = graphics_info[0] + graphics_port = graphics_info[2] + graphics_passwd = graphics_info[3] + + try: + host = _get_request_host() + + default_dir = kimchi_config.get_virtviewerfiles_path() + file_path = os.path.join(default_dir, '%s-access.vv' % vm_name) + + file_params = { + 'type': graphics_type, + 'graphics_port': graphics_port, + 'graphics_passwd': graphics_passwd, + 'host': host, + 'path': file_path + } + write_virt_viewer_file(file_params) + return file_path + + except Exception as e: + raise OperationFailed("KCHVM0084E", + {'name': vm_name, 'err': e.message}) + + +class VMVirtViewerFileModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _check_if_vm_running(self, name): + dom = VMModel.get_vm(name, self.conn) + d_info = dom.info() + if d_info[0] != libvirt.VIR_DOMAIN_RUNNING: + raise NotFoundError("KCHVM0083E", {'name': name})
Is really necessary implement this function? Isn't there any other way to get the information from any other class/method already implemented? Not a big deal, but only a way reuse already methods.
+ + def lookup(self, name): + self._check_if_vm_running(name) + graphics_info = VMModel.get_graphics(name, self.conn) + file_path = create_virt_viewer_file(name, graphics_info) + + return 'plugins/kimchi/data/virtviewerfiles/%s' %\ + os.path.basename(file_path) -- 2.5.5
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel
-- Paulo Ricardo Paz Vital Linux Technology Center, IBM Systems http://www.ibm.com/linux/ltc/

On 07/08/2016 03:51 PM, Paulo Ricardo Paz Vital wrote:
On Jul 08 12:10PM, dhbarboza82@gmail.com wrote:
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
This new module contains functions to create virt viewer files and a new model called VMVirtViewerFileModel that will return a link to the new generated script file in the lookup() method.
Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/virtviewerfile.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 model/virtviewerfile.py
diff --git a/model/virtviewerfile.py b/model/virtviewerfile.py new file mode 100644 index 0000000..9b2a9bd --- /dev/null +++ b/model/virtviewerfile.py @@ -0,0 +1,100 @@ +# +# Project Kimchi +# +# Copyright IBM Corp, 2016 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import cherrypy +import libvirt +import os + +from wok.config import config as wok_config +from wok.exception import NotFoundError, OperationFailed +from wok.plugins.kimchi import config as kimchi_config +from wok.plugins.kimchi.model.vms import VMModel + + +def write_virt_viewer_file(params): + file_template = """\ +[virt-viewer] +type=%(type)s +host=%(host)s +port=%(graphics_port)s +""" + file_contents = file_template % params + + if params.get('graphics_passwd'): + file_contents += 'password=%s\n' % params['graphics_passwd'] + + try: + with open(params.get('path'), 'w') as vv_file: + vv_file.write(file_contents) + except Exception: + raise + + +def _get_request_host(): + host = cherrypy.request.headers.get('Host') + if not host: + host = wok_config.get("server", "host") + host = host.split(':')[0] + return host + + +def create_virt_viewer_file(vm_name, graphics_info): + graphics_type = graphics_info[0] + graphics_port = graphics_info[2] + graphics_passwd = graphics_info[3] + + try: + host = _get_request_host() + + default_dir = kimchi_config.get_virtviewerfiles_path() + file_path = os.path.join(default_dir, '%s-access.vv' % vm_name) + + file_params = { + 'type': graphics_type, + 'graphics_port': graphics_port, + 'graphics_passwd': graphics_passwd, + 'host': host, + 'path': file_path + } + write_virt_viewer_file(file_params) + return file_path + + except Exception as e: + raise OperationFailed("KCHVM0084E", + {'name': vm_name, 'err': e.message}) + + +class VMVirtViewerFileModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _check_if_vm_running(self, name): + dom = VMModel.get_vm(name, self.conn) + d_info = dom.info() + if d_info[0] != libvirt.VIR_DOMAIN_RUNNING: + raise NotFoundError("KCHVM0083E", {'name': name}) Is really necessary implement this function? Isn't there any other way to get the information from any other class/method already implemented?
Not a big deal, but only a way reuse already methods.
The only backend code I saw that checks whether the VM is running or not is an internal implementation of serialconsole.py: def is_running(self): """ Checks if this guest is currently in a running state. """ return self._guest.state(0)[0] == libvirt.VIR_DOMAIN_RUNNING or \ self._guest.state(0)[0] == libvirt.VIR_DOMAIN_PAUSED This method can't be turned to static easily because it relies on the internal _guest attribute. We can create a staticmethod on vms.py and make everyone use it. I just need to check if the PAUSED condition applies to this feature too.
+ + def lookup(self, name): + self._check_if_vm_running(name) + graphics_info = VMModel.get_graphics(name, self.conn) + file_path = create_virt_viewer_file(name, graphics_info) + + return 'plugins/kimchi/data/virtviewerfiles/%s' %\ + os.path.basename(file_path) -- 2.5.5
_______________________________________________ Kimchi-devel mailing list Kimchi-devel@ovirt.org http://lists.ovirt.org/mailman/listinfo/kimchi-devel

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds 3 new unit tests in test_model.py to test the new VirtViewerModel. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- tests/test_model.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index 9a6a1aa..2fc1d38 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -18,15 +18,20 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import __builtin__ as builtins + import grp import lxml.etree as ET import os import pwd +import mock import re import shutil import time import unittest +from mock import call, mock_open, patch + import tests.utils as utils import wok.objectstore @@ -43,6 +48,7 @@ from wok.plugins.kimchi import osinfo from wok.plugins.kimchi.config import kimchiPaths as paths from wok.plugins.kimchi.model import model from wok.plugins.kimchi.model.libvirtconnection import LibvirtConnection +from wok.plugins.kimchi.model.virtviewerfile import VMVirtViewerFileModel from wok.plugins.kimchi.model.vms import VMModel import iso_gen @@ -108,7 +114,8 @@ class ModelTests(unittest.TestCase): # test_rest or test_mockmodel to avoid overriding problems LibvirtConnection._connections['test:///default'] = {} - os.unlink(self.tmp_store) + if os.path.isfile(self.tmp_store): + os.unlink(self.tmp_store) def test_vm_info(self): inst = model.Model('test:///default', self.tmp_store) @@ -338,6 +345,94 @@ class ModelTests(unittest.TestCase): inst.template_delete('test') + @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') + def test_vm_virtviewerfile_vmnotrunning(self): + inst = model.Model(objstore_loc=self.tmp_store) + params = {'name': 'test', + 'source_media': {'type': 'disk', 'path': UBUNTU_ISO}} + inst.templates_create(params) + + with RollbackContext() as rollback: + params = {'name': 'kimchi-vnc', + 'template': '/plugins/kimchi/templates/test'} + task1 = inst.vms_create(params) + inst.task_wait(task1['id']) + rollback.prependDefer(inst.vm_delete, 'kimchi-vnc') + + expected_error_msg = "KCHVM0083E" + with self.assertRaisesRegexp(NotFoundError, expected_error_msg): + vvmodel = VMVirtViewerFileModel(conn=inst.conn) + vvmodel.lookup('kimchi-vnc') + + inst.template_delete('test') + + @mock.patch('wok.plugins.kimchi.model.virtviewerfile._get_request_host') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMModel.get_graphics') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMVirtViewerFileModel._check_if_vm_running') + def test_vm_virtviewerfile_vnc(self, mock_vm_running, mock_get_graphics, + mock_get_host): + + mock_get_host.return_value = 'kimchi-test-host' + mock_get_graphics.return_value = ['vnc', 'listen', '5999', None] + mock_vm_running.return_value = True + + vvmodel = VMVirtViewerFileModel(conn=None) + + open_ = mock_open(read_data='') + with patch.object(builtins, 'open', open_): + vvfilepath = vvmodel.lookup('kimchi-vm') + + self.assertEqual( + vvfilepath, + 'plugins/kimchi/data/virtviewerfiles/kimchi-vm-access.vv' + ) + + expected_write_content = "[virt-viewer]\ntype=vnc\n"\ + "host=kimchi-test-host\nport=5999\n" + self.assertEqual( + open_().write.mock_calls, [call(expected_write_content)] + ) + + mock_get_graphics.assert_called_once_with('kimchi-vm', None) + mock_vm_running.assert_called_once_with('kimchi-vm') + + @mock.patch('wok.plugins.kimchi.model.virtviewerfile._get_request_host') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMModel.get_graphics') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMVirtViewerFileModel._check_if_vm_running') + def test_vm_virtviewerfile_spice_passwd(self, mock_vm_running, + mock_get_graphics, + mock_get_host): + + mock_get_host.return_value = 'kimchi-test-host' + mock_get_graphics.return_value = [ + 'spice', 'listen', '6660', 'spicepasswd' + ] + mock_vm_running.return_value = True + + vvmodel = VMVirtViewerFileModel(conn=None) + + open_ = mock_open(read_data='') + with patch.object(builtins, 'open', open_): + vvfilepath = vvmodel.lookup('kimchi-vm') + + self.assertEqual( + vvfilepath, + 'plugins/kimchi/data/virtviewerfiles/kimchi-vm-access.vv' + ) + + expected_write_content = "[virt-viewer]\ntype=spice\n"\ + "host=kimchi-test-host\nport=6660\npassword=spicepasswd\n" + self.assertEqual( + open_().write.mock_calls, [call(expected_write_content)] + ) + + mock_get_graphics.assert_called_once_with('kimchi-vm', None) + mock_vm_running.assert_called_once_with('kimchi-vm') + @unittest.skipUnless(utils.running_as_root(), "Must be run as root") def test_vm_serial(self): inst = model.Model(objstore_loc=self.tmp_store) -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> The FirewallManager class opens and closes firewall ports to allow for virt viewer connections in the graphics server of the VM. For Fedora distros and Ubuntu, 'firewall-cmd' and 'ufw' is used respectively. For all other distros, 'iptables' is used. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/virtviewerfile.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/model/virtviewerfile.py b/model/virtviewerfile.py index 9b2a9bd..03b6f19 100644 --- a/model/virtviewerfile.py +++ b/model/virtviewerfile.py @@ -26,6 +26,7 @@ from wok.config import config as wok_config from wok.exception import NotFoundError, OperationFailed from wok.plugins.kimchi import config as kimchi_config from wok.plugins.kimchi.model.vms import VMModel +from wok.utils import run_command, wok_log def write_virt_viewer_file(params): @@ -98,3 +99,96 @@ class VMVirtViewerFileModel(object): return 'plugins/kimchi/data/virtviewerfiles/%s' %\ os.path.basename(file_path) + + +class FirewallManager(object): + + @staticmethod + def check_if_firewall_cmd_enabled(): + _, _, r_code = run_command(['firewall-cmd', '--state', '-q']) + return r_code == 0 + + @staticmethod + def check_if_ufw_enabled(): + _, _, r_code = run_command(['ufw', 'status']) + return r_code == 0 + + def __init__(self): + self.opened_ports = {} + self.firewall_provider = None + + if self.check_if_firewall_cmd_enabled(): + self.firewall_provider = FirewallCMDProvider() + elif self.check_if_ufw_enabled(): + self.firewall_provider = UFWProvider() + else: + self.firewall_provider = IPTablesProvider() + + def add_vm_graphics_port(self, vm_name, port): + self.firewall_provider.enable_tcp_port(port) + self.opened_ports[vm_name] = port + + def remove_vm_graphics_port(self, vm_name): + port = self.opened_ports.pop(vm_name, None) + if port: + self.firewall_provider.disable_tcp_port(port) + + def remove_all_vms_ports(self): + for port in self.opened_ports.values(): + self.firewall_provider.disable_tcp_port(port) + + self.opened_ports = {} + + +class FirewallCMDProvider(object): + + @staticmethod + def enable_tcp_port(port): + _, err, r_code = run_command( + ['firewall-cmd', '--add-port=%s/tcp' % port] + ) + if r_code != 0: + wok_log.error('Error when adding port to firewall-cmd: %s' % err) + + @staticmethod + def disable_tcp_port(port): + _, err, r_code = run_command( + ['firewall-cmd', '--remove-port=%s/tcp' % port] + ) + if r_code != 0: + wok_log.error('Error when removing port from ' + 'firewall-cmd: %s' % err) + + +class UFWProvider(object): + + @staticmethod + def enable_tcp_port(port): + _, err, r_code = run_command(['ufw', 'allow', '%s/tcp' % port]) + if r_code != 0: + wok_log.error('Error when adding port to ufw: %s' % err) + + @staticmethod + def disable_tcp_port(port): + _, err, r_code = run_command(['ufw', 'deny', '%s/tcp' % port]) + if r_code != 0: + wok_log.error('Error when removing port from ufw: %s' % err) + + +class IPTablesProvider(object): + + @staticmethod + def enable_tcp_port(port): + cmd = ['iptables', '-I', 'INPUT', '-p', 'tcp', '--dport', + port, '-j', 'ACCEPT'] + _, err, r_code = run_command(cmd) + if r_code != 0: + wok_log.error('Error when adding port to iptables: %s' % err) + + @staticmethod + def disable_tcp_port(port): + cmd = ['iptables', '-D', 'INPUT', '-p', 'tcp', '--dport', + port, '-j', 'ACCEPT'] + _, err, r_code = run_command(cmd) + if r_code != 0: + wok_log.error('Error when removing port from itables: %s' % err) -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch adds unit tests for the FirewallManager class. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- tests/test_model.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 2fc1d38..1162d2c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -48,6 +48,7 @@ from wok.plugins.kimchi import osinfo from wok.plugins.kimchi.config import kimchiPaths as paths from wok.plugins.kimchi.model import model from wok.plugins.kimchi.model.libvirtconnection import LibvirtConnection +from wok.plugins.kimchi.model.virtviewerfile import FirewallManager from wok.plugins.kimchi.model.virtviewerfile import VMVirtViewerFileModel from wok.plugins.kimchi.model.vms import VMModel @@ -433,6 +434,76 @@ class ModelTests(unittest.TestCase): mock_get_graphics.assert_called_once_with('kimchi-vm', None) mock_vm_running.assert_called_once_with('kimchi-vm') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.run_command') + def test_firewall_provider_firewallcmd(self, mock_run_cmd): + mock_run_cmd.side_effect = [ + ['', '', 0], ['', '', 0], ['', '', 0] + ] + + fw_manager = FirewallManager() + fw_manager.add_vm_graphics_port('vm-name', 5905) + self.assertEqual(fw_manager.opened_ports, {'vm-name': 5905}) + + fw_manager.remove_vm_graphics_port('vm-name') + self.assertEqual(fw_manager.opened_ports, {}) + + mock_run_cmd.assert_has_calls( + [ + call(['firewall-cmd', '--state', '-q']), + call(['firewall-cmd', '--add-port=5905/tcp']), + call(['firewall-cmd', '--remove-port=5905/tcp']) + ] + ) + + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.run_command') + def test_firewall_provider_ufw(self, mock_run_cmd): + mock_run_cmd.side_effect = [ + ['', '', 1], ['', '', 0], ['', '', 0], ['', '', 0] + ] + + fw_manager = FirewallManager() + fw_manager.add_vm_graphics_port('vm-name', 5905) + self.assertEqual(fw_manager.opened_ports, {'vm-name': 5905}) + + fw_manager.remove_vm_graphics_port('vm-name') + self.assertEqual(fw_manager.opened_ports, {}) + + mock_run_cmd.assert_has_calls( + [ + call(['firewall-cmd', '--state', '-q']), + call(['ufw', 'status']), + call(['ufw', 'allow', '5905/tcp']), + call(['ufw', 'deny', '5905/tcp']) + ] + ) + + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.run_command') + def test_firewall_provider_iptables(self, mock_run_cmd): + mock_run_cmd.side_effect = [ + ['', '', 1], ['', '', 1], ['', '', 0], ['', '', 0] + ] + + fw_manager = FirewallManager() + fw_manager.add_vm_graphics_port('vm-name', 5905) + self.assertEqual(fw_manager.opened_ports, {'vm-name': 5905}) + + fw_manager.remove_vm_graphics_port('vm-name') + self.assertEqual(fw_manager.opened_ports, {}) + + iptables_add = ['iptables', '-I', 'INPUT', '-p', 'tcp', '--dport', + 5905, '-j', 'ACCEPT'] + + iptables_del = ['iptables', '-D', 'INPUT', '-p', 'tcp', '--dport', + 5905, '-j', 'ACCEPT'] + + mock_run_cmd.assert_has_calls( + [ + call(['firewall-cmd', '--state', '-q']), + call(['ufw', 'status']), + call(iptables_add), call(iptables_del) + ] + ) + @unittest.skipUnless(utils.running_as_root(), "Must be run as root") def test_vm_serial(self): inst = model.Model(objstore_loc=self.tmp_store) -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> In this patch, the FirewallManager class is used together with libvirt events to: - open the firewall port to the graphics server when a call to lookup() is made; - close the firewall port when the virtual machine is turned off. A cleanup() method was also added to the cherrypy 'exit' engine to ensure that no firewall port previously opened by the virtviewerfile module will be left opened when Kimchi/WoK exits. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- model/virtviewerfile.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/model/virtviewerfile.py b/model/virtviewerfile.py index 03b6f19..ad2168d 100644 --- a/model/virtviewerfile.py +++ b/model/virtviewerfile.py @@ -85,6 +85,16 @@ def create_virt_viewer_file(vm_name, graphics_info): class VMVirtViewerFileModel(object): def __init__(self, **kargs): self.conn = kargs['conn'] + self.firewall_mngr = FirewallManager() + self.vm_event_callbacks = {} + cherrypy.engine.subscribe('exit', self.cleanup) + + def cleanup(self): + wok_log.info('Closing any VNC/SPICE firewall ports ' + 'opened by Kimchi ...') + self.firewall_mngr.remove_all_vms_ports() + for cb_id in self.vm_event_callbacks.values(): + self.conn.get().domainEventDeregisterAny(cb_id) def _check_if_vm_running(self, name): dom = VMModel.get_vm(name, self.conn) @@ -92,11 +102,41 @@ class VMVirtViewerFileModel(object): if d_info[0] != libvirt.VIR_DOMAIN_RUNNING: raise NotFoundError("KCHVM0083E", {'name': name}) + def event_vmshutdown_cb(self, conn, dom, event, detail, *args): + if event == libvirt.VIR_DOMAIN_EVENT_STOPPED: + vm_name = dom.name() + self.firewall_mngr.remove_vm_graphics_port(vm_name) + cb_id = self.vm_event_callbacks.pop(vm_name, None) + self.conn.get().domainEventDeregisterAny(cb_id) + + def handleVMShutdownPowerOff(self, vm_name): + try: + dom = VMModel.get_vm(vm_name, self.conn) + cb_id = self.conn.get().domainEventRegisterAny( + dom, + libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE, + self.event_vmshutdown_cb, + None + ) + self.vm_event_callbacks[vm_name] = cb_id + + except (libvirt.libvirtError, AttributeError) as e: + if type(e) == AttributeError: + reason = 'Libvirt service is not running' + else: + reason = e.message + wok_log.error("Register of LIFECYCLE event failed: %s" % reason) + def lookup(self, name): self._check_if_vm_running(name) graphics_info = VMModel.get_graphics(name, self.conn) file_path = create_virt_viewer_file(name, graphics_info) + if not self.vm_event_callbacks.get(name, None): + graphics_port = graphics_info[2] + self.firewall_mngr.add_vm_graphics_port(name, graphics_port) + self.handleVMShutdownPowerOff(name) + return 'plugins/kimchi/data/virtviewerfiles/%s' %\ os.path.basename(file_path) -- 2.5.5

From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> This patch changes existing tests and add a new test to assert that the latest changes with FirewallManager and libvirt event listening in virtviewerfile.py is working properly. Signed-off-by: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com> --- tests/test_model.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 1162d2c..fcc6b5f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -20,6 +20,7 @@ import __builtin__ as builtins +import base64 import grp import lxml.etree as ET import os @@ -353,17 +354,19 @@ class ModelTests(unittest.TestCase): 'source_media': {'type': 'disk', 'path': UBUNTU_ISO}} inst.templates_create(params) + vm_name = 'kìmchí-vñç' + with RollbackContext() as rollback: - params = {'name': 'kimchi-vnc', + params = {'name': vm_name.decode('utf-8'), 'template': '/plugins/kimchi/templates/test'} task1 = inst.vms_create(params) inst.task_wait(task1['id']) - rollback.prependDefer(inst.vm_delete, 'kimchi-vnc') + rollback.prependDefer(inst.vm_delete, vm_name.decode('utf-8')) expected_error_msg = "KCHVM0083E" with self.assertRaisesRegexp(NotFoundError, expected_error_msg): vvmodel = VMVirtViewerFileModel(conn=inst.conn) - vvmodel.lookup('kimchi-vnc') + vvmodel.lookup(vm_name.decode('utf-8')) inst.template_delete('test') @@ -371,8 +374,13 @@ class ModelTests(unittest.TestCase): @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' 'VMModel.get_graphics') @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'FirewallManager.add_vm_graphics_port') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMVirtViewerFileModel.handleVMShutdownPowerOff') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' 'VMVirtViewerFileModel._check_if_vm_running') - def test_vm_virtviewerfile_vnc(self, mock_vm_running, mock_get_graphics, + def test_vm_virtviewerfile_vnc(self, mock_vm_running, mock_handleVMOff, + mock_add_port, mock_get_graphics, mock_get_host): mock_get_host.return_value = 'kimchi-test-host' @@ -398,13 +406,21 @@ class ModelTests(unittest.TestCase): mock_get_graphics.assert_called_once_with('kimchi-vm', None) mock_vm_running.assert_called_once_with('kimchi-vm') + mock_handleVMOff.assert_called_once_with('kimchi-vm') + mock_add_port.assert_called_once_with('kimchi-vm', '5999') @mock.patch('wok.plugins.kimchi.model.virtviewerfile._get_request_host') @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' 'VMModel.get_graphics') @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'FirewallManager.add_vm_graphics_port') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'VMVirtViewerFileModel.handleVMShutdownPowerOff') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' 'VMVirtViewerFileModel._check_if_vm_running') def test_vm_virtviewerfile_spice_passwd(self, mock_vm_running, + mock_handleVMOff, + mock_add_port, mock_get_graphics, mock_get_host): @@ -433,6 +449,8 @@ class ModelTests(unittest.TestCase): mock_get_graphics.assert_called_once_with('kimchi-vm', None) mock_vm_running.assert_called_once_with('kimchi-vm') + mock_handleVMOff.assert_called_once_with('kimchi-vm') + mock_add_port.assert_called_once_with('kimchi-vm', '6660') @mock.patch('wok.plugins.kimchi.model.virtviewerfile.run_command') def test_firewall_provider_firewallcmd(self, mock_run_cmd): @@ -504,6 +522,48 @@ class ModelTests(unittest.TestCase): ] ) + @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'FirewallManager.remove_vm_graphics_port') + @mock.patch('wok.plugins.kimchi.model.virtviewerfile.' + 'FirewallManager.add_vm_graphics_port') + def test_vm_virtviewerfile_vmlifecycle(self, mock_add_port, + mock_remove_port): + + inst = model.Model(objstore_loc=self.tmp_store) + params = {'name': 'test', + 'source_media': {'type': 'disk', 'path': UBUNTU_ISO}} + inst.templates_create(params) + + vm_name = 'kìmçhí-vñç' + + with RollbackContext() as rollback: + params = {'name': u'%s' % vm_name.decode('utf-8'), + 'template': '/plugins/kimchi/templates/test'} + task1 = inst.vms_create(params) + inst.task_wait(task1['id']) + rollback.prependDefer(inst.vm_delete, vm_name.decode('utf-8')) + + inst.vm_start(vm_name.decode('utf-8')) + + graphics_info = VMModel.get_graphics(vm_name.decode('utf-8'), + inst.conn) + graphics_port = graphics_info[2] + + vvmodel = VMVirtViewerFileModel(conn=inst.conn) + vvmodel.lookup(vm_name.decode('utf-8')) + + inst.vm_poweroff(vm_name.decode('utf-8')) + time.sleep(5) + + mock_add_port.assert_called_once_with(vm_name.decode('utf-8'), + graphics_port) + mock_remove_port.assert_called_once_with( + base64.b64encode(vm_name) + ) + + inst.template_delete('test') + @unittest.skipUnless(utils.running_as_root(), "Must be run as root") def test_vm_serial(self): inst = model.Model(objstore_loc=self.tmp_store) -- 2.5.5

Reviewed-By: Lucio Correia <luciojhc@linux.vnet.ibm.com> On 08-07-2016 12:10, dhbarboza82@gmail.com wrote:
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
v2: - fixed typo and capital letters in i18n.py - fixed 'double dict' error in virtviewer module - added tests with virtual machine containing utf-8 characters
This patch set adds the Virt-Viewer launcher backend to Kimchi.
This feature consists of a new resource located in:
**URI:** /plugins/kimchi/vms/*:name*/snapshots/current
that retrieves a download link to a .vv file to be used by a Virt-Viewer compatible desktop app to connect to the remote virtual machine.
This backend takes cares of handling firewall rules to allow the connection to be succesfull. Note that no firewall port will be opened unless a download is made - if the user decides to use noVNC or spice-html5 instead Kimchi will not touch the host firewall.
Example:
[danielhb@arthas kimchi]$ curl -u root -H "Content-Type: application/json" -H "Accept: application/json" -X GET "http://localhost:8010/plugins/kimchi/vms/OpenSUSE-Leap-42.1/virtviewerfile" Enter host password for user 'root': [virt-viewer] type=vnc host=localhost port=5904
After this call, port 5904 was opened in the host to allow for a virt-viewer connection.
When shutting down the virtual machine or WoK, a cleanup is made to close any ports left opened.
Daniel Henrique Barboza (9): Virt-Viewer launcher: docs and i18n changes Virt-Viewer launcher: Makefile and config changes Virt-Viewer launcher: control/vms.py and model/vms.py changes Virt-Viewer launcher: virtviewerfile module Virt-Viewer launcher: test changes Virt-Viewer launcher: adding FirewallManager class Virt-Viewer launcher: test changes for firewall manager Virt-Viewer launcher: libvirt events to control firewall Virt-Viewer launcher: changes after adding libvirt event listening
Makefile.am | 2 + config.py.in | 10 ++- control/vms.py | 12 +++ docs/API.md | 6 ++ i18n.py | 2 + model/virtviewerfile.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++ model/vms.py | 9 +- tests/test_config.py.in | 6 ++ tests/test_model.py | 228 +++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 503 insertions(+), 6 deletions(-) create mode 100644 model/virtviewerfile.py
-- Lucio Correia Software Engineer IBM LTC Brazil
participants (5)
-
Daniel Henrique Barboza
-
Daniel Henrique Barboza
-
dhbarboza82@gmail.com
-
Lucio Correia
-
Paulo Ricardo Paz Vital