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

Reply via email to