Change in vdsm[master]: vdsm-tool: Add register verb

dougsland at redhat.com dougsland at redhat.com
Sat May 16 17:02:05 UTC 2015


Douglas Schilling Landgraf has uploaded a new change for review.

Change subject: vdsm-tool: Add register verb
......................................................................

vdsm-tool: Add register verb

The new verb register will make the registration
from a node into the Engine. This verb supports
old Engine registration schema and the new one as
service. This commit it's an alternative for
the registration discussions.

Supported Engine: >= 3.3

Change-Id: Ica800027beec1e5a20165bb5e1e78baf9283c1da
Signed-off-by: Douglas Schilling Landgraf <dougsland at redhat.com>
---
M debian/vdsm-python.install
M lib/vdsm/tool/Makefile.am
A lib/vdsm/tool/register.py
M vdsm.spec.in
4 files changed, 524 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/66/40966/1

diff --git a/debian/vdsm-python.install b/debian/vdsm-python.install
index 7a50353..394d3d3 100644
--- a/debian/vdsm-python.install
+++ b/debian/vdsm-python.install
@@ -36,6 +36,7 @@
 ./usr/lib/python2.7/dist-packages/vdsm/tool/load_needed_modules.py
 ./usr/lib/python2.7/dist-packages/vdsm/tool/nwfilter.py
 ./usr/lib/python2.7/dist-packages/vdsm/tool/passwd.py
+./usr/lib/python2.7/dist-packages/vdsm/tool/register.py
 ./usr/lib/python2.7/dist-packages/vdsm/tool/restore_nets.py
 ./usr/lib/python2.7/dist-packages/vdsm/tool/service.py
 ./usr/lib/python2.7/dist-packages/vdsm/tool/transient.py
diff --git a/lib/vdsm/tool/Makefile.am b/lib/vdsm/tool/Makefile.am
index 9f8108c..f939d90 100644
--- a/lib/vdsm/tool/Makefile.am
+++ b/lib/vdsm/tool/Makefile.am
@@ -39,6 +39,7 @@
 	nwfilter.py \
 	configfile.py \
 	configurator.py \
+	register.py \
 	restore_nets.py \
 	service.py \
 	transient.py \
