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 <[email protected]> --- 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 + + +@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 <[email protected]> _______________________________________________ vdsm-patches mailing list [email protected] https://lists.fedorahosted.org/mailman/listinfo/vdsm-patches
