#! /usr/bin/python -E # Authors: Martin Kosek # # Copyright (C) 2011 Red Hat # see file 'COPYING' for use and warranty information # # 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, either version 3 of the License, or # (at your option) any later version. # # 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, see . # from ipapython.config import IPAOptionParser from ipapython import version from ipapython import ipautil from ipapython.ipautil import CalledProcessError from ipaserver.install import installutils import ipaclient.ipachangeconf from optparse import OptionGroup from ipapython.ipa_log_manager import * import sys import os import signal import tempfile import socket import time import threading import errno from socket import SOCK_STREAM, SOCK_DGRAM CONNECT_TIMEOUT = 5 RESPONDERS = [ ] QUIET = False CCACHE_FILE = "/etc/ipa/.conncheck_ccache" KRB5_CONFIG = None class CheckedPort(object): def __init__(self, port, port_type, description): self.port = port self.port_type = port_type self.description = description BASE_PORTS = [ CheckedPort(389, SOCK_STREAM, "Directory Service: Unsecure port"), CheckedPort(636, SOCK_STREAM, "Directory Service: Secure port"), CheckedPort(88, SOCK_STREAM, "Kerberos KDC: TCP"), CheckedPort(88, SOCK_DGRAM, "Kerberos KDC: UDP"), CheckedPort(464, SOCK_STREAM, "Kerberos Kpasswd: TCP"), CheckedPort(464, SOCK_DGRAM, "Kerberos Kpasswd: UDP"), CheckedPort(80, SOCK_STREAM, "HTTP Server: Unsecure port"), CheckedPort(443, SOCK_STREAM, "HTTP Server: Secure port"), ] CA_PORTS = [ CheckedPort(7389, SOCK_STREAM, "PKI-CA: Directory Service port"), ] def print_info(msg): if not QUIET: print msg def parse_options(): parser = IPAOptionParser(version=version.VERSION) replica_group = OptionGroup(parser, "on-replica options") replica_group.add_option("-m", "--master", dest="master", help="Master address with running IPA for output connection check") replica_group.add_option("-a", "--auto-master-check", dest="auto_master_check", action="store_true", default=False, help="Automatically execute connection check on master") replica_group.add_option("-r", "--realm", dest="realm", help="Realm name") replica_group.add_option("-k", "--kdc", dest="kdc", help="Master KDC. Defaults to master address") replica_group.add_option("-p", "--principal", dest="principal", default="admin", help="Principal to use to log in to remote master") replica_group.add_option("-w", "--password", dest="password", sensitive=True, help="Password for the principal"), parser.add_option_group(replica_group) master_group = OptionGroup(parser, "on-master options") master_group.add_option("-R", "--replica", dest="replica", help="Address of remote replica machine to check against") parser.add_option_group(master_group) common_group = OptionGroup(parser, "common options") common_group.add_option("-c", "--check-ca", dest="check_ca", action="store_true", default=False, help="Check also ports for Certificate Authority") common_group.add_option("", "--hostname", dest="hostname", help="The hostname of this server (FQDN). " "By default a nodename from uname(2) is used.") parser.add_option_group(common_group) parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False, help="Print debugging information") parser.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False, help="Output only errors") options, args = parser.parse_args() safe_options = parser.get_safe_opts(options) if options.master and options.replica: parser.error("on-master and on-replica options are mutually exclusive!") if options.master: if options.auto_master_check and not options.realm: parser.error("Realm is parameter is required to connect to remote master!") if not os.getegid() == 0: parser.error("You can only run on-replica part as root.") if options.master and not options.kdc: options.kdc = options.master if not options.master and not options.replica: parser.error("No action: you should select either --replica or --master option.") if not options.hostname: options.hostname = socket.getfqdn() if options.quiet: global QUIET QUIET = True return safe_options, options def logging_setup(options): log_file = None if os.getegid() == 0: log_file = "/var/log/ipareplica-conncheck.log" standard_logging_setup(log_file, debug=options.debug) def clean_responders(responders): if not responders: return for responder in responders: responder.stop() for responder in responders: responder.join() responders.remove(responder) def sigterm_handler(signum, frame): # do what SIGINT does (raise a KeyboardInterrupt) sigint_handler = signal.getsignal(signal.SIGINT) if callable(sigint_handler): sigint_handler(signum, frame) def configure_krb5_conf(realm, kdc, filename): krbconf = ipaclient.ipachangeconf.IPAChangeConf("IPA Installer") krbconf.setOptionAssignment(" = ") krbconf.setSectionNameDelimiters(("[","]")) krbconf.setSubSectionDelimiters(("{","}")) krbconf.setIndent((""," "," ")) opts = [{'name':'comment', 'type':'comment', 'value':'File created by ipa-replica-conncheck'}, {'name':'empty', 'type':'empty'}] #[libdefaults] libdefaults = [{'name':'default_realm', 'type':'option', 'value':realm}] libdefaults.append({'name':'dns_lookup_realm', 'type':'option', 'value':'false'}) libdefaults.append({'name':'dns_lookup_kdc', 'type':'option', 'value':'false'}) libdefaults.append({'name':'rdns', 'type':'option', 'value':'false'}) libdefaults.append({'name':'ticket_lifetime', 'type':'option', 'value':'24h'}) libdefaults.append({'name':'forwardable', 'type':'option', 'value':'yes'}) opts.append({'name':'libdefaults', 'type':'section', 'value': libdefaults}) opts.append({'name':'empty', 'type':'empty'}) #the following are necessary only if DNS discovery does not work #[realms] realms_info =[{'name':'kdc', 'type':'option', 'value':ipautil.format_netloc(kdc, 88)}, {'name':'admin_server', 'type':'option', 'value':ipautil.format_netloc(kdc, 749)}] realms = [{'name':realm, 'type':'subsection', 'value':realms_info}] opts.append({'name':'realms', 'type':'section', 'value':realms}) opts.append({'name':'empty', 'type':'empty'}) #[appdefaults] pamopts = [{'name':'debug', 'type':'option', 'value':'false'}, {'name':'ticket_lifetime', 'type':'option', 'value':'36000'}, {'name':'renew_lifetime', 'type':'option', 'value':'36000'}, {'name':'forwardable', 'type':'option', 'value':'true'}, {'name':'krb4_convert', 'type':'option', 'value':'false'}] appopts = [{'name':'pam', 'type':'subsection', 'value':pamopts}] opts.append({'name':'appdefaults', 'type':'section', 'value':appopts}) root_logger.debug("Writing temporary Kerberos configuration to %s:\n%s" % (filename, krbconf.dump(opts))) krbconf.newConf(filename, opts) class PortResponder(threading.Thread): def __init__(self, port, port_type, socket_timeout=1): super(PortResponder, self).__init__() self.port = port self.port_type = port_type self.socket_timeout = socket_timeout self._stop_request = False def run(self): while not self._stop_request: try: ipautil.bind_port_responder(self.port, self.port_type, socket_timeout=self.socket_timeout, responder_data="FreeIPA") except socket.timeout: pass except socket.error, e: if e.errno == errno.EADDRINUSE: time.sleep(1) else: raise def stop(self): self._stop_request = True def port_check(host, port_list): ip = installutils.resolve_host(host) if not ip: raise RuntimeError("Port check failed! Unable to resolve host name '%s'" % host) ports_failed = [] ports_udp_warning = [] # conncheck could not verify that port is open for port in port_list: if ipautil.host_port_open(host, port.port, port.port_type, socket_timeout=CONNECT_TIMEOUT): result = "OK" else: if port.port_type == socket.SOCK_DGRAM: ports_udp_warning.append(port) result = "WARNING" else: ports_failed.append(port) result = "FAILED" print_info(" %s (%d): %s" % (port.description, port.port, result)) if ports_udp_warning: print "The following UDP ports could not be verified as open: %s" \ % ", ".join(str(port.port) for port in ports_udp_warning) print "This can happen if they are already bound to an application" print "and ipa-replica-conncheck cannot attach own UDP responder." if ports_failed: msg_ports = [] for port in ports_failed: port_type_text = "TCP" if port.port_type == SOCK_STREAM else "UDP" msg_ports.append('%d (%s)' % (port.port, port_type_text)) raise RuntimeError("Port check failed! Inaccessible port(s): %s" \ % ", ".join(msg_ports)) def main(): safe_options, options = parse_options() logging_setup(options) root_logger.debug('%s was invoked with options: %s' % (sys.argv[0], safe_options)) root_logger.debug("missing options might be asked for interactively later\n") signal.signal(signal.SIGTERM, sigterm_handler) required_ports = BASE_PORTS if options.check_ca: required_ports.extend(CA_PORTS) if options.replica: print_info("Check connection from master to remote replica '%s':" % options.replica) port_check(options.replica, required_ports) print_info("\nConnection from master to replica is OK.") # kinit to foreign master if options.master: # check ports on master first print_info("Check connection from replica to remote master '%s':" % options.master) tcp_ports = [ port for port in required_ports if port.port_type == SOCK_STREAM ] udp_ports = [ port for port in required_ports if port.port_type == SOCK_DGRAM ] port_check(options.master, tcp_ports) if udp_ports: print_info("\nThe following list of ports use UDP protocol and would need to be") print_info("checked manually:") for port in udp_ports: result = "SKIPPED" print_info(" %s (%d): %s" % (port.description, port.port, result)) print_info("\nConnection from replica to master is OK.") # create listeners global RESPONDERS print_info("Start listening on required ports for remote master check") for port in required_ports: root_logger.debug("Start listening on port %d (%s)" % (port.port, port.description)) responder = PortResponder(port.port, port.port_type) responder.start() RESPONDERS.append(responder) if options.auto_master_check: (krb_fd, krb_name) = tempfile.mkstemp() os.close(krb_fd) configure_krb5_conf(options.realm, options.kdc, krb_name) global KRB5_CONFIG KRB5_CONFIG = krb_name print_info("Get credentials to log in to remote master") if options.principal.find('@') == -1: principal = '%s@%s' % (options.principal, options.realm) user = options.principal else: principal = options.principal user = options.principal.partition('@')[0] if options.password: password=options.password else: password = installutils.read_password(principal, confirm=False, validate=False, retry=False) if password is None: sys.exit("\nPrincipal password required") stderr='' (stdout, stderr, returncode) = ipautil.run(['/usr/bin/kinit', principal], env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, stdin=password, raiseonerr=False) if returncode != 0: raise RuntimeError("Cannot acquire Kerberos ticket: %s" % stderr) # Verify kinit was actually successful stderr='' (stdout, stderr, returncode) = ipautil.run(['/usr/bin/kvno', 'host/%s' % options.master], env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME':CCACHE_FILE}, raiseonerr=False) if returncode != 0: raise RuntimeError("Could not get ticket for master server: %s" % stderr) remote_check_opts = ['--replica %s' % options.hostname] if options.check_ca: remote_check_opts.append('--check-ca') print_info("Execute check on remote master") stderr = '' remote_addr = "%s@%s" % (user, options.master) (stdout, stderr, returncode) = ipautil.run(['/usr/bin/ssh', '-q', '-o StrictHostKeychecking=no', '-o UserKnownHostsFile=/dev/null', remote_addr, "/usr/sbin/ipa-replica-conncheck " + " ".join(remote_check_opts)], env={'KRB5_CONFIG':KRB5_CONFIG, 'KRB5CCNAME' : CCACHE_FILE}, raiseonerr=False) print_info(stdout) if returncode != 0: raise RuntimeError("Remote master check failed with following error message(s):\n%s" % stderr) else: # wait until user test is ready print_info("Listeners are started. Use CTRL+C to terminate the listening part after the test.") print_info("") print_info("Please run the following command on remote master:") remote_check_opts = ['--replica %s' % options.hostname] if options.check_ca: remote_check_opts.append('--check-ca') print_info("/usr/sbin/ipa-replica-conncheck " + " ".join(remote_check_opts)) time.sleep(3600) print_info("Connection check timeout: terminating listening program") if __name__ == "__main__": try: sys.exit(main()) except SystemExit, e: sys.exit(e) except KeyboardInterrupt: print_info("\nCleaning up...") sys.exit(1) except RuntimeError, e: sys.exit(e) finally: clean_responders(RESPONDERS) for file_name in (CCACHE_FILE, KRB5_CONFIG): if file_name: try: os.remove(file_name) except OSError: pass