On 31/07/15 18:31, Martin Basti wrote:
On 28/07/15 09:52, David Kupka wrote:
On 27/07/15 16:45, David Kupka wrote:
On 15/01/15 17:13, David Kupka wrote:
On 01/15/2015 03:22 PM, David Kupka wrote:
On 01/15/2015 12:43 PM, David Kupka wrote:
On 01/12/2015 06:34 PM, Martin Basti wrote:
On 09/01/15 14:43, David Kupka wrote:
On 01/07/2015 04:15 PM, Martin Basti wrote:
On 07/01/15 12:27, David Kupka wrote:
https://fedorahosted.org/freeipa/ticket/4249

Thank you for patch:

1)
-        root_logger.error("Cannot update DNS records! "
-                          "Failed to connect to server '%s'.",
server)
+        ips = get_local_ipaddresses()
+    except CalledProcessError as e:
+        root_logger.error("Cannot update DNS records. %s" % e)

IMO the error message should be more specific,  add there
something
like
"Unable to get local IP addresses". at least in log.debug()

2)
+    lines = ipresult[0].replace('\\', '').split('\n')

.replace() is not needed

3)
+    if len(ips) == 0:

if not ips:

is more pythonic by PEP8


Thanks for catching these. Updated patch attached.

merciful NACK

Thank you for the patch, unfortunately I hit one issue which needs
to be
resolved.

If "sync PTR" is activated in zone settings, and reverse zone
doesn't
exists, nsupdate/BIND returns SERVFAIL and ipa-client-install print
Error message, 'DNS update failed'. In fact, all A/AAAA records was
succesfully updated, only PTR records failed.

Bind log:
named-pkcs11[28652]: updating zone 'example.com/IN': adding an RR at
'vm-101.example.com' AAAA

named-pkcs11[28652]: PTR record synchronization (addition) for
A/AAAA
'vm-101.example.com.' refused: unable to find active reverse zone
for IP
address '2620:52:0:104c:21a:4aff:fe10:4eaa': not found

With IPv6 we have several addresses from different reverse zones and
this situation may happen often.
I suggest following:
1) Print list of addresses which will be updated. (Now if update
fails,
user needs to read log, which addresses installer tried to update)
2) Split nsupdates per A/AAAA record.
3a) If failed, check with DNS query if A/AAAA and PTR record are
there
and print proper error message
3b) Just print A/AAAA (or PTR) record may not be updated for
particular
IP address.

Any other suggestions are welcome.


After long discussion with DNS and UX guru I've implemented it this
way:
1. Call nsupdate only once with all updates.
2. Verify that the expected records are resolvable.
3. If no print list of missing A/AAAA, list of missing PTR records
and
list to mismatched PTR record.

As this is running inside client we can't much more and it's up to
user
to check what's rotten in his DNS setup.

Updated patch attached.


_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel



One more change to behave well in -crazy- exotic environments that
resolves more PTR records for single IP.



_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel


Yet another change to make language nerds and our UX guru happy :-)


_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel


Rebased patch attached.


Updated patch attached.

Just for record this patch is for dualstack/IPv6 support.
IMO this ticket also requires to fix ipa-join to support IPv6.

I still have doubts to have multihomed support as default, this may be
unexpected change of ipa-client-install behavior.
I know, is hard to detect which addresses user want to register in IPA
without crystal ball, but it should not be impossible :-) .

I propose following solution:

To add new options:
--multihomed or --all-ip-address - all IP addresses from client will be
used
--ip-address  - adress which will be registered on (IPA) DNS server
--ip-address-interface - interface from which address will be registered


0) without any option specified, current behavior will be used + IPv6
* detect which address is used to communicate with IPA server
* detect interface where this address belongs
* use ipv4 and all ipv6 addresses of this interface
* if --enable-dns-updates=true: configure SSSD as is configured now:
automatically detect which address is used + patched SSSD will also
updates proper IPv6 address

1) --multihomed or --all-ip-addresses (this is multihomed ticket)
* all adresses will be used
* if --enable-dns-updates=true: SSSD will be configured to send all
ip_addresses

2) --ip-address option specified:
* only specified addresses will be used (+ check if this addresses exist
locally)
* if --enable-dns-updates=true: ERROR dynamic updates may change this
address (user should choose static vs dynamic)

