Vitor de Lima has uploaded a new change for review.

Change subject: services, setup: VirtIO console proxy [WIP]
......................................................................

services, setup: VirtIO console proxy [WIP]

This is a work-in-progress implementation of the VirtIO console proxy,
it is based on a secondary instance of the SSH server which uses
public key authentication, retrieving the allowed keys from the
engine and later accessing the text consoles of the VMs using virsh.

Change-Id: I034ef8e6d10da5dc93eda61e0c5c518ca13a5a28
Signed-off-by: Vitor de Lima <vdel...@redhat.com>
---
A packaging/services/ovirt-console-proxy/vmproxy
A packaging/services/ovirt-console-proxy/vmproxy-authkeys
A packaging/setup/ovirt_engine_setup/console_proxy/__init__.py
A packaging/setup/ovirt_engine_setup/console_proxy/constants.py
A packaging/setup/plugins/ovirt-engine-setup/console_proxy/__init__.py
A packaging/setup/plugins/ovirt-engine-setup/console_proxy/config.py
A packaging/setup/plugins/ovirt-engine-setup/console_proxy/pki.py
7 files changed, 644 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/06/35906/1

diff --git a/packaging/services/ovirt-console-proxy/vmproxy 
b/packaging/services/ovirt-console-proxy/vmproxy
new file mode 100644
index 0000000..4387d1d
--- /dev/null
+++ b/packaging/services/ovirt-console-proxy/vmproxy
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+
+import ConfigParser
+import argparse
+import urlparse
+import urllib
+import urllib2
+import json
+import sys
+import os
+
+
+def log_error(message):
+    sys.stderr.write(message)
+    sys.exit(-1)
+
+def read_config_option(config, name):
+    try:
+        return config.get("main", name)
+    except ConfigParser.NoOptionError:
+        log_error('The console proxy is not properly configured, the %s 
parameter is missing' % name)
+    except ConfigParser.NoSectionError:
+        log_error('The console proxy is not properly configured, the 
configuration file is invalid')
+
+def read_config():
+    config = ConfigParser.ConfigParser()
+
+    config.read(['/var/lib/vmproxy/console_proxy.conf'])
+
+    base_url = read_config_option(config, 'base_url')
+    username = read_config_option(config, 'username')
+    password = read_config_option(config, 'password')
+
+    return base_url, username, password
+
+def install_password_manager(base_url, username, password):
+    password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+
+    password_manager.add_password(None, base_url, username, password)
+
+    auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
+
+    opener = urllib2.build_opener(auth_handler)
+
+    urllib2.install_opener(opener)
+
+def get_json(url):
+    try:
+        url_handler = urllib2.urlopen(url)
+    except Exception, e:
+        log_error('Could not contact the engine: %s' + str(e))
+
+    try:
+        content = json.load(url_handler)
+    except ValueError, e:
+        log_error('Could not parse the received data: %s' + str(e))
+
+    return content
+
+def encode_url(base_url, service, **kwargs):
+    url = urlparse.urljoin(base_url, service)
+
+    if len(kwargs) > 0:
+        params_url = urllib.urlencode(kwargs)
+        url += '?' + params_url
+
+    return url
+
+def read_args():
+    parser = argparse.ArgumentParser('Text console proxy', description='Open a 
text console to a VM')
+
+    parser.add_argument("--vm-name", required=True, help='VM to be accessed')
+    parser.add_argument("--user-guid", required=True, help='User with 
permissions to the VM')
+
+    args = parser.parse_args()
+
+    vm_name = args.vm_name
+    user_guid = args.user_guid
+
+    return user_guid, vm_name
+
+def run_virsh_console(vm, host):
+    os.execlp('virsh', 'virsh', '-c', 'qemu+tls://' + host + '/system', 
'console', vm)
+
+def main():
+    base_url, username, password = read_config()
+
+    user_guid, vm_name = read_args()
+
+    install_password_manager(base_url, username, password)
+
+    consoles = get_json(encode_url(base_url, 'availableconsoles', 
user_guid=user_guid))
+
+    if not vm_name:
+        if len(consoles) == 0:
+            log_error("There are not any available virtual machines to 
connect")
+
+        while True:
+            vm_list = consoles.keys()
+
+            for index, vm in enumerate(vm_list):
+                print "%d. %s" % (index+1, vm)
+
+            try:
+                vm_index = int(raw_input('> '))
+            except ValueError:
+                pass
+            else:
+                if 1 <= vm_index <= len(consoles):
+                    vm_name = vm_list[vm_index-1]
+                    run_virsh_console(vm_name, consoles[vm_name])
+    else:
+        if vm_name in consoles:
+            run_virsh_console(vm_name, consoles[vm_name])
+        else:
+            log_error('Could not find the target virtual machine')
+
+if __name__ == "__main__":
+    main()
diff --git a/packaging/services/ovirt-console-proxy/vmproxy-authkeys 
b/packaging/services/ovirt-console-proxy/vmproxy-authkeys
new file mode 100644
index 0000000..a48a132
--- /dev/null
+++ b/packaging/services/ovirt-console-proxy/vmproxy-authkeys
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+
+import ConfigParser
+import urlparse
+import urllib
+import urllib2
+import json
+import sys
+
+
+authkeys_line = 'command="/usr/bin/vmproxy --vm-name 
\\"${SSH_ORIGINAL_COMMAND}\\" --user-guid 
\\"%s\\"",no-agent-forwarding,no-port-forwarding,no-user-rc,no-X11-forwarding 
%s'
+
+
+def log_error(message):
+    sys.stderr.write(message)
+    sys.exit(-1)
+
+def read_config_option(config, name):
+    try:
+        return config.get("main", name)
+    except ConfigParser.NoOptionError:
+        log_error('The console proxy is not properly configured, the %s 
parameter is missing' % name)
+    except ConfigParser.NoSectionError:
+        log_error('The console proxy is not properly configured, the 
configuration file is invalid')
+
+def read_config():
+    config = ConfigParser.ConfigParser()
+
+    config.read(['/var/lib/vmproxy/console_proxy.conf'])
+
+    base_url = read_config_option(config, 'base_url')
+    username = read_config_option(config, 'username')
+    password = read_config_option(config, 'password')
+
+    return base_url, username, password
+
+def install_password_manager(base_url, username, password):
+    password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+
+    password_manager.add_password(None, base_url, username, password)
+
+    auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
+
+    opener = urllib2.build_opener(auth_handler)
+
+    urllib2.install_opener(opener)
+
+def get_json(url):
+    try:
+        url_handler = urllib2.urlopen(url)
+    except Exception, e:
+        log_error('Could not contact the engine: %s' + str(e))
+
+    try:
+        content = json.load(url_handler)
+    except ValueError, e:
+        log_error('Could not parse the received data: %s' + str(e))
+
+    return content
+
+def encode_url(base_url, service, **kwargs):
+    url = urlparse.urljoin(base_url, service)
+
+    if len(kwargs) > 0:
+        params_url = urllib.urlencode(kwargs)
+        url += '?' + params_url
+
+    return url
+
+def main():
+    base_url, username, password = read_config()
+
+    install_password_manager(base_url, username, password)
+
+    users = get_json(encode_url(base_url, 'publickeys'))
+
+    for guid, public_key in users:
+        print authkeys_line % (guid, public_key)
+
+if __name__ == "__main__":
+    main()
diff --git a/packaging/setup/ovirt_engine_setup/console_proxy/__init__.py 
b/packaging/setup/ovirt_engine_setup/console_proxy/__init__.py
new file mode 100644
index 0000000..74cb8e6
--- /dev/null
+++ b/packaging/setup/ovirt_engine_setup/console_proxy/__init__.py
@@ -0,0 +1,25 @@
+#
+# ovirt-engine-setup -- ovirt engine setup
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""ovirt_engine_setup module."""
+
+
+__all__ = []
+
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/packaging/setup/ovirt_engine_setup/console_proxy/constants.py 
b/packaging/setup/ovirt_engine_setup/console_proxy/constants.py
new file mode 100644
index 0000000..d2c990e
--- /dev/null
+++ b/packaging/setup/ovirt_engine_setup/console_proxy/constants.py
@@ -0,0 +1,78 @@
+#
+# ovirt-engine-setup -- ovirt engine setup
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""Constants."""
+
+
+import os
+import gettext
+_ = lambda m: gettext.dgettext(message=m, domain='ovirt-engine-setup')
+
+
+from otopi import util
+
+
+from ovirt_engine_setup.constants import osetupattrsclass
+from ovirt_engine_setup.constants import osetupattrs
+
+
+@util.export
+@util.codegen
+@osetupattrsclass
+class ConfigEnv(object):
+
+    @osetupattrs(
+        answerfile=True,
+        summary=True,
+        description=_('Configure VirtIO Console Proxy'),
+        postinstallfile=True,
+    )
+    def CONSOLE_PROXY_CONFIG(self):
+        return 'OVESETUP_CONFIG/consoleProxyConfig'
+
+    @osetupattrs(
+        answerfile=True,
+    )
+    def CONSOLE_PROXY_PASSWORD(self):
+        return 'OVESETUP_CONFIG/consoleProxyPassword'
+
+@util.export
+class Stages(object):
+
+    CONFIG_CONSOLE_PROXY_CUSTOMIZATION = \
+        'setup.config.console-proxy.customization'
+
+# FIXME
+@util.export
+class FileLocations(object):
+
+    OVIRT_ENGINE_CONSOLE_PROXY_CONFIG = \
+        '/var/lib/vmproxy/console_proxy.conf'
+
+    OVIRT_ENGINE_PKI_CONSOLE_CA_CERT = \
+        "/var/lib/vmproxy/.pki/libvirt/cacert.pem"
+
+    OVIRT_ENGINE_PKI_CONSOLE_CERT = \
+        "/var/lib/vmproxy/.pki/libvirt/clientcert.pem"
+
+    OVIRT_ENGINE_PKI_CONSOLE_KEY = \
+        "/var/lib/vmproxy/.pki/libvirt/clientkey.pem"
+
+
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git 
a/packaging/setup/plugins/ovirt-engine-setup/console_proxy/__init__.py 
b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/__init__.py
new file mode 100644
index 0000000..53841f5
--- /dev/null
+++ b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/__init__.py
@@ -0,0 +1,34 @@
+#
+# ovirt-engine-setup -- ovirt engine setup
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""ovirt-host-setup console_proxy plugin."""
+
+
+from otopi import util
+
+
+from . import config
+from . import pki
+
+
+@util.export
+def createPlugins(context):
+    config.Plugin(context=context)
+    pki.Plugin(context=context)
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/packaging/setup/plugins/ovirt-engine-setup/console_proxy/config.py 
b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/config.py
new file mode 100644
index 0000000..7e3916e
--- /dev/null
+++ b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/config.py
@@ -0,0 +1,223 @@
+#
+# ovirt-engine-setup -- ovirt engine setup
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""console proxy plugin."""
+
+
+import gettext
+_ = lambda m: gettext.dgettext(message=m, domain='ovirt-engine-setup')
+
+
+from otopi import constants as otopicons
+from otopi import filetransaction
+from otopi import util
+from otopi import plugin
+
+
+from ovirt_engine_setup import constants as osetupcons
+from ovirt_engine_setup.engine_common import constants as oengcommcons
+from ovirt_engine_setup.console_proxy import constants as ocpcons
+from ovirt_engine_setup import dialog
+from ovirt_engine_setup.engine import vdcoption
+
+
+@util.export
+class Plugin(plugin.PluginBase):
+    """console proxy plugin."""
+
+    def __init__(self, context):
+        super(Plugin, self).__init__(context=context)
+        self._needStart = False
+        self._enabled = True
+
+    @plugin.event(
+        stage=plugin.Stages.STAGE_CUSTOMIZATION,
+        name=ocpcons.Stages.CONFIG_CONSOLE_PROXY_CUSTOMIZATION,
+        condition=lambda self: self._enabled,
+        before=(
+            osetupcons.Stages.DIALOG_TITLES_E_PRODUCT_OPTIONS,
+        ),
+        after=(
+            osetupcons.Stages.DIALOG_TITLES_S_PRODUCT_OPTIONS,
+        ),
+    )
+    def _customization(self):
+
+        if self.environment[
+            ocpcons.ConfigEnv.CONSOLE_PROXY_CONFIG
+        ] is None:
+            self.environment[
+                ocpcons.ConfigEnv.CONSOLE_PROXY_CONFIG
+            ] = dialog.queryBoolean(
+                dialog=self.dialog,
+                name='OVESETUP_CONFIG_CONSOLE_PROXY',
+                note=_(
+                    'Configure VirtIO Console Proxy on this host '
+                    '(@VALUES@) [@DEFAULT@]: '
+                ),
+                prompt=True,
+                default=True,
+            )
+        self._enabled = self.environment[
+            ocpcons.ConfigEnv.CONSOLE_PROXY_CONFIG
+        ]
+
+    @plugin.event(
+        stage=plugin.Stages.STAGE_CUSTOMIZATION,
+        before=(
+            oengcommcons.Stages.DIALOG_TITLES_E_ENGINE,
+        ),
+        after=(
+            oengcommcons.Stages.DB_CONNECTION_STATUS,
+            oengcommcons.Stages.DIALOG_TITLES_S_ENGINE,
+        ),
+        condition=lambda self: self._enabled,
+    )
+    def _password_customization(self):
+        if not self.environment[
+            osetupcons.EngineDBEnv.NEW_DATABASE
+        ]:
+            self.dialog.note(
+                text=_(
+                    'Skipping storing options as database already '
+                    'prepared'
+                ),
+            )
+        else:
+            if self.environment[ocpcons.ConfigEnv.CONSOLE_PROXY_PASSWORD] is 
None:
+                valid = False
+                password = None
+                while not valid:
+                    password = self.dialog.queryString(
+                        name='OVESETUP_CONFIG_CONSOLE_PASSWORD_SETUP',
+                        note=_('Console proxy internal password: '),
+                        prompt=True,
+                        hidden=True,
+                    )
+                    password2 = self.dialog.queryString(
+                        name='OVESETUP_CONFIG_CONSOLE_PASSWORD_SETUP',
+                        note=_('Confirm console proxy internal password: '),
+                        prompt=True,
+                        hidden=True,
+                    )
+
+                    if password != password2:
+                        self.logger.warning(_('Passwords do not match'))
+                    else:
+                        try:
+                            import cracklib
+                            cracklib.FascistCheck(password)
+                            valid = True
+                        except ImportError:
+                            # do not force this optional feature
+                            self.logger.debug(
+                                'cannot import cracklib',
+                                exc_info=True,
+                            )
+                            valid = True
+                        except ValueError as e:
+                            self.logger.warning(
+                                _('Password is weak: {error}').format(
+                                    error=e,
+                                )
+                            )
+                            valid = dialog.queryBoolean(
+                                dialog=self.dialog,
+                                
name='OVESETUP_CONFIG_WEAK_CONSOLE_PROXY_PASSWORD',
+                                note=_(
+                                    'Use weak password? '
+                                    '(@VALUES@) [@DEFAULT@]: '
+                                ),
+                                prompt=True,
+                                default=False,
+                            )
+
+                self.environment[
+                    osetupcons.ConfigEnv.CONSOLE_PROXY_PASSWORD
+                ] = password
+
+    @plugin.event(
+        stage=plugin.Stages.STAGE_MISC,
+        after=(
+            oengcommcons.Stages.CONFIG_DB_ENCRYPTION_AVAILABLE,
+        ),
+        condition=lambda self: self._enabled,
+    )
+    def _miscEncrypted(self):
+        vdcoption.VdcOption(
+            statement=self.environment[
+                osetupcons.EngineDBEnv.STATEMENT
+            ]
+        ).updateVdcOptions(
+            options=(
+                {
+                    'name': 'ConsoleProxyPassword',
+                    'value': self.environment[
+                        osetupcons.ConfigEnv.CONSOLE_PROXY_PASSWORD
+                    ],
+                    'encrypt': True,
+                },
+            ),
+        )
+
+    @plugin.event(
+        stage=plugin.Stages.STAGE_MISC,
+        condition=lambda self: (
+            self._enabled,
+        ),
+    )
+    def _misc_config(self):
+        if self.environment[oengcommcons.ConfigEnv.JBOSS_AJP_PORT]:
+            engineURI = osetupcons.Const.ENGINE_URI
+        else:
+            engineURI = '/'
+
+        baseURL =  _('https://{fqdn}:{httpsPort}{engineURI}services/').format(
+            fqdn=self.environment[osetupcons.ConfigEnv.FQDN],
+            httpsPort=self.environment[
+                oengcommcons.ConfigEnv.PUBLIC_HTTPS_PORT
+            ],
+            engineURI=engineURI,
+        )
+
+        self.environment[otopicons.CoreEnv.MAIN_TRANSACTION].append(
+            filetransaction.FileTransaction(
+                name=(
+                    ocpcons.FileLocations.
+                    OVIRT_ENGINE_CONSOLE_PROXY_CONFIG
+                ),
+                content=(
+                    "[main]\n"
+                    "base_url={base_url}\n"
+                    "username={username}\n"
+                    "password={password}\n"
+                ).format(
+                    base_url=baseURL,
+                    username='consoleproxy',
+                    password= self.environment[
+                        osetupcons.ConfigEnv.CONSOLE_PROXY_PASSWORD
+                    ],
+                ),
+                modifiedList=self.environment[
+                    otopicons.CoreEnv.MODIFIED_FILES
+                ],
+            )
+        )
+
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/packaging/setup/plugins/ovirt-engine-setup/console_proxy/pki.py 
b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/pki.py
new file mode 100644
index 0000000..fec5dd2
--- /dev/null
+++ b/packaging/setup/plugins/ovirt-engine-setup/console_proxy/pki.py
@@ -0,0 +1,84 @@
+#
+# ovirt-engine-setup -- ovirt engine setup
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+"""console proxy plugin."""
+
+import shutil
+
+import gettext
+_ = lambda m: gettext.dgettext(message=m, domain='ovirt-engine-setup')
+
+from otopi import util
+from otopi import plugin
+
+
+from ovirt_engine_setup import constants as osetupcons
+from ovirt_engine_setup.engine_common import constants as oengcommcons
+from ovirt_engine_setup.console_proxy import constants as ocpcons
+
+
+@util.export
+class Plugin(plugin.PluginBase):
+    """console proxy plugin."""
+
+    def __init__(self, context):
+        super(Plugin, self).__init__(context=context)
+        self._enabled = True
+
+    @plugin.event(
+        stage=plugin.Stages.STAGE_MISC,
+        after=(
+                oengcommcons.Stages.RENAME_PKI_CONF_MISC,
+        ),
+        condition=lambda self: self.environment[
+            ocpcons.ConfigEnv.CONSOLE_PROXY_CONFIG
+        ],
+        )
+    def _misc(self):
+        # TODO
+        # this implementation is not transactional
+        # too many issues with legacy ca implementation
+        # need to work this out to allow transactional
+        rc, stdout, stderr = self.execute(
+            args=(
+                osetupcons.FileLocations.OVIRT_ENGINE_PKI_PKCS12_EXTRACT,
+                '--name=%s' % 'engine',
+                '--passin=%s' % (
+                    self.environment[osetupcons.PKIEnv.STORE_PASS],
+                ),
+                '--cert=%s'% (
+                    ocpcons.FileLocations.OVIRT_ENGINE_PKI_CONSOLE_CERT,
+                ),
+            ),
+            )
+
+        shutil.copy(osetupcons.FileLocations.OVIRT_ENGINE_PKI_ENGINE_CA_CERT, 
ocpcons.FileLocations.OVIRT_ENGINE_PKI_CONSOLE_CA_CERT);
+
+        self.execute(
+            args=(
+                osetupcons.FileLocations.OVIRT_ENGINE_PKI_PKCS12_EXTRACT,
+                '--name=%s' % 'engine',
+                '--passin=%s' % (
+                    self.environment[osetupcons.PKIEnv.STORE_PASS],
+                ),
+                '--key=%s' % (
+                    ocpcons.FileLocations.OVIRT_ENGINE_PKI_CONSOLE_KEY,
+                ),
+            ),
+            )
+


-- 
To view, visit http://gerrit.ovirt.org/35906
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I034ef8e6d10da5dc93eda61e0c5c518ca13a5a28
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-engine
Gerrit-Branch: master
Gerrit-Owner: Vitor de Lima <vdel...@redhat.com>
_______________________________________________
Engine-patches mailing list
Engine-patches@ovirt.org
http://lists.ovirt.org/mailman/listinfo/engine-patches

Reply via email to