On Sat, 2011-05-28 at 00:10 -0400, Rob Crittenden wrote: > Martin Kosek wrote: > > On Mon, 2011-05-23 at 16:41 -0400, Rob Crittenden wrote: > >> Martin Kosek wrote: > >>> This is a first version of connection checking program for replica > >>> installation. See patch for program purpose description. Currently, > >>> there is no man pages for the program. > >>> > >>> Note to Simo and Rob: I use password for logging as admin. Btw would it > >>> be safe to have an admin keytab in the replica file? Replica file > >>> contents are lying freely in /tmp after the replica installation. > >>> > >>> Martin > >> > >> nack, you aren't including the new binary in the spec. > > > > Oh, thanks for this one. > > > >> > >> You should also: > >> > >> - set KRB5CCNAME to a temporary ccache and remove that when the install > >> exists (successful or not) > > > > Done. > > > >> - remove the temporary krb5.conf you create > > > > Done. > > > >> - be a bit more explicit what we are doing, at least more than "Run > >> connection check to master". > > > > Actually, I am if you run the new script separately. I removed "--quiet" > > parameter passed to the script in ipa-replica-install so that it is more > > verbose. Plus, I improved texts sent to the user. > > > >> - yes, we should remove the replica file contents > > > > I enhanced ipa-replica-install to do that. > > > > Martin > > > > Works great until the very end: > ... > ... > > Execute check on remote master > Check connection from master to remote replica 'slinky.greyoak.com': > Directory Service: unsecure port (389): FAILED > Directory Service: secure port (636): FAILED > Kerberos (88): OK > > Remote master check failed with following error message(s): > Could not chdir to home directory /home/admin: No such file or directory > Port check failed! Unaccessible port(s): 389, 636 > > Connection check failed with following error: None > > rob
Right, I introduced this wrong error message in the last patch. I fixed this one and also one typo. Updated patch attached. Martin
>From ac6c38804498480c472106b054121d4aafc8423a Mon Sep 17 00:00:00 2001 From: Martin Kosek <mko...@redhat.com> Date: Sun, 22 May 2011 19:17:07 +0200 Subject: [PATCH] Connection check program for replica installation When connection between a master machine and future replica is not sane, the replica installation may fail unexpectedly with inconvenient error messages. One common problem is misconfigured firewall. This patch adds a program ipa-replica-conncheck which tests the connection using the following procedure: 1) Execute the on-replica check testing the connection to master 2) Open required ports on local machine 3) Ask user to run the on-master part of the check OR run it automatically: a) kinit to master as default admin user with given password b) run the on-master part using ssh 4) When master part is executed, it checks connection back to the replica and prints the check result This program is run by ipa-replica-install as mandatory part. It can, however, be skipped using --skip-conncheck option. ipa-replica-install now requires password for admin user to run the command on remote master. https://fedorahosted.org/freeipa/ticket/1107 --- freeipa.spec.in | 1 + install/tools/ipa-replica-conncheck | 372 +++++++++++++++++++++++++++++++ install/tools/ipa-replica-install | 40 ++++ install/tools/man/ipa-replica-install.1 | 6 + ipapython/ipautil.py | 73 ++++++ 5 files changed, 492 insertions(+), 0 deletions(-) create mode 100755 install/tools/ipa-replica-conncheck diff --git a/freeipa.spec.in b/freeipa.spec.in index b9366165a6efe9515e9b3527947d301948a714f5..5042bfe592014b49b0691081831e603e6156e8ce 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -357,6 +357,7 @@ fi %doc COPYING README Contributors.txt %{_sbindir}/ipa-dns-install %{_sbindir}/ipa-server-install +%{_sbindir}/ipa-replica-conncheck %{_sbindir}/ipa-replica-install %{_sbindir}/ipa-replica-prepare %{_sbindir}/ipa-replica-manage diff --git a/install/tools/ipa-replica-conncheck b/install/tools/ipa-replica-conncheck new file mode 100755 index 0000000000000000000000000000000000000000..5030e5da7866de3558d2ffa7fa4a89dcc2ccf70f --- /dev/null +++ b/install/tools/ipa-replica-conncheck @@ -0,0 +1,372 @@ +#! /usr/bin/python -E +# Authors: Martin Kosek <mko...@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. +# + +from ipapython.config import IPAOptionParser +from ipapython import version +from ipapython import ipautil +from ipapython.ipautil import CalledProcessError +import ipaclient.ipachangeconf +from optparse import OptionGroup +import logging +import sys +import os +import signal +import tempfile +import getpass +import socket +import time +import threading +import errno + +CONNECT_TIMEOUT = 5 +RESPONDERS = [ ] +QUIET = False +CCACHE_FILE = "/etc/ipa/.conncheck_ccache" +KRB5_CONFIG = None + +class CheckedPort(object): + def __init__(self, port, stream, description): + self.port = port + self.stream = stream + self.description = description + +BASE_PORTS = [ + CheckedPort(389, True, "Directory Service: unsecure port"), + CheckedPort(636, True, "Directory Service: secure port"), + CheckedPort(88, False, "Kerberos"), + ] + +CA_PORTS = [ + CheckedPort(7389, True, "PKI-CA: Directory Service"), + CheckedPort(9444, True, "PKI-CA: EE Secure port"), + CheckedPort(9445, True, "PKI-CA: Admin Secure port"), + CheckedPort(9446, True, "PKI-CA: EE Secure Client Auth port"), + CheckedPort(9180, True, "PKI-CA: Unsecure 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 of 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): + if os.getegid() == 0: + log_file = "/var/log/ipareplica-conncheck.log" + old_umask = os.umask(077) + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(message)s', + filename=log_file, + filemode='w') + os.umask(old_umask) + + console = logging.StreamHandler() + # If the debug option is set, also log debug messages to the console + if options.debug: + console.setLevel(logging.DEBUG) + else: + # Otherwise, log critical and error messages + console.setLevel(logging.ERROR) + formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + +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): + print_info("\nCleaning up...") + + global RESPONDERS + clean_responders(RESPONDERS) + + sys.exit(1) + +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':kdc+':88'}, + {'name':'admin_server', 'type':'option', 'value':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}) + + logging.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, socket_stream = True, socket_timeout=1): + super(PortResponder, self).__init__() + self.port = port + self.socket_stream = socket_stream + 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.socket_stream, + 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): + failed_ports = [] + for port in port_list: + if ipautil.host_port_open(host, port.port, port.stream, CONNECT_TIMEOUT): + result = "OK" + else: + failed_ports.append(port) + result = "FAILED" + print_info(" %s (%d): %s" % (port.description, port.port, result)) + + if failed_ports: + msg_ports = ", ".join([str(port.port) for port in failed_ports]) + raise RuntimeError("Port check failed! Inaccessible port(s): %s" % msg_ports) + +def main(): + safe_options, options = parse_options() + + logging_setup(options) + logging.debug('%s was invoked with options: %s' % (sys.argv[0], safe_options)) + logging.debug("missing options might be asked for interactively later\n") + + signal.signal(signal.SIGTERM, sigterm_handler) + signal.signal(signal.SIGINT, 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) + port_check( options.master, required_ports) + 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: + logging.debug("Start listening on port %d (%s)" % (port.port, port.description)) + responder = PortResponder(port.port, port.stream) + 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 = getpass.getpass("Password for %s: " % principal) + + 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) + + 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', 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: + 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 + diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install index 293a0a06c8e4ff608d8327135ea1b4e008ab4d33..1b84daef107734c782a67710a356356e4f9f1971 100755 --- a/install/tools/ipa-replica-install +++ b/install/tools/ipa-replica-install @@ -38,6 +38,7 @@ from ipapython.config import IPAOptionParser from ipapython import sysrestore CACERT="/etc/ipa/ca.crt" +REPLICA_INFO_TOP_DIR=None class ReplicaConfig: def __init__(self): @@ -58,6 +59,8 @@ def parse_options(): default=False, help="gather extra debugging information") parser.add_option("-p", "--password", dest="password", sensitive=True, help="Directory Manager (existing master) password") + parser.add_option("-w", "--admin-password", dest="admin_password", sensitive=True, + help="Admin user Kerberos password used for connection check") parser.add_option("--setup-dns", dest="setup_dns", action="store_true", default=False, help="configure bind with our zone") parser.add_option("--forwarder", dest="forwarders", action="append", @@ -71,6 +74,8 @@ def parse_options(): help="Do not use DNS for hostname lookup during installation") parser.add_option("--no-pkinit", dest="setup_pkinit", action="store_false", default=True, help="disables pkinit setup steps") + parser.add_option("--skip-conncheck", dest="skip_conncheck", action="store_true", + default=False, help="skip connection check to remote master") parser.add_option("-U", "--unattended", dest="unattended", action="store_true", default=False, help="unattended installation never prompts the user") @@ -382,6 +387,8 @@ def main(): try: top_dir, dir = expand_info(filename, dirman_password) + global REPLICA_INFO_TOP_DIR + REPLICA_INFO_TOP_DIR = top_dir except Exception, e: print "ERROR: Failed to decrypt or open the replica file." print "Verify you entered the correct Directory Manager password." @@ -402,6 +409,32 @@ def main(): sys.exit(0) config.dir = dir + + # check connection + if not options.skip_conncheck: + print "Run connection check to master" + args = ["/usr/sbin/ipa-replica-conncheck", "--master", config.master_host_name, + "--auto-master-check", "--realm", config.realm_name, + "--principal", "admin", + "--hostname", config.host_name] + + if options.admin_password: + args.extend(["--password", options.admin_password]) + + cafile = config.dir + "/cacert.p12" + if ipautil.file_exists(cafile): # with CA + args.append('--check-ca') + logging.debug("Running ipa-replica-conncheck with following arguments: %s" % + " ".join(args)) + (stdin, stderr, returncode) = ipautil.run(args,raiseonerr=False, capture_output=False) + + if returncode != 0: + sys.exit("Connection check failed!" + + "\nPlease fix your network settings according to error messages above." + + "\nIf the check results are not valid it can be skipped with --skip-conncheck parameter.") + else: + print "Connection check OK" + # Create the management framework config file # Note: We must do this before bootstraping and finalizing ipalib.api fd = open("/etc/ipa/default.conf", "w") @@ -549,6 +582,13 @@ except Exception, e: logging.debug(message) except KeyboardInterrupt: print "Installation cancelled." +finally: + # always try to remove decrypted replica file + try: + if REPLICA_INFO_TOP_DIR: + shutil.rmtree(REPLICA_INFO_TOP_DIR) + except OSError: + pass print "" print "Your system may be partly configured." diff --git a/install/tools/man/ipa-replica-install.1 b/install/tools/man/ipa-replica-install.1 index 3ee304224bb6db249b74cca736e95bba2c4356af..8889235462ef52327c951714ab62982b5003c8cc 100644 --- a/install/tools/man/ipa-replica-install.1 +++ b/install/tools/man/ipa-replica-install.1 @@ -36,6 +36,9 @@ Enable debug logging when more verbose output is needed \fB\-p\fR, \fB\-\-password\fR=\fIDM_PASSWORD\fR Directory Manager (existing master) password .TP +\fB\-w\fR \fIADMIN_PASSWORD\fR, \fB\-\-admin\-password\fR=\fIADMIN_PASSWORD\fR +Admin user Kerberos password used for connection check +.TP \fB\-\-setup\-dns\fR Generate a DNS zone if it does not exist already and configure the DNS server. This option requires that you either specify at least one DNS forwarder through @@ -58,6 +61,9 @@ Do not use DNS for hostname lookup during installation \fB\-\-no\-pkinit\fR Disables pkinit setup steps .TP +\fB\-\-skip\-conncheck\fR +Skip connection check to remote master +.TP \fB\-U\fR, \fB\-\-unattended\fR An unattended installation that will never prompt for user input .SH "EXIT STATUS" diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index 4280cd9f4f8ff6a98f5ff9808d8a9c6cfe8df75b..d521c27fd6f2ccd69882641c4b1222f272b9c8ed 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -32,6 +32,7 @@ import copy import stat import shutil import urllib2 +import socket from ipapython import ipavalidate from types import * @@ -1015,3 +1016,75 @@ def chkconfig_add(service_name): def chkconfig_del(service_name): run(["/sbin/chkconfig", "--del", service_name]) +def host_port_open(host, port, socket_stream=True, socket_timeout=None): + families = (socket.AF_INET, socket.AF_INET6) + success = False + + if socket_stream: + socket_type = socket.SOCK_STREAM + else: + socket_type = socket.SOCK_DGRAM + + for family in families: + try: + try: + s = socket.socket(family, socket_type) + except socket.error: + continue + + if socket_timeout is not None: + s.settimeout(socket_timeout) + + s.connect((host, port)) + success = True + except socket.error, e: + pass + finally: + s.close() + + if success: + return True + + return False + +def bind_port_responder(port, socket_stream=True, socket_timeout=None, responder_data=None): + families = (socket.AF_INET, socket.AF_INET6) + + if socket_stream: + socket_type = socket.SOCK_STREAM + else: + socket_type = socket.SOCK_DGRAM + + host = '' # all available interfaces + + for family in families: + try: + s = socket.socket(family, socket_type) + except socket.error, e: + if family == families[-1]: # last available family + raise e + + if socket_timeout is not None: + s.settimeout(socket_timeout) + + if socket_stream: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + try: + s.bind((host, port)) + + if socket_stream: + s.listen(1) + connection, client_address = s.accept() + try: + if responder_data: + connection.sendall(responder_data) #pylint: disable=E1101 + finally: + connection.close() + else: + data, addr = s.recvfrom( 512 ) # buffer size is 1024 bytes + + if responder_data: + s.sendto(responder_data, addr) + finally: + s.close() -- 1.7.5.1
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel