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