diff --git a/lib/vdsm/tool/register.py b/lib/vdsm/tool/register.py
new file mode 100644
index 0000000..c75e64f
--- /dev/null
+++ b/lib/vdsm/tool/register.py
@@ -0,0 +1,521 @@
+# Copyright (C) 2012-2015 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+import argparse
+import getpass
+import logging
+import M2Crypto
+import os
+import pwd
+import requests
+import selinux
+import socket
+import ssl
+import sys
+import tempfile
+
+from . import expose
+from .. import utils
+
+from vdsm.utils import getHostUUID
+
+
+class Register(object):
+
+    def __init__(self, engine_fqdn, engine_http_port=None,
+                 fingerprint=None, ssh_port=None,
+                 ssh_user=None, check_fqdn=True,
+                 vdc_port=None, node_fqdn=None,
+                 node_name=None):
+        """
+        Attributes:
+
+        engine_fqdn - Engine FQDN
+        engine_port - Engine http port
+        fingeprint  - Fingerprint to be validated
+        ssh_user    - SSH user that will establish the connection from Engine
+        ssh_port    - Port of ssh daemon is running
+        check_fqdn  - Validate Engine FQDN against CA (True or False)
+                      Default is TRUE
+        vdc_port    - The communication port between VDSM and Engine
+        node_fqdn   - Specify node FQDN
+        node_name   - Specify node name
+        """
+        self.logger = self.__set_logger()
+        self.logger.info("=======================================")
+        self.logger.info("Registering the node")
+        self.logger.info("=======================================")
+        self.logger.info("Received the following attributes:")
+
+        self.engine_fqdn = engine_fqdn
+        self.logger.info("Engine FQDN: {fqdn}".format(fqdn=self.engine_fqdn))
+
+        self.engine_url = "https://{e}".format(e=engine_fqdn)
+        self.logger.info("Engine URL: {url}".format(url=self.engine_url))
+        self.check_fqdn = check_fqdn
+
+        self.fprint = fingerprint
+        self.logger.info("Fingerprint: {fp}".format(fp=self.fprint))
+
+        if node_fqdn is None:
+            self.node_fqdn = socket.gethostname()
+        else:
+            self.node_fqdn = node_fqdn
+        self.logger.info("Node FQDN: {nf}".format(nf=self.node_fqdn))
+
+        if node_name is None:
+            self.node_name = socket.gethostname().split(".")[0]
+        else:
+            self.node_name = node_name
+        self.logger.info("Node name: {na}".format(na=self.node_name))
+
+        if ssh_user is None:
+            self.ssh_user = getpass.getuser()
+        else:
+            self.ssh_user = ssh_user
+        self.logger.info("SSH User: {su}".format(su=self.ssh_user))
+
+        if ssh_port is None:
+            self.ssh_port = "22"
+        else:
+            self.ssh_port = ssh_port
+        self.logger.info("SSH Port: {sp}".format(sp=self.ssh_port))
+
+        if vdc_port is None:
+            self.vdc_port = "54321"
+        else:
+            self.vdc_port = vdc_port
+        self.logger.info("VDC Port: {sp}".format(sp=self.vdc_port))
+
+        if engine_http_port is None:
+            self.engine_port = "443"
+        else:
+            self.engine_port = engine_http_port
+        self.logger.info("Engine http port: {hp}".format(hp=self.engine_port))
+
+        self.ca_dir = "/etc/pki/ovirt-engine/"
+        self.ca_engine = "{d}{f}".format(d=self.ca_dir, f="ca.pem")
+        self.logger.info("Engine CA: {ca}".format(ca=self.ca_engine))
+
+        self.__get_protocol()
+
+    def __get_protocol(self):
+        """
+        Determine if Engine is running in registration
+        protocol version legacy or service
+        """
+
+        self.__print_and_log("Identifying the registration protocol...",
+                             level="info")
+
+        ucmd = "/ovirt-engine/services/host-register?version=1&command="
+        __GET_VERSION = "https://{e}{u}{c}".format(e=self.engine_fqdn,
+                                                   u=ucmd,
+                                                   c="get-version")
+
+        try:
+            res = requests.get(__GET_VERSION, verify=False)
+        except Exception as e:
+            self.__print_and_log(e, level="error")
+            raise e
+
+        if res.status_code != 200:
+            self.reg_protocol = "legacy"
+            self.url_CA = self.engine_url
+
+            self.url_ssh_key = "{e}{k}".format(e=self.engine_url,
+                                               k="/engine.ssh.key.txt")
+
+            ureg = "/OvirtEngineWeb/register?vds_ip={fqdn}" \
+                "&vds_name={name}&port={mp}".format(fqdn=self.node_fqdn,
+                                                    name=self.node_name,
+                                                    mp=self.vdc_port)
+
+            self.url_reg = "{e}{u}".format(e=self.engine_url, u=ureg)
+        else:
+            self.reg_protocol = "service"
+
+            self.url_CA = "{e}{uc}{c}".format(e=self.engine_url,
+                                              uc=ucmd,
+                                              c="get-pki-trust")
+
+            self.url_ssh_key = "{e}{uc}{c}".format(e=self.engine_url,
+                                                   uc=ucmd,
+                                                   c="get-ssh-trust")
+
+            ureg = "{uc}register&name={name}&address={fqdn}&sshUser={sshu}&" \
+                   "sshPort={sshp}&port={mp}".format(uc=ucmd,
+                                                     name=self.node_name,
+                                                     fqdn=self.node_fqdn,
+                                                     sshu=self.ssh_user,
+                                                     sshp=self.ssh_port,
+                                                     mp=self.vdc_port)
+
+            self.url_reg = "{e}{u}".format(e=self.engine_url, u=ureg)
+
+        self.__print_and_log("Registration procotol selected: {p}".format(
+                             p=self.reg_protocol), level="info")
+
+        self.logger.info("Download CA via: {u}".format(u=self.url_CA))
+        self.logger.info("Download SSH via: {u}".format(u=self.url_ssh_key))
+
+    def __print_and_log(self, msg, level):
+        """
+        Print and log a message
+        """
+        print(msg)
+
+        if level == "error":
+            self.logger.error(msg, exc_info=True)
+        elif level == "debug":
+            self.logger.debug(msg)
+        elif level == "warning":
+            self.logger.warning(msg)
+        elif level == "critical":
+            self.logger.critical(msg)
+        elif level == "info":
+            self.logger.info(msg)
+
+    def __set_logger(self):
+        """
+        The logging settings
+        Saving log in: /var/log/vdsm/register.log
+        """
+        # Avoid node side effects of logging module not been working
+        logging.shutdown()
+        reload(logging)
+
+        logging.basicConfig(
+            filename="/var/log/vdsm/register.log",
+            level=logging.INFO,
+            format='%(asctime)s %(message)s',
+            datefmt='%m/%d/%Y %I:%M:%S %p',
+        )
+
+        if sys.version_info >= (2, 7):
+            logging.captureWarnings(True)
+
+        return logging.getLogger(__name__)
+
+    def __execute_http_request(self, url, cert_validation=True):
+        """
+        Execute http requests
+        url -- URL to be requested
+        cert_validation -- SSL cert will be verified
+
+        Returns: Content of http request
+        """
+        if cert_validation and self.check_fqdn:
+            cert_validation = self.ca_engine
+        else:
+            cert_validation = False
+
+        try:
+            res = requests.get("{u}".format(u=url), verify=cert_validation)
+            if res.status_code != 200:
+                self.logger.error("http response was not OK", exc_info=True)
+                raise requests.RequestException
+        except requests.RequestException as e:
+            self.logger.error("Cannot connect to engine", exc_info=True)
+            raise e
+
+        return res.content
+
+    def __silent_restorecon(self, path):
+        """
+        Execute selinux restorecon cmd to determined file
+
+        Args
+        path -- full path to file
+        """
+
+        try:
+            if selinux.is_selinux_enabled():
+                selinux.restorecon(path)
+        except:
+            self.__print_and_log("restorecon %s failed" % path)
+
+    def __calculate_fingerprint(self, cert):
+        """
+        Calculate fingerprint of certificate
+
+        Args
+        cert -- certificate file to be calculated the fingerprint
+
+        Returns
+        The fingerprint
+        """
+
+        with open(cert, 'r') as f:
+            cert = f.read()
+
+        x509 = M2Crypto.X509.load_cert_string(cert, M2Crypto.X509.FORMAT_PEM)
+        fp = x509.get_fingerprint('sha1')
+        fp = ':'.join(fp[pos:pos + 2] for pos in range(0, len(fp), 2))
+
+        return fp
+
+    def download_ca(self):
+        """
+        Download CA from Engine and save self.ca_engine
+        """
+        if self.reg_protocol == "legacy":
+            # The legacy version uses the format: UUID_MACADDRESS
+            self.uuid = getHostUUID(legacy=True)
+            self.url_reg += "&vds_unique_id={u}".format(u=self.uuid)
+        else:
+            # Non legacy version uses the format: UUID
+            self.uuid = getHostUUID(legacy=False)
+            self.url_reg += "&uniqueId={u}".format(u=self.uuid)
+
+        self.logger.info("Registration via: {u}".format(u=self.url_reg))
+
+        __VDSM_ID = "/etc/vdsm/vdsm.id"
+        if not os.path.exists(__VDSM_ID):
+            with open(__VDSM_ID, 'w') as f:
+                f.write(self.uuid)
+
+            if utils.isOvirtNode():
+                from ovirt.node.utils.fs import Config
+                Config().persist(__VDSM_ID)
+
+        self.__print_and_log("Host UUID: {u}".format(u=self.uuid),
+                             level="info")
+
+        self.__print_and_log("Collecting CA data from Engine...",
+                             level="info")
+
+        # If engine CA dir doesnt exist create it and download the ca.pem
+        temp_ca_file = None
+        if os.path.exists(self.ca_engine):
+            calculated_fprint = self.__calculate_fingerprint(self.ca_engine)
+        else:
+            if not os.path.exists(self.ca_dir):
+                os.makedirs(self.ca_dir, 0o755)
+                self.__silent_restorecon(self.ca_dir)
+                if utils.isOvirtNode():
+                    from ovirt.node.utils.fs import Config
+                    Config().persist(self.ca_dir)
+
+            if self.reg_protocol == "legacy":
+                res = ssl.get_server_certificate(
+                    (self.engine_fqdn, int(self.engine_port))
+                )
+            else:
+                res = self.__execute_http_request(self.url_CA,
+                                                  cert_validation=False)
+
+            with tempfile.NamedTemporaryFile(
+                dir=os.path.dirname(self.ca_dir),
+                delete=False
+            ) as f:
+                f.write(res)
+
+            calculated_fprint = self.__calculate_fingerprint(f.name)
+            temp_ca_file = True
+
+        if self.fprint and self.fprint != calculated_fprint:
+            msg = "The fingeprints doesn't match:\n" \
+                  "Calculated fingerprint: [{c}]\n" \
+                  "Attribute fingerprint:  [{a}]".format(c=calculated_fprint,
+                                                         a=self.fprint)
+
+            self.__print_and_log(msg, "error")
+            if temp_ca_file:
+                os.unlink(f.name)
+            raise RuntimeError(msg)
+
+        if temp_ca_file:
+            os.rename(f.name, self.ca_engine)
+
+        self.fprint = calculated_fprint
+        self.__print_and_log("Calculated fingerprint: {f}".format(
+                             f=self.fprint), level="info")
+
+        if utils.isOvirtNode():
+            from ovirt.node.utils.fs import Config
+            Config().persist(self.ca_engine)
+
+    def download_ssh(self):
+        """
+        Download ssh authorized keys and save it in the node
+        """
+        _auth_keys_dir = pwd.getpwuid(pwd.getpwnam(
+                                      self.ssh_user).pw_uid).pw_dir + "/.ssh"
+        _auth_keys = _auth_keys_dir + "/authorized_keys"
+
+        if not os.path.exists(_auth_keys_dir):
+            os.makedirs(_auth_keys_dir, 0o700)
+            self.__silent_restorecon(_auth_keys_dir)
+            if utils.isOvirtNode():
+                from ovirt.node.utils.fs import Config
+                Config().persist(_auth_keys_dir)
+
+        res = self.__execute_http_request(self.url_ssh_key)
+        with tempfile.NamedTemporaryFile(
+            dir=_auth_keys_dir,
+            delete=False
+        ) as f:
+            f.write(res)
+
+        # If ssh key is new append it into autorized_keys
+        with open(f.name, "r") as f_ro:
+            content = f_ro.read()
+            with open(_auth_keys, "a+") as f_w:
+                if content not in f_w.read():
+                    f_w.write(content)
+                    os.chmod(_auth_keys, 0o600)
+                    self.__silent_restorecon(_auth_keys)
+
+        os.unlink(f.name)
+        if utils.isOvirtNode():
+            from ovirt.node.utils.fs import Config
+            Config().persist(_auth_keys)
+
+    def execute_registration(self):
+        """
+        Trigger the registration command against Engine
+        """
+        self.__execute_http_request(self.url_reg)
+        self.__print_and_log("Registration completed, host is pending approval"
+                             " on Engine: {e}".format(e=self.engine_fqdn),
+                             level="info")
+
+    def get_fingerprint(self):
+        """
+        Return the fingerprint for validation
+        """
+        return self.fprint
+
+
+ at expose('register')
+def main(*args):
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawTextHelpFormatter,
+        description='Tool to register node to Engine',
+        epilog='Example of use:\n%(prog)s '
+                    '--engine-fqdn engine.mydomain'
+    )
+
+    # Needed by vdsm-tool
+    parser.add_argument(
+        'register',
+    )
+
+    parser.add_argument(
+        '--node-fqdn',
+        help="Define node FQDN or IP address."
+             " If not provided, will be used system host name",
+    )
+
+    parser.add_argument(
+        '--node-name',
+        help="Define node name."
+             " If not provided, will be used system short host name"
+             " (the name before the first dot in the system host name)",
+    )
+
+    parser.add_argument(
+        '--engine-fqdn',
+        help="Engine FQDN (See also: --check-fqdn)",
+        required=True
+    )
+
+    parser.add_argument(
+        '--ssh-user',
+        help="SSH username to establish the connection with Engine. "
+             "If not provided, the user which is "
+             "executing the script will catch and used",
+    )
+
+    parser.add_argument(
+        '--ssh-port',
+        help="SSH port to establish the connection with Engine "
+             "If not provided, the script will use the default "
+             "SSH port 22"
+    )
+
+    parser.add_argument(
+        '--check-fqdn',
+        help="Disable or Enable FQDN check for Engine CA, this option "
+             "is enabled by default (Use: True or False)",
+    )
+
+    parser.add_argument(
+        '--fingerprint',
+        help="Specify an existing fingerprint to be validated against "
+             "Engine CA fingerprint",
+    )
+
+    args = parser.parse_args()
+
+    reg = Register(engine_fqdn=args.engine_fqdn,
+                   node_fqdn=args.node_fqdn,
+                   node_name=args.node_name,
+                   ssh_user=args.ssh_user,
+                   ssh_port=args.ssh_port,
+                   fingerprint=args.fingerprint,
+                   check_fqdn=args.check_fqdn)
+
+    reg.download_ca()
+    reg.download_ssh()
+    reg.execute_registration()
+
+if __name__ == '__main__':
+    main()
+
+"""
+Registration schema:
+
+UUID
+=========
+    - If there is UUID already generated for the system will be
+      available in /etc/vdsm/vdsm.id
+
+    - In case, there is no UUID, use auxiliary function from VDSM
+      to generate it and store in /etc/vdsm/vdsm.id
+
+Legacy reg:
+============
+    - Process UUID
+
+    - Download CA via
+      https://ENGINE_FQDN
+
+    - Download ssh pub key
+      https://ENGINE_FQDN/engine.ssh.key.txt
+
+    - Register via URL:
+      (Original .NET version and earlier Linux versions)
+      https://ENGINE_FQDN/RHEVManagerWeb/VdsAutoRegistration.aspx?vds_ip=NODE_FQDN_OR_IP&vds_name=NODE_NAME&vds_unique_id=NODE_UUID&port=54321
+
+      or
+
+      https://ENGINE_FQDN/OvirtEngineWeb/register?vds_ip=NODE_FQDN_OR_IP&vds_name=NODE_NAME&vds_unique_id=NODE_UUID&port=54321
+
+Service reg:
+============
+    - Process UUID
+
+    - Download CA via get-pki-trust URL
+      https://ENGINE_FQDN/ovirt-engine/services/host-register?version=1&command=get-pki-trust
+
+    - Download ssh pub key via get-ssh-trust URL
+      https://ENGINE_FQDN/ovirt-engine/services/host-register?version=1&command=get-ssh-trust
+
+    - Register via URL:
+      https://ENGINE_FQDN/ovirt-engine/services/host-register?version=1&command=register&name=NODE_NAME&address=NO_FQDN_OR_IP&uniqueId=NODE_UUID&sshUser=SSH_USERNAME&sshPort=SSHD_PORT
+"""
diff --git a/vdsm.spec.in b/vdsm.spec.in
index 52a8b37..108dff6 100644
--- a/vdsm.spec.in
+++ b/vdsm.spec.in
@@ -1372,6 +1372,7 @@
 %{python_sitelib}/%{vdsm_name}/tool/configurators/sebool.py*
 %{python_sitelib}/%{vdsm_name}/tool/configurators/multipath.py*
 %{python_sitelib}/%{vdsm_name}/tool/dump_volume_chains.py*
+%{python_sitelib}/%{vdsm_name}/tool/register.py*
 %{python_sitelib}/%{vdsm_name}/tool/restore_nets.py*
 %{python_sitelib}/%{vdsm_name}/tool/service.py*
 %{python_sitelib}/%{vdsm_name}/tool/transient.py*


-- 
To view, visit https://gerrit.ovirt.org/40966
To unsubscribe, visit https://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ica800027beec1e5a20165bb5e1e78baf9283c1da
Gerrit-PatchSet: 1
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Douglas Schilling Landgraf <dougsland at redhat.com>


More information about the vdsm-patches mailing list