From 12cafe49be52deca889c6a909f6451a464085b06 Mon Sep 17 00:00:00 2001
From: David Kupka <dkupka@redhat.com>
Date: Sun, 4 Jan 2015 15:04:18 -0500
Subject: [PATCH] client: Add support for multiple IP addresses during
installation.
https://fedorahosted.org/freeipa/ticket/4249
Reviewed-By: Martin Basti <mbasti@redhat.com>
---
ipa-client/ipa-install/ipa-client-install | 289 +++++++++++++++++++++++-------
1 file changed, 223 insertions(+), 66 deletions(-)
diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index 91323ae115a27d221bcbc43fee887c56d99c8635..793de4fc950ad73b1d88f9ab4bd5178afc8b813d 100755
--- a/ipa-client/ipa-install/ipa-client-install
+++ b/ipa-client/ipa-install/ipa-client-install
@@ -32,6 +32,7 @@ try:
from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError
import shutil
from krbV import Krb5Error
+ import dns
import nss.nss as nss
import SSSDConfig
@@ -180,9 +181,15 @@ def parse_options():
basic_group.add_option("--configure-firefox", dest="configure_firefox",
action="store_true", default=False,
help="configure Firefox")
- parser.add_option_group(basic_group)
basic_group.add_option("--firefox-dir", dest="firefox_dir", default=None,
help="specify directory where Firefox is installed (for example: '/usr/lib/firefox')")
+ basic_group.add_option("--ip-address", dest="ip_addresses", default=[],
+ action="append", help="Specify IP address that should be added to DNS."
+ " This option can be used multiple times")
+ basic_group.add_option("--all-ip-addresses", dest="all_ip_addresses",
+ default=False, action="store_true", help="All routable IP"
+ " addresses configured on any inteface will be added to DNS")
+ parser.add_option_group(basic_group)
sssd_group = OptionGroup(parser, "SSSD options")
sssd_group.add_option("--permit", dest="permit",
@@ -223,6 +230,15 @@ def parse_options():
if options.no_nisdomain and options.nisdomain:
parser.error("--no-nisdomain cannot be used together with --nisdomain")
+ if options.ip_addresses:
+ if options.dns_updates:
+ parser.error("--ip-address cannot be used together with"
+ " --enable-dns-updates")
+
+ if options.all_ip_addresses:
+ parser.error("--ip-address cannot be used together with"
+ " --all-ip-addresses")
+
return safe_opts, options
def logging_setup(options):
@@ -1285,6 +1301,11 @@ def configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, options, clie
if options.dns_updates:
domain.set_option('dyndns_update', True)
+ if options.all_ip_addresses:
+ domain.set_option('dyndns_iface', '*')
+ else:
+ iface = get_server_connection_interface(cli_server[0])
+ domain.set_option('dyndns_iface', iface)
if options.krb5_offline_passwords:
domain.set_option('krb5_store_password_if_offline', True)
@@ -1501,39 +1522,41 @@ def unconfigure_nisdomain():
services.knownservices.domainname.disable()
-def resolve_ipaddress(server):
- """ Connect to the server's LDAP port in order to determine what ip
- address this machine uses as "public" ip (relative to the server).
-
- Returns a tuple with the IP address and address family when
- connection was successful. Socket error is raised otherwise.
- """
- last_socket_error = None
-
- for res in socket.getaddrinfo(server, 389, socket.AF_UNSPEC,
- socket.SOCK_STREAM):
- af, socktype, proto, canonname, sa = res
- try:
- s = socket.socket(af, socktype, proto)
- except socket.error, e:
- last_socket_error = e
- s = None
+def get_iface_from_ip(ip_addr):
+ ipresult = ipautil.run([paths.IP, '-oneline', 'address', 'show'])
+ for line in ipresult[0].split('\n'):
+ fields = line.split()
+ if len(fields) < 6:
continue
-
+ if fields[2] not in ['inet', 'inet6']:
+ continue
+ (ip, mask) = fields[3].rsplit('/', 1)
+ if ip == ip_addr:
+ return fields[1]
+ else:
+ raise RuntimeError("IP %s not assigned to any interface." % ip_addr)
+
+
+def get_local_ipaddresses(iface=None):
+ args = [paths.IP, '-oneline', 'address', 'show']
+ if iface:
+ args += ['dev', iface]
+ ipresult = ipautil.run(args)
+ lines = ipresult[0].split('\n')
+ ips = []
+ for line in lines:
+ fields = line.split()
+ if len(fields) < 6:
+ continue
+ if fields[2] not in ['inet', 'inet6']:
+ continue
+ (ip, mask) = fields[3].rsplit('/', 1)
try:
- s.connect(sa)
- sockname = s.getsockname()
-
- # For both IPv4 and IPv6 own IP address is always the first item
- return (sockname[0], af)
- except socket.error, e:
- last_socket_error = e
- finally:
- if s:
- s.close()
+ ips.append(ipautil.CheckedIPAddress(ip))
+ except ValueError:
+ continue
+ return ips
- if last_socket_error is not None:
- raise last_socket_error # pylint: disable=E0702
def do_nsupdate(update_txt):
root_logger.debug("Writing nsupdate commands to %s:", UPDATE_FILE)
@@ -1558,21 +1581,24 @@ def do_nsupdate(update_txt):
return result
-UPDATE_TEMPLATE_A = """
-debug
+DELETE_TEMPLATE_A = """
update delete $HOSTNAME. IN A
show
send
-update add $HOSTNAME. $TTL IN A $IPADDRESS
-show
-send
"""
-UPDATE_TEMPLATE_AAAA = """
-debug
+DELETE_TEMPLATE_AAAA = """
update delete $HOSTNAME. IN AAAA
show
send
+"""
+ADD_TEMPLATE_A = """
+update add $HOSTNAME. $TTL IN A $IPADDRESS
+show
+send
+"""
+
+ADD_TEMPLATE_AAAA = """
update add $HOSTNAME. $TTL IN AAAA $IPADDRESS
show
send
@@ -1581,46 +1607,174 @@ send
UPDATE_FILE = paths.IPA_DNS_UPDATE_TXT
CCACHE_FILE = paths.IPA_DNS_CCACHE
-def update_dns(server, hostname):
+def update_dns(server, hostname, options):
try:
- (ip, af) = resolve_ipaddress(server)
- except socket.gaierror, e:
- root_logger.debug("update_dns: could not connect to server: %s", e)
- root_logger.error("Cannot update DNS records! "
- "Failed to connect to server '%s'.", server)
- return
-
- sub_dict = dict(HOSTNAME=hostname,
- IPADDRESS=ip,
- TTL=1200
- )
-
- if af == socket.AF_INET:
- template = UPDATE_TEMPLATE_A
- elif af == socket.AF_INET6:
- template = UPDATE_TEMPLATE_AAAA
+ ips = get_local_ipaddresses()
+ except CalledProcessError as e:
+ root_logger.error("Cannot update DNS records. %s" % e)
+ root_logger.debug("Unable to get local IP addresses.")
+
+ if options.all_ip_addresses:
+ update_ips = ips
+ elif options.ip_addresses:
+ update_ips = []
+ for ip in options.ip_addresses:
+ update_ips.append(ipautil.CheckedIPAddress(ip))
else:
- root_logger.info("Failed to determine this machine's ip address.")
- root_logger.warning("Failed to update DNS A record.")
+ try:
+ iface = get_server_connection_interface(server)
+ except RuntimeError as e:
+ root_logger.error("Cannot update DNS records. %s" % e)
+ return
+ try:
+ update_ips = get_local_ipaddresses(iface)
+ except CalledProcessError as e:
+ root_logger.error("Cannot update DNS records. %s" % e)
+ return
+
+ if not update_ips:
+ root_logger.info("Failed to determine this machine's ip address(es).")
return
- update_txt = ipautil.template_str(template, sub_dict)
+ update_txt = "debug\n"
+ update_txt += ipautil.template_str(DELETE_TEMPLATE_A,
+ dict(HOSTNAME=hostname))
+ update_txt += ipautil.template_str(DELETE_TEMPLATE_AAAA,
+ dict(HOSTNAME=hostname))
+
+ for ip in update_ips:
+ sub_dict = dict(HOSTNAME=hostname, IPADDRESS=ip, TTL=1200)
+ if ip.version == 4:
+ template = ADD_TEMPLATE_A
+ elif ip.version == 6:
+ template = ADD_TEMPLATE_AAAA
+ update_txt += ipautil.template_str(template, sub_dict)
+
+ if not do_nsupdate(update_txt):
+ root_logger.error("Failed to update DNS records.")
+ verify_dns_update(hostname, update_ips)
+
- if do_nsupdate(update_txt):
- root_logger.info("DNS server record set to: %s -> %s", hostname, ip)
+def verify_dns_update(fqdn, ips):
+ """
+ Verify that the fqdn resolves to all IP addresses and
+ that there's matching PTR record for every IP address.
+ """
+ # verify A/AAAA records
+ missing_ips = [str(ip) for ip in ips]
+ extra_ips = []
+ for record_type in [dns.rdatatype.A, dns.rdatatype.AAAA]:
+ root_logger.debug('DNS resolver: Query: %s IN %s' %
+ (fqdn, dns.rdatatype.to_text(record_type)))
+ try:
+ answers = dns.resolver.query(fqdn, record_type)
+ except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
+ root_logger.debug('DNS resolver: No record.')
+ except dns.resolver.NoNameservers:
+ root_logger.debug('DNS resolver: No nameservers answered the'
+ 'query.')
+ except dns.exception.DNSException:
+ root_logger.debug('DNS resolver error.')
+ else:
+ for rdata in answers:
+ try:
+ missing_ips.remove(rdata.address)
+ except ValueError:
+ extra_ips.append(rdata.address)
+
+ # verify PTR records
+ fqdn_name = dns.name.from_text(fqdn)
+ wrong_reverse = {}
+ missing_reverse = [str(ip) for ip in ips]
+ for ip in ips:
+ ip_str = str(ip)
+ addr = dns.reversename.from_address(ip_str)
+ root_logger.debug('DNS resolver: Query: %s IN PTR' % addr)
+ try:
+ answers = dns.resolver.query(addr, dns.rdatatype.PTR)
+ except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
+ root_logger.debug('DNS resolver: No record.')
+ except dns.resolver.NoNameservers:
+ root_logger.debug('DNS resolver: No nameservers answered the'
+ 'query.')
+ except dns.exception.DNSException:
+ root_logger.debug('DNS resolver error.')
+ else:
+ missing_reverse.remove(ip_str)
+ for rdata in answers:
+ if not rdata.target == fqdn_name:
+ wrong_reverse.setdefault(ip_str, []).append(rdata.target)
+
+ if missing_ips:
+ root_logger.warning('Missing A/AAAA record(s) for host %s: %s.' %
+ (fqdn, ', '.join(missing_ips)))
+ if extra_ips:
+ root_logger.warning('Extra A/AAAA record(s) for host %s: %s.' %
+ (fqdn, ', '.join(extra_ips)))
+ if missing_reverse:
+ root_logger.warning('Missing reverse record(s) for address(es): %s.' %
+ ', '.join(missing_reverse))
+ if wrong_reverse:
+ root_logger.warning('Incorrect reverse record(s):')
+ for ip in wrong_reverse:
+ for target in wrong_reverse[ip]:
+ root_logger.warning('%s is pointing to %s instead of %s' %
+ (ip, target, fqdn_name))
+
+def get_server_connection_interface(server):
+ # connect to IPA server, get all ip addresses of inteface used to connect
+ for res in socket.getaddrinfo(server, 389, socket.AF_UNSPEC, socket.SOCK_STREAM):
+ (af, socktype, proto, canonname, sa) = res
+ try:
+ s = socket.socket(af, socktype, proto)
+ except socket.error as e:
+ last_error = e
+ s = None
+ continue
+ try:
+ s.connect(sa)
+ sockname = s.getsockname()
+ ip = sockname[0]
+ except socket.error as e:
+ last_error = e
+ continue
+ finally:
+ if s:
+ s.close()
+ try:
+ return get_iface_from_ip(ip)
+ except (CalledProcessError, RuntimeError) as e:
+ last_error = e
else:
- root_logger.error("Failed to update DNS records.")
+ msg = "Cannot get server connection interface"
+ if last_error:
+ msg += ": %s" % (last_error)
+ raise RuntimeError(msg)
-def client_dns(server, hostname, dns_updates=False):
+
+def client_dns(server, hostname, options):
dns_ok = ipautil.is_host_resolvable(hostname)
if not dns_ok:
- root_logger.warning("Hostname (%s) not found in DNS", hostname)
+ root_logger.warning("Hostname (%s) does not have A/AAAA record.",
+ hostname)
+
+ if (options.dns_updates or options.all_ip_addresses or options.ip_addresses
+ or not dns_ok):
+ update_dns(server, hostname, options)
- if dns_updates or not dns_ok:
- update_dns(server, hostname)
+
+def check_ip_addresses(options):
+ if options.ip_addresses:
+ for ip in options.ip_addresses:
+ try:
+ ipautil.CheckedIPAddress(ip, match_local=True)
+ except ValueError as e:
+ root_logger.error(e)
+ return False
+ return True
def update_ssh_keys(server, hostname, ssh_dir, create_sshfp):
if not os.path.isdir(ssh_dir):
@@ -2127,6 +2281,9 @@ def install(options, env, fstore, statestore):
if not options.ca_cert_file and get_cert_path(options.ca_cert_file) == CACERT:
root_logger.warning("Using existing certificate '%s'.", CACERT)
+ if not check_ip_addresses(options):
+ return CLIENT_INSTALL_ERROR
+
# Create the discovery instance
ds = ipadiscovery.IPADiscovery()
@@ -2717,7 +2874,7 @@ def install(options, env, fstore, statestore):
root_logger.info("Added CA certificates to the default NSS database.")
if not options.on_master:
- client_dns(cli_server[0], hostname, options.dns_updates)
+ client_dns(cli_server[0], hostname, options)
configure_certmonger(fstore, subject_base, cli_realm, hostname,
options, ca_enabled)
--
2.4.3