Yedidyah Bar David has uploaded a new change for review. Change subject: packaging: setup: config remote engine host access ......................................................................
packaging: setup: config remote engine host access Ask user whether to use ssh as root to access remote engine host, or provide instructions to do actions there. Allow signing a certificate by remote engine ca either way. Change-Id: I67d709ba2b0138b8d372deb0b6de5e7a9836f417 Related-To: https://bugzilla.redhat.com/1118328 Signed-off-by: Yedidyah Bar David <[email protected]> --- M ovirt-engine.spec.in M packaging/setup/ovirt_engine_setup/constants.py A packaging/setup/ovirt_engine_setup/remote_engine.py A packaging/setup/ovirt_engine_setup/remote_engine_base.py M packaging/setup/plugins/ovirt-engine-common/base/core/__init__.py A packaging/setup/plugins/ovirt-engine-common/base/remote_engine/__init__.py A packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine.py A packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_manual_files.py A packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_root_ssh.py 9 files changed, 1,133 insertions(+), 1 deletion(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/31/33231/1 diff --git a/ovirt-engine.spec.in b/ovirt-engine.spec.in index 9491764..bfab301 100644 --- a/ovirt-engine.spec.in +++ b/ovirt-engine.spec.in @@ -354,6 +354,7 @@ Requires: libxml2-python Requires: logrotate Requires: otopi >= 1.2.2 +Requires: python-paramiko Conflicts: %{name}-dwh-setup < 3.5.0 Conflicts: %{name}-reports-setup < 3.5.0 diff --git a/packaging/setup/ovirt_engine_setup/constants.py b/packaging/setup/ovirt_engine_setup/constants.py index f868e6c..ef338ff 100644 --- a/packaging/setup/ovirt_engine_setup/constants.py +++ b/packaging/setup/ovirt_engine_setup/constants.py @@ -253,6 +253,10 @@ FIREWALL_MANAGER_FIREWALLD = 'firewalld' ISO_DOMAIN_NFS_DEFAULT_ACL_FORMAT = '{fqdn}(rw)' + REMOTE_ENGINE_SETUP_STYLE_AUTO_SSH = 'auto_ssh' + REMOTE_ENGINE_SETUP_STYLE_MANUAL_FILES = 'manual_files' + REMOTE_ENGINE_SETUP_STYLE_MANUAL_INLINE = 'manual_inline' + @util.export @util.codegen @@ -284,6 +288,8 @@ ORIGINAL_GENERATED_BY_VERSION = 'OVESETUP_CORE/originalGeneratedByVersion' SETUP_ATTRS_MODULES = 'OVESETUP_CORE/setupAttributesModules' + + REMOTE_ENGINE = 'OVESETUP_CORE/remoteEngine' @util.export @@ -385,6 +391,33 @@ FQDN_REVERSE_VALIDATION = 'OVESETUP_CONFIG/fqdnReverseValidation' FQDN_NON_LOOPBACK_VALIDATION = 'OVESETUP_CONFIG/fqdnNonLoopback' + REMOTE_ENGINE_SETUP_STYLES = 'OVESETUP_CONFIG/remoteEngineSetupStyles' + + @osetupattrs( + answerfile=True, + ) + def REMOTE_ENGINE_SETUP_STYLE(self): + return 'OVESETUP_CONFIG/remoteEngineSetupStyle' + + @osetupattrs( + answerfile=True, + ) + def REMOTE_ENGINE_HOST_SSH_PORT(self): + return 'OVESETUP_CONFIG/remoteEngineHostSshPort' + + # Optional, used if supplied + REMOTE_ENGINE_HOST_CLIENT_KEY = 'OVESETUP_CONFIG/remoteEngineHostClientKey' + + # Optional, used if supplied, currently only log if not there + REMOTE_ENGINE_HOST_KNOWN_HOSTS = \ + 'OVESETUP_CONFIG/remoteEngineHostKnownHosts' + + @osetupattrs( + answerfile=True, + ) + def REMOTE_ENGINE_HOST_ROOT_PASSWORD(self): + return 'OVESETUP_CONFIG/remoteEngineHostRootPassword' + @util.export @util.codegen diff --git a/packaging/setup/ovirt_engine_setup/remote_engine.py b/packaging/setup/ovirt_engine_setup/remote_engine.py new file mode 100644 index 0000000..9534e3a --- /dev/null +++ b/packaging/setup/ovirt_engine_setup/remote_engine.py @@ -0,0 +1,469 @@ +# +# 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. +# + + +import os +import tempfile +import time +import gettext +_ = lambda m: gettext.dgettext(message=m, domain='ovirt-engine-setup') + + +from M2Crypto import X509 +from M2Crypto import EVP +from M2Crypto import RSA + + +from otopi import util +from otopi import base +from otopi import constants as otopicons +from otopi import filetransaction + + +from ovirt_engine_setup import constants as osetupcons + + [email protected] +class RemoteEngine(base.Base): + + _instance = None + + def __init__(self, plugin): + super(RemoteEngine, self).__init__() + self._plugin = plugin + self._style = None + self._client = None + + @property + def plugin(self): + return self._plugin + + @property + def dialog(self): + return self._plugin.dialog + + @property + def environment(self): + return self._plugin.environment + + @property + def logger(self): + return self._plugin.logger + + def style(self): + if self._style is None: + self.configure() + return self._style + + def execute_on_engine(self, cmd, timeout=60, text=None): + return self._style.execute_on_engine( + cmd=cmd, + timeout=timeout, + text=text, + ) + + def copy_from_engine(self, file_name): + return self._style.copy_from_engine( + file_name=file_name, + ) + + def copy_to_engine( + self, + file_name, + content, + inp_env_key=None, + ): + return self._style.copy_to_engine( + file_name=file_name, + content=content, + inp_env_key=inp_env_key, + ) + + def cleanup(self): + if self._style: + return self._style.cleanup() + + def configure(self, fqdn): + key = osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLE + styles = dict( + ( + str(i + 1), + s + ) + for i, s in enumerate( + self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLES + ] + ) + ) + if self.environment[key] is None: + choices = sorted(styles.keys()) + descs = ''.join( + '{c} - {desc}\n'.format( + c=c, + desc=styles[c].desc(), + ) + for c in choices + ) + reply = self.dialog.queryString( + name='REMOTE_ENGINE_SETUP_STYLE', + note=_( + 'Setup will need to do some actions on the remote engine ' + 'server. Either automatically, using ssh as root to ' + 'access it, or you will be prompted to manually ' + 'perform each such action.\n' + 'Please choose one of the following:\n' + '{descs}' + '(@VALUES@) [@DEFAULT@]: ' + ).format( + descs=descs, + ), + prompt=True, + validValues=choices, + default=choices[0], + ) + self.environment[key] = styles[reply].name + if self._style is None: + self._style = next( + s for i, s in styles.items() + if s.name == self.environment[key] + ) + self._style.configure(fqdn=fqdn) + + [email protected] +class EnrollCert(base.Base): + + def __init__( + self, + remote_engine, + engine_fqdn, + base_name, + base_touser, + key_file, + cert_file, + csr_fname_envkey, + engine_ca_cert_file, + engine_pki_requests_dir, + engine_pki_certs_dir, + key_size, + url, + ): + super(EnrollCert, self).__init__() + self._need_key = False + self._need_cert = False + self._key = None + self._pubkey = None + self._csr = None + self._cert = None + self._csr_file = None + self._engine_csr_file = None + self._engine_cert_file = None + self._remote_name = None + self._enroll_command = None + + self._remote_engine = remote_engine + self._engine_fqdn = engine_fqdn + self._base_name = base_name + self._base_touser = base_touser + self._key_file = key_file + self._cert_file = cert_file + self._csr_fname_envkey = csr_fname_envkey + self._engine_ca_cert_file = engine_ca_cert_file + self._engine_pki_requests_dir = engine_pki_requests_dir + self._engine_pki_certs_dir = engine_pki_certs_dir + self._key_size = key_size + self._url = url + + self._plugin = remote_engine.plugin + + @property + def plugin(self): + return self._plugin + + @property + def dialog(self): + return self._plugin.dialog + + @property + def environment(self): + return self._plugin.environment + + @property + def logger(self): + return self._plugin.logger + + def _genCsr(self): + rsa = RSA.gen_key(self._key_size, 65537) + rsapem = rsa.as_pem(cipher=None) + evp = EVP.PKey() + evp.assign_rsa(rsa) + rsa = None # should not be freed here + csr = X509.Request() + csr.set_pubkey(evp) + csr.sign(evp, 'sha1') + return rsapem, csr.as_pem(), csr.get_pubkey().as_pem(cipher=None) + + def _enroll_cert_auto_ssh(self): + cert = None + self.logger.info( + _( + "Signing the {base_touser} certificate on the engine server" + ).format( + base_touser=self._base_touser, + ) + ) + + tries_left = 30 + goodcert = False + while not goodcert and tries_left > 0: + try: + self._remote_engine.copy_to_engine( + file_name='{pkireqdir}/{remote_name}.req'.format( + pkireqdir=self._engine_pki_requests_dir, + remote_name=self._remote_name, + ), + content=self._csr, + ) + self._remote_engine.execute_on_engine(cmd=self._enroll_command) + cert = self._remote_engine.copy_from_engine( + file_name='{pkicertdir}/{remote_name}.cer'.format( + pkicertdir=self._engine_pki_certs_dir, + remote_name=self._remote_name, + ), + ) + goodcert = self._pubkey == X509.load_cert_string( + cert + ).get_pubkey().as_pem(cipher=None) + if not goodcert: + self.logger.error( + _( + 'Failed to sign {base_touser} certificate on ' + 'engine server' + ).format( + base_touser=self._base_touser, + ) + ) + except: + self.logger.error( + _( + 'Error while trying to sign {base_touser} certificate' + ).format( + base_touser=self._base_touser, + ) + ) + self.logger.debug('Error signing cert', exc_info=True) + tries_left -= 1 + if not goodcert and tries_left > 0: + self.dialog.note( + text=_('Trying again...') + ) + time.sleep(10) + + self.logger.info( + _('{base_touser} certificate signed successfully').format( + base_touser=self._base_touser, + ) + ) + return cert + + def _enroll_cert_manual_files(self): + cert = None + csr_fname = self.environment[self._csr_fname_envkey] + with ( + open(csr_fname, 'w') if csr_fname + else tempfile.NamedTemporaryFile(mode='w', delete=False) + ) as self._csr_file: + self._csr_file.write(self._csr) + self.dialog.note( + text=_( + "\n\nTo sign the {base_touser} certificate on the engine " + "server, please:\n\n" + "1. Copy {tmpcsr} from here to {enginecsr} on the engine " + "server.\n\n" + "2. Run on the engine server:\n\n" + "{enroll_command}\n\n" + "3. Copy {enginecert} from the engine server to some file " + "here. Provide the file name below.\n\n" + "See {url} for more details, including using an external " + "certificate authority." + ).format( + base_touser=self._base_touser, + tmpcsr=self._csr_file.name, + enginecsr='{pkireqdir}/{remote_name}.req'.format( + pkireqdir=self._engine_pki_requests_dir, + remote_name=self._remote_name, + ), + enroll_command=self._enroll_command, + enginecert='{pkicertdir}/{remote_name}.cer'.format( + pkicertdir=self._engine_pki_certs_dir, + remote_name=self._remote_name, + ), + url=self._url, + ), + ) + goodcert = False + while not goodcert: + filename = self.dialog.queryString( + name='ENROLL_CERT_MANUAL_FILES_{base_name}'.format( + base_name=self._base_name, + ), + note=_( + '\nPlease input the location of the file where you ' + 'copied the signed certificate in step 3 above: ' + ), + prompt=True, + ) + try: + with open(filename) as f: + cert = f.read() + goodcert = self._pubkey == X509.load_cert_string( + cert + ).get_pubkey().as_pem(cipher=None) + if not goodcert: + self.logger.error( + _( + 'The certificate in {cert} does not match ' + 'the request in {csr}. Please try again.' + ).format( + cert=filename, + csr=self._csr_file.name, + ) + ) + except: + self.logger.error( + _( + 'Error while reading or parsing {cert}. ' + 'Please try again.' + ).format( + cert=filename, + ) + ) + self.logger.debug('Error reading cert', exc_info=True) + self.logger.info( + _('{base_touser} certificate read successfully').format( + base_touser=self._base_touser, + ) + ) + return cert + + def _enroll_cert_manual_inline(self): + pass + + def enroll_cert(self): + cert = None + + self.logger.debug('enroll_cert') + self._need_cert = not os.path.exists(self._cert_file) + self._need_key = not os.path.exists(self._key_file) + + if self._need_key: + self._key, self._csr, self._pubkey = self._genCsr() + self._need_cert = True + + if self._need_cert: + self._remote_name = '{name}-{fqdn}'.format( + name=self._base_name, + fqdn=self.environment[osetupcons.ConfigEnv.FQDN], + ) + self._enroll_command = ( + " /usr/share/ovirt-engine/bin/pki-enroll-request.sh \\\n" + " --name={remote_name} \\\n" + " --subject=\"" + "$(openssl x509 -in {engine_ca_cert_file} -noout " + "-subject | sed 's;subject= \(/C=[^/]*/O=[^/]*\)/.*;\\1;')" + "/CN={fqdn}\"" + ).format( + remote_name=self._remote_name, + engine_ca_cert_file=self._engine_ca_cert_file, + fqdn=self.environment[osetupcons.ConfigEnv.FQDN], + ) + self._remote_engine.configure(fqdn=self._engine_fqdn) + # TODO + # This is ugly - we rely on having these two plugins + # and do not support others. A good fix will: + # 1. Be completely pluggable + # 2. Will not duplicate the code in this function + # 3. Will be nice to the user in every style + # 4. Have a clearly-defined interface where relevant + # Perhaps we'll have to give up on some of these, not sure + # Also, for the meantime, we might/should implement + # manual_inline and have another function for that, + # or perhaps make _enroll_cert_manual_files work with both. + cert = { + osetupcons.Const.REMOTE_ENGINE_SETUP_STYLE_AUTO_SSH: + self._enroll_cert_auto_ssh, + osetupcons.Const.REMOTE_ENGINE_SETUP_STYLE_MANUAL_FILES: + self._enroll_cert_manual_files, + }[ + self._remote_engine.style().name + ]() + self._cert = cert + + def add_to_transaction( + self, + uninstall_group_name, + uninstall_group_desc, + ): + uninstall_files = [] + self.environment[ + osetupcons.CoreEnv.REGISTER_UNINSTALL_GROUPS + ].createGroup( + group=uninstall_group_name, + description=uninstall_group_desc, + optional=True, + ).addFiles( + group=uninstall_group_name, + fileList=uninstall_files, + ) + if self._need_key: + self.environment[otopicons.CoreEnv.MAIN_TRANSACTION].append( + filetransaction.FileTransaction( + name=self._key_file, + mode=0o600, + owner=self.environment[osetupcons.SystemEnv.USER_ENGINE], + enforcePermissions=True, + content=self._key, + modifiedList=uninstall_files, + ) + ) + + if self._need_cert: + self.environment[otopicons.CoreEnv.MAIN_TRANSACTION].append( + filetransaction.FileTransaction( + name=self._cert_file, + mode=0o600, + owner=self.environment[osetupcons.SystemEnv.USER_ENGINE], + enforcePermissions=True, + content=self._cert, + modifiedList=uninstall_files, + ) + ) + + def cleanup(self): + if self._csr_file is not None: + try: + os.unlink(self._csr_file.name) + except OSError: + self.logger.debug( + "Failed to delete '%s'", + self._csr_file.name, + exc_info=True, + ) + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/setup/ovirt_engine_setup/remote_engine_base.py b/packaging/setup/ovirt_engine_setup/remote_engine_base.py new file mode 100644 index 0000000..ca6216f --- /dev/null +++ b/packaging/setup/ovirt_engine_setup/remote_engine_base.py @@ -0,0 +1,74 @@ +# +# 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. +# + + +from otopi import util +from otopi import base + + [email protected] +class RemoteEngineBase(base.Base): + + def __init__(self, plugin): + super(RemoteEngineBase, self).__init__() + self._plugin = plugin + + @property + def plugin(self): + return self._plugin + + @property + def dialog(self): + return self._plugin.dialog + + @property + def environment(self): + return self._plugin.environment + + @property + def logger(self): + return self._plugin.logger + + @property + def name(self): + raise RuntimeError('Unset') + + def desc(self): + raise RuntimeError('Unset') + + def configure(self, fqdn): + pass + + def execute_on_engine(self, cmd, timeout=60): + pass + + def copy_from_engine(self, file_name): + pass + + def copy_to_engine( + self, + file_name, + content, + inp_env_key=None, + ): + pass + + def cleanup(self): + pass + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/setup/plugins/ovirt-engine-common/base/core/__init__.py b/packaging/setup/plugins/ovirt-engine-common/base/core/__init__.py index a15202f..259ed5e 100644 --- a/packaging/setup/plugins/ovirt-engine-common/base/core/__init__.py +++ b/packaging/setup/plugins/ovirt-engine-common/base/core/__init__.py @@ -1,6 +1,6 @@ # # ovirt-engine-setup -- ovirt engine setup -# Copyright (C) 2013 Red Hat, Inc. +# 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. diff --git a/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/__init__.py b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/__init__.py new file mode 100644 index 0000000..0b1491e --- /dev/null +++ b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/__init__.py @@ -0,0 +1,37 @@ +# +# 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-remove core plugin.""" + + +from otopi import util + + +from . import remote_engine +from . import remote_engine_manual_files +from . import remote_engine_root_ssh + + [email protected] +def createPlugins(context): + remote_engine.Plugin(context=context) + remote_engine_manual_files.Plugin(context=context) + remote_engine_root_ssh.Plugin(context=context) + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine.py b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine.py new file mode 100644 index 0000000..39b1457 --- /dev/null +++ b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine.py @@ -0,0 +1,92 @@ +# +# 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. +# + + +"""Remote engine plugin.""" + + +from otopi import constants as otopicons +from otopi import util +from otopi import plugin + + +from ovirt_engine_setup import constants as osetupcons +from ovirt_engine_setup import remote_engine + + [email protected] +class Plugin(plugin.PluginBase): + """Remote engine plugin.""" + + def __init__(self, context): + super(Plugin, self).__init__(context=context) + + @plugin.event( + stage=plugin.Stages.STAGE_INIT, + ) + def _init(self): + self.environment.setdefault( + osetupcons.CoreEnv.REMOTE_ENGINE, + None + ) + self.environment.setdefault( + osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLE, + None + ) + self.environment.setdefault( + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_SSH_PORT, + None + ) + self.environment.setdefault( + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_CLIENT_KEY, + None + ) + self.environment.setdefault( + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_KNOWN_HOSTS, + None + ) + self.environment.setdefault( + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_ROOT_PASSWORD, + None + ) + self.environment[ + otopicons.CoreEnv.LOG_FILTER_KEYS + ].append( + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_ROOT_PASSWORD + ) + self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLES + ] = [] + + @plugin.event( + stage=plugin.Stages.STAGE_SETUP, + ) + def _setup(self): + self.environment[ + osetupcons.CoreEnv.REMOTE_ENGINE + ] = remote_engine.RemoteEngine(plugin=self) + + @plugin.event( + stage=plugin.Stages.STAGE_CLEANUP, + ) + def _cleanup(self): + self.environment[ + osetupcons.CoreEnv.REMOTE_ENGINE + ].cleanup() + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_manual_files.py b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_manual_files.py new file mode 100644 index 0000000..b2eddb3 --- /dev/null +++ b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_manual_files.py @@ -0,0 +1,143 @@ +# +# 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. +# + + +import os +import tempfile +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 import remote_engine_base + + [email protected] +class Plugin(plugin.PluginBase): + + class _ManualFiles(remote_engine_base.RemoteEngineBase): + + def __init__(self, plugin): + super(Plugin._ManualFiles, self).__init__(plugin=plugin) + self._plugin = plugin + self._tempfiles = [] + + @property + def plugin(self): + return self._plugin + + @property + def dialog(self): + return self._plugin.dialog + + @property + def environment(self): + return self._plugin.environment + + @property + def logger(self): + return self._plugin.logger + + @property + def name(self): + return osetupcons.Const.REMOTE_ENGINE_SETUP_STYLE_MANUAL_FILES + + def desc(self): + return _( + 'Perform each action manually, use files to copy content ' + 'around' + ) + + def configure(self, fqdn): + self._fqdn = fqdn + + def execute_on_engine(self, cmd, timeout=60, text=None): + self.dialog.note( + text=text if text else _( + 'Please run on the engine server:\n\n' + '{cmd}\n\n' + ).format( + cmd=cmd + ) + ) + + def copy_from_engine(self, file_name, dialog_name): + resfilename = self.dialog.queryString( + name=dialog_name, + note=_( + 'Please copy {file_name} from the engine server to some ' + 'file here.\n' + 'Please input the location of the local file where you ' + 'copied {file_name} from the engine server: ' + ), + prompt=True, + ) + with open(resfilename) as f: + res = f.read() + return res + + def copy_to_engine(self, file_name, content, inp_env_key): + fname = self.environment.get(inp_env_key) + with ( + open(fname, 'w') if fname + else tempfile.NamedTemporaryFile(mode='w', delete=False) + ) as inpfile: + inpfile.write(content) + self.dialog.note( + text=_( + 'Please copy {inpfile} from here to {file_name} on the ' + 'engine server.\n' + ).format( + inpfile=inpfile.name, + file_name=file_name, + ) + ) + self._tempfiles.append(fname) + + def cleanup(self): + for f in self._tempfiles: + if f is not None: + try: + os.unlink(f.name) + except OSError: + self.logger.debug( + "Failed to delete '%s'", + f.name, + exc_info=True, + ) + + def __init__(self, context): + super(Plugin, self).__init__(context=context) + + @plugin.event( + stage=plugin.Stages.STAGE_SETUP, + ) + def _setup(self): + self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLES + ].append( + self._ManualFiles( + plugin=self, + ) + ) + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_root_ssh.py b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_root_ssh.py new file mode 100644 index 0000000..373c381 --- /dev/null +++ b/packaging/setup/plugins/ovirt-engine-common/base/remote_engine/remote_engine_root_ssh.py @@ -0,0 +1,283 @@ +# +# 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. +# + + +import socket +import paramiko +import time +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 import remote_engine_base + + [email protected] +class Plugin(plugin.PluginBase): + + class _RootSshManager(remote_engine_base.RemoteEngineBase): + + def __init__(self, plugin): + super(Plugin._RootSshManager, self).__init__(plugin=plugin) + self._plugin = plugin + self._client = None + self._fqdn = None + + @property + def plugin(self): + return self._plugin + + @property + def dialog(self): + return self._plugin.dialog + + @property + def environment(self): + return self._plugin.environment + + @property + def logger(self): + return self._plugin.logger + + @property + def name(self): + return osetupcons.Const.REMOTE_ENGINE_SETUP_STYLE_AUTO_SSH + + def desc(self): + return _('Access remote engine server using ssh as root') + + def _ssh_get_port(self): + port_valid = False + port = None + interactive = False + while not port_valid: + try: + key = osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_SSH_PORT + if self.environment[key] is None: + interactive = True + port = int( + self.dialog.queryString( + name='SSH_ACCESS_REMOTE_ENGINE_PORT', + note=_( + 'ssh port on remote engine server ' + '[@DEFAULT@]: ' + ), + prompt=True, + default=22, + ) + ) + self.environment[key] = port + paramiko.Transport((self._fqdn, port)) + port_valid = True + except ValueError as e: + self.logger.debug('exception', exc_info=True) + msg = _( + 'Invalid port number: {error}' + ).format( + error=e, + ) + if interactive: + self.logger.error(msg) + else: + raise RuntimeError(msg) + except (paramiko.SSHException, socket.gaierror) as e: + self.logger.debug('exception', exc_info=True) + msg = _( + 'Unable to connect to {fqdn}:{port}: {error}' + ).format( + fqdn=self._fqdn, + port=port, + error=e, + ) + if interactive: + self.logger.error(msg) + else: + raise RuntimeError(msg) + + def _ssh_connect(self): + connected = False + interactive = False + password = self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_ROOT_PASSWORD + ] + bad_password = False + while not connected: + try: + if password is None or bad_password: + interactive = True + password = self.dialog.queryString( + name='SSH_ACCESS_REMOTE_ENGINE_PASSWORD', + note=_( + 'root password on remote engine server ' + '{fqdn}: ' + ).format( + fqdn=self._fqdn, + ), + prompt=True, + hidden=True, + default='', + ) + client = paramiko.SSHClient() + client.set_missing_host_key_policy( + paramiko.WarningPolicy() + ) + # TODO Currently the warning goes only to the log file. + # We should probably write our own policy with a custom + # exception so that we can catch it below and verify with + # the user that it's ok. + client.load_system_host_keys( + self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_KNOWN_HOSTS + ] + ) + client.connect( + hostname=self._fqdn, + port=self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_SSH_PORT + ], + username='root', + password=password, + key_filename=self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_HOST_CLIENT_KEY + ], + ) + self._client = client + connected = True + except ( + paramiko.SSHException, + paramiko.AuthenticationException, + socket.gaierror, + ) as e: + self.logger.debug('exception', exc_info=True) + msg = _('Error: {error}').format(error=e) + if interactive: + self.logger.error(msg) + else: + raise RuntimeError(msg) + bad_password = True + + def configure(self, fqdn): + self._fqdn = fqdn + self._ssh_get_port() + self._ssh_connect() + + def execute_on_engine(self, cmd, timeout=60, text=None): + # Currently do not allow writing to stdin, only "batch mode" + # TODO consider something more complex/general, e.g. writing + # to stdin/reading from stdout interactively + self.logger.debug( + 'Executing on remote engine %s: %s' % + ( + self._fqdn, + cmd, + ) + ) + stdin, stdout, stderr = self._client.exec_command(cmd) + stdin.channel.shutdown(2) + outbuf = '' + errbuf = '' + exited = False + rc = None + while ( + (not stdout.channel.exit_status_ready()) and + (timeout > 0) + ): + time.sleep(1) + timeout -= 1 + while stdout.channel.recv_ready(): + outbuf += stdout.channel.recv(1000) + while stderr.channel.recv_ready(): + errbuf += stderr.channel.recv(1000) + + if not stdout.channel.exit_status_ready(): + stdout.channel.close() + stderr.channel.close() + while stdout.channel.recv_ready(): + outbuf += stdout.channel.recv(1000) + while stderr.channel.recv_ready(): + errbuf += stderr.channel.recv(1000) + time.sleep(1) + if stdout.channel.exit_status_ready(): + exited = True + rc = stdout.channel.recv_exit_status() + + return { + 'stdout': outbuf, + 'stderr': errbuf, + 'exited': exited, + 'rc': rc, + } + + def copy_from_engine(self, file_name): + self.logger.debug( + 'Copying data from remote engine %s:%s' % + ( + self._fqdn, + file_name, + ) + ) + sf = self._client.open_sftp() + res = None + with sf.open(file_name, 'r') as f: + res = f.read() + return res + + def copy_to_engine( + self, + file_name, + content, + inp_env_key=None + ): + self.logger.debug( + 'Copying data to remote engine %s:%s' % + ( + self._fqdn, + file_name, + ) + ) + sf = self._client.open_sftp() + with sf.open(file_name, 'w') as f: + f.write(content) + + def cleanup(self): + if self._client: + self._client.close() + + def __init__(self, context): + super(Plugin, self).__init__(context=context) + + @plugin.event( + stage=plugin.Stages.STAGE_SETUP, + # We want to be the default, so add early + priority=plugin.Stages.PRIORITY_HIGH, + ) + def _setup(self): + self.environment[ + osetupcons.ConfigEnv.REMOTE_ENGINE_SETUP_STYLES + ].append( + self._RootSshManager( + plugin=self, + ) + ) + + +# vim: expandtab tabstop=4 shiftwidth=4 -- To view, visit http://gerrit.ovirt.org/33231 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I67d709ba2b0138b8d372deb0b6de5e7a9836f417 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: ovirt-engine-3.5 Gerrit-Owner: Yedidyah Bar David <[email protected]> _______________________________________________ Engine-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-patches