3) --ip-address-interface option specified:
* only addresses from specified interfaces will be used
* if --enable-dns-updates=true: SSSD will be configured to use these
interfaces to get addresses that will be dynamically updated on dns

Modification of current patch should not be hard, we already have almost
everything implemented:
* method get_local_addresses should return dict {interface:[list of
addresses]}, this can be used in all of 4 cases.
* restore original function to detect IP address used to communicate
with IPA server

I insist on 0) and 1), others may be stretch goal (easy to implement)
(It would be shame to not implemented multihomed support together with
this ticket, as it requires max 5 extra lines of code)

Seems my proposal reasonable?

What is you opinion Martin? Should we just use all addresses to be
registered, or try to keep old behavior as much as possible?

Martin^2


0-2 implemented, IMO there is no real use-case for 3. It can be added later when/if there is need.
Updated patch (+ rebase for ipa-4-2 branch) attached.

--
David Kupka
From f5ceda9960810d14af9d10c7b23edbe40965750f Mon Sep 17 00:00:00 2001
From: David Kupka <dku...@redhat.com>
Date: Tue, 18 Aug 2015 19:45:23 +0200
Subject: [PATCH] client: Add support for multiple IP addresses during
 installation.

https://fedorahosted.org/freeipa/ticket/4249
---
 ipa-client/ipa-install/ipa-client-install | 283 +++++++++++++++++++++++-------
 1 file changed, 220 insertions(+), 63 deletions(-)

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index e0f53559e8405640866e084d2aaece7c971d8bc7..4c199f8a8401a3d2a9333b45fc4ae18481e73135 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 could be specified multiple times")
+    basic_group.add_option("--all-ip-addresses", dest="all_ip_addresses",
+        default=False, action="store_true", help="All configured 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).
+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)
 
-        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 as e:
-            last_socket_error = e
-            s = None
+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 as 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 as 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
-                )
+        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 af == socket.AF_INET:
-        template = UPDATE_TEMPLATE_A
-    elif af == socket.AF_INET6:
-        template = UPDATE_TEMPLATE_AAAA
+    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))
 
-    if do_nsupdate(update_txt):
-        root_logger.info("DNS server record set to: %s -> %s", hostname, ip)
-    else:
+    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)
 
-def client_dns(server, hostname, dns_updates=False):
+
+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:
+        msg = "Cannot get server connection interface"
+        if last_error:
+            msg += ": %s" % (last_error)
+        raise RuntimeError(msg)
+
+
+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 dns_updates or not dns_ok:
-        update_dns(server, hostname)
+    if (options.dns_updates or options.all_ip_addresses or options.ip_addresses
+            or not dns_ok):
+        update_dns(server, hostname, options)
+
+
+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()
 
@@ -2736,7 +2893,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

From f917acd8df6e91c6131db7c13e45040f02445864 Mon Sep 17 00:00:00 2001
From: David Kupka <dku...@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
---
 ipa-client/ipa-install/ipa-client-install | 283 +++++++++++++++++++++++-------
 1 file changed, 220 insertions(+), 63 deletions(-)

diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install
index 17cd2b6879aeb0c4b80dae9fe4218c0a741c0990..978bc5f713df53f160a15f47a636a593dbf7da8a 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 could be specified multiple times")
+    basic_group.add_option("--all-ip-addresses", dest="all_ip_addresses",
+        default=False, action="store_true", help="All configured 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).
+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)
 
-        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_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
-                )
+        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 af == socket.AF_INET:
-        template = UPDATE_TEMPLATE_A
-    elif af == socket.AF_INET6:
-        template = UPDATE_TEMPLATE_AAAA
+    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))
 
-    if do_nsupdate(update_txt):
-        root_logger.info("DNS server record set to: %s -> %s", hostname, ip)
-    else:
+    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)
 
-def client_dns(server, hostname, dns_updates=False):
+
+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:
+        msg = "Cannot get server connection interface"
+        if last_error:
+            msg += ": %s" % (last_error)
+        raise RuntimeError(msg)
+
+
+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 dns_updates or not dns_ok:
-        update_dns(server, hostname)
+    if (options.dns_updates or options.all_ip_addresses or options.ip_addresses
+            or not dns_ok):
+        update_dns(server, hostname, options)
+
+
+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

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to