On 20.5.2011 20:27, Jan Cholasta wrote:
On 10.5.2011 20:06, Jan Cholasta wrote:
Parse netmasks in IP addresses passed to server install.

ticket 1212

Patch updated.

TODO: Write unit test for ipapython.ipautil.CheckedIPAddress
TODO: Clean unreachable code paths off of ipa-server-install (?)
TODO: Workarounds for netaddr bugs (?)



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

Fixed ipa-replica-prepare and added a unit test.

--
Jan Cholasta
>From 412dee8f9980523376d411ef166c73455a87e22b Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 24 May 2011 15:27:46 +0200
Subject: [PATCH] Parse netmasks in IP addresses passed to server install.

ticket 1212
---
 freeipa.spec.in                      |    1 +
 install/tools/ipa-dns-install        |    9 +++--
 install/tools/ipa-replica-install    |    4 ++-
 install/tools/ipa-replica-prepare    |   15 ++++----
 install/tools/ipa-server-install     |   36 +++++++++----------
 ipapython/config.py                  |   13 ++++++-
 ipapython/ipautil.py                 |   63 ++++++++++++++++++++++++++++++++++
 ipaserver/install/installutils.py    |   39 ++++++++++-----------
 tests/test_ipapython/__init__.py     |   22 ++++++++++++
 tests/test_ipapython/test_ipautil.py |   56 ++++++++++++++++++++++++++++++
 10 files changed, 206 insertions(+), 52 deletions(-)
 create mode 100644 tests/test_ipapython/__init__.py
 create mode 100644 tests/test_ipapython/test_ipautil.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index b936616..fba2f31 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -188,6 +188,7 @@ Requires: python-kerberos >= 1.1-3
 %endif
 Requires: authconfig
 Requires: gnupg
+Requires: iproute
 Requires: pyOpenSSL
 Requires: python-nss >= 0.11
 Requires: python-lxml
diff --git a/install/tools/ipa-dns-install b/install/tools/ipa-dns-install
index aac85bf..491585b 100755
--- a/install/tools/ipa-dns-install
+++ b/install/tools/ipa-dns-install
@@ -37,9 +37,10 @@ def parse_options():
                       sensitive=True, help="admin password")
     parser.add_option("-d", "--debug", dest="debug", action="store_true",
                       default=False, help="print debugging information")
-    parser.add_option("--ip-address", dest="ip_address", help="Master Server IP Address")
+    parser.add_option("--ip-address", dest="ip_address",
+                      type="ipnet", help="Master Server IP Address")
     parser.add_option("--forwarder", dest="forwarders", action="append",
-                      help="Add a DNS forwarder")
+                      type="ipaddr", help="Add a DNS forwarder")
     parser.add_option("--no-forwarders", dest="no_forwarders", action="store_true",
                       default=False, help="Do not add any DNS forwarders, use root servers instead")
     parser.add_option("--no-reverse", dest="no_reverse",
@@ -130,12 +131,14 @@ def main():
     if options.ip_address:
         ip_address = options.ip_address
     else:
-        ip_address = resolve_host(api.env.host)
+        hostaddr = resolve_host(api.env.host)
+        ip_address = hostaddr and ipautil.CheckedIPAddress(hostaddr)
     if not ip_address or not verify_ip_address(ip_address):
         if options.unattended:
             sys.exit("Unable to resolve IP address for host name")
         else:
             ip_address = read_ip_address(api.env.host, fstore)
+    ip_address = str(ip_address)
     logging.debug("will use ip_address: %s\n", ip_address)
 
     if options.no_forwarders:
diff --git a/install/tools/ipa-replica-install b/install/tools/ipa-replica-install
index 49df7fe..2b7c8ca 100755
--- a/install/tools/ipa-replica-install
+++ b/install/tools/ipa-replica-install
@@ -63,7 +63,7 @@ def parse_options():
     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",
-                      help="Add a DNS forwarder")
+                      type="ipaddr", help="Add a DNS forwarder")
     parser.add_option("--no-forwarders", dest="no_forwarders", action="store_true",
                       default=False, help="Do not add any DNS forwarders, use root servers instead")
     parser.add_option("--no-reverse", dest="no_reverse", action="store_true",
@@ -285,6 +285,8 @@ def install_bind(config, options):
     ip_address = resolve_host(config.host_name)
     if not ip_address:
         sys.exit("Unable to resolve IP address for host name")
+    ip = installutils.parse_ip_address(ip_address)
+    ip_address = str(ip)
 
     create_reverse = True
     if options.unattended:
diff --git a/install/tools/ipa-replica-prepare b/install/tools/ipa-replica-prepare
index e912235..759f588 100755
--- a/install/tools/ipa-replica-prepare
+++ b/install/tools/ipa-replica-prepare
@@ -24,7 +24,6 @@ import logging, tempfile, shutil, os, pwd
 import traceback
 from ConfigParser import SafeConfigParser
 import krbV
-from optparse import OptionParser
 
 from ipapython import ipautil
 from ipaserver.install import bindinstance, dsinstance, installutils, certs
@@ -32,11 +31,12 @@ from ipaserver.install.bindinstance import add_zone, add_reverse_zone, add_rr, a
 from ipaserver.install.replication import check_replication_plugin, enable_replication_version_checking
 from ipaserver.plugins.ldap2 import ldap2
 from ipapython import version
+from ipapython.config import IPAOptionParser
 from ipalib import api, errors, util
 
 def parse_options():
     usage = "%prog [options] FQDN (e.g. replica.example.com)"
-    parser = OptionParser(usage=usage, version=version.VERSION)
+    parser = IPAOptionParser(usage=usage, version=version.VERSION)
 
     parser.add_option("--dirsrv_pkcs12", dest="dirsrv_pkcs12",
                       help="install certificate for the directory server")
@@ -53,7 +53,7 @@ def parse_options():
     parser.add_option("-p", "--password", dest="password", 
                       help="Directory Manager (existing master) password")
     parser.add_option("--ip-address", dest="ip_address",
-                      help="Add A and PTR records of the future replica")
+                      type="ipnet", help="Add A and PTR records of the future replica")
     parser.add_option("--ca", dest="ca_file", default="/root/cacert.p12",
                       help="Location of CA PKCS#12 file, default /root/cacert.p12")
     parser.add_option("--no-pkinit", dest="setup_pkinit", action="store_false",
@@ -425,10 +425,11 @@ def main():
         name = domain.pop(0)
         domain = ".".join(domain)
 
-        zone = add_zone(domain, nsaddr=options.ip_address)
-        add_rr(zone, name, "A", options.ip_address)
-        add_reverse_zone(options.ip_address)
-        add_ptr_rr(options.ip_address, replica_fqdn)
+        ip_address = str(options.ip_address)
+        zone = add_zone(domain, nsaddr=ip_address)
+        add_rr(zone, name, "A", ip_address)
+        add_reverse_zone(ip_address)
+        add_ptr_rr(ip_address, replica_fqdn)
 
 try:
     if not os.geteuid()==0:
diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index 00b1334..4a93802 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -100,11 +100,12 @@ def parse_options():
     parser.add_option("", "--external_ca_file", dest="external_ca_file",
                       help="File containing PKCS#10 of the external CA chain")
     parser.add_option("--hostname", dest="host_name", help="fully qualified name of server")
-    parser.add_option("--ip-address", dest="ip_address", help="Master Server IP Address")
+    parser.add_option("--ip-address", dest="ip_address",
+                      type="ipnet", help="Master Server IP Address")
     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",
-                      help="Add a DNS forwarder")
+                      type="ipaddr", help="Add a DNS forwarder")
     parser.add_option("--no-forwarders", dest="no_forwarders", action="store_true",
                       default=False, help="Do not add any DNS forwarders, use root servers instead")
     parser.add_option("--no-reverse", dest="no_reverse", action="store_true",
@@ -615,37 +616,34 @@ def main():
     domain_name = domain_name.lower()
 
     # Check we have a public IP that is associated with the hostname
-    ip = resolve_host(host_name)
-    if ip is None:
-        if options.ip_address:
-            ip = options.ip_address
+    hostaddr = resolve_host(host_name)
+    if hostaddr is not None:
+        ip = CheckedIPAddress(hostaddr)
+    else:
+        ip = options.ip_address
     if ip is None and options.unattended:
         sys.exit("Unable to resolve IP address for host name")
 
     if not verify_ip_address(ip):
-        ip = ""
+        ip = None
         if options.unattended:
             sys.exit(1)
 
-    if options.ip_address and options.ip_address != ip:
-        if options.setup_dns:
-            if not verify_ip_address(options.ip_address):
-                return 1
-            ip = options.ip_address
-        else:
+    if options.ip_address:
+        if options.ip_address != ip and not options.setup_dns:
             print >>sys.stderr, "Error: the hostname resolves to an IP address that is different"
             print >>sys.stderr, "from the one provided on the command line.  Please fix your DNS"
             print >>sys.stderr, "or /etc/hosts file and restart the installation."
             return 1
 
-    if options.unattended:
-        if not ip:
-            sys.exit("Unable to resolve IP address")
+        ip = options.ip_address
+        if not verify_ip_address(ip):
+            return 1
 
-    if not ip:
+    if ip is None:
         ip = read_ip_address(host_name, fstore)
-        logging.debug("read ip_address: %s\n" % ip)
-    ip_address = ip
+        logging.debug("read ip_address: %s\n" % str(ip))
+    ip_address = str(ip)
 
     print "The IPA Master Server will be configured with"
     print "Hostname:    " + host_name
diff --git a/ipapython/config.py b/ipapython/config.py
index 7e5b195..c785085 100644
--- a/ipapython/config.py
+++ b/ipapython/config.py
@@ -18,7 +18,8 @@
 #
 
 import ConfigParser
-from optparse import Option, Values, OptionParser, IndentedHelpFormatter
+from optparse import Option, Values, OptionParser, IndentedHelpFormatter, OptionValueError
+from copy import copy
 
 import socket
 import ipapython.dnsclient
@@ -46,12 +47,22 @@ class IPAFormatter(IndentedHelpFormatter):
             ret += "%s %s\n" % (spacing, line)
         return ret
 
+def check_ip_option(option, opt, value):
+    from ipapython.ipautil import CheckedIPAddress
+    try:
+        return CheckedIPAddress(value, parse_netmask=(option.type == "ipnet"))
+    except Exception as e:
+        raise OptionValueError("option %s: invalid IP address %s: %s" % (opt, value, e))
+
 class IPAOption(Option):
     """
     optparse.Option subclass with support of options labeled as
     security-sensitive such as passwords.
     """
     ATTRS = Option.ATTRS + ["sensitive"]
+    TYPES = Option.TYPES + ("ipaddr", "ipnet")
+    TYPE_CHECKER = copy(Option.TYPE_CHECKER)
+    TYPE_CHECKER["ipaddr"] = TYPE_CHECKER["ipnet"] = check_ip_option
 
 class IPAOptionParser(OptionParser):
     """
diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index 4280cd9..2ad9240 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -39,6 +39,7 @@ from types import *
 import re
 import xmlrpclib
 import datetime
+import netaddr
 from ipapython import config
 try:
     from subprocess import CalledProcessError
@@ -63,6 +64,68 @@ def get_domain_name():
 
     return domain_name
 
+class CheckedIPAddress(netaddr.IPAddress):
+    def __init__(self, addr, match_local=True, parse_netmask=True):
+        if isinstance(addr, CheckedIPAddress):
+            super(CheckedIPAddress, self).__init__(addr)
+            self.prefixlen = addr.prefixlen
+            self.interface = addr.interface
+            return
+
+        net = None
+        iface = None
+
+        if isinstance(addr, netaddr.IPNetwork):
+            net = addr
+            addr = net.ip
+        elif isinstance(addr, netaddr.IPAddress):
+            pass
+        else:
+            try:
+                addr = netaddr.IPAddress(addr)
+            except ValueError:
+                net = netaddr.IPNetwork(addr)
+                if not parse_netmask:
+                    raise ValueError("netmask and prefix length not allowed here")
+                addr = net.ip
+
+        if addr.version not in (4, 6):
+            raise ValueError("unsupported IP version")
+        if addr.is_loopback():
+            raise ValueError("cannot use loopback IP address")
+
+        if match_local:
+            if addr.version == 4:
+                family = 'inet'
+            elif addr.version == 6:
+                family = 'inet6'
+
+            ipresult = run(['/sbin/ip', '-family', family, '-oneline', 'address', 'show'])
+            lines = ipresult[0].split('\n')
+            for line in lines:
+                fields = line.split()
+                if len(fields) < 4:
+                    continue
+
+                ifnet = netaddr.IPNetwork(fields[3])
+                if ifnet == net or ifnet.ip == addr:
+                    net = ifnet
+                    iface = fields[1]
+                    break
+
+        if net is None:
+            if addr.version == 4:
+                net = netaddr.IPNetwork(netaddr.cidr_abbrev_to_verbose(str(addr)))
+            elif addr.version == 6:
+                net = netaddr.IPNetwork(str(addr) + '/64')
+
+        super(CheckedIPAddress, self).__init__(addr)
+        self.prefixlen = net.prefixlen
+        self.interface = iface
+
+    def is_local(self):
+        return self.interface is not None
+
 def realm_to_suffix(realm_name):
     s = realm_name.split(".")
     terms = ["dc=" + x.lower() for x in s]
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 3868c4d..480c350 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -148,17 +148,18 @@ def verify_fqdn(host_name,no_host_dns=False):
     else:
         print "Warning: Hostname (%s) not found in DNS" % host_name
 
-def verify_ip_address(ip):
-    is_ok = True
+def parse_ip_address(addr, match_local=True, parse_netmask=True):
     try:
-        socket.inet_pton(socket.AF_INET, ip)
-    except:
-        try:
-            socket.inet_pton(socket.AF_INET6, ip)
-        except:
-            print "Unable to verify IP address"
-            is_ok = False
-    return is_ok
+        ip = ipautil.CheckedIPAddress(addr, match_local=match_local, parse_netmask=parse_netmask)
+        if match_local and not ip.is_local():
+            print "Warning: No network interface matches IP address %s" % addr
+        return ip
+    except Exception as e:
+        print "Error: Invalid IP Address %s: %s" % (addr, e)
+        return None
+
+def verify_ip_address(addr, match_local=True, parse_netmask=True):
+    return parse_ip_address(addr, match_local, parse_netmask) is not None
 
 def record_in_hosts(ip, host_name, file="/etc/hosts"):
     hosts = open(file, 'r').readlines()
@@ -191,19 +192,17 @@ def add_record_to_hosts(ip, host_name, file="/etc/hosts"):
 def read_ip_address(host_name, fstore):
     while True:
         ip = ipautil.user_input("Please provide the IP address to be used for this host name", allow_empty = False)
+        ip_parsed = parse_ip_address(ip)
 
-        if ip == "127.0.0.1" or ip == "::1":
-            print "The IPA Server can't use localhost as a valid IP"
-            continue
-
-        if verify_ip_address(ip):
+        if ip_parsed is not None:
             break
 
+    ip = str(ip_parsed)
     print "Adding ["+ip+" "+host_name+"] to your /etc/hosts file"
     fstore.backup_file("/etc/hosts")
     add_record_to_hosts(ip, host_name)
 
-    return ip
+    return ip_parsed
 
 def read_dns_forwarders():
     addrs = []
@@ -215,15 +214,13 @@ def read_dns_forwarders():
                                     allow_empty=True)
             if not ip:
                 break
-            if ip == "127.0.0.1" or ip == "::1":
-                print "You cannot use localhost as a DNS forwarder"
-                continue
-            if not verify_ip_address(ip):
+            ip_parsed = parse_ip_address(ip, match_local=False, parse_netmask=False)
+            if ip_parsed is None:
                 print "DNS forwarder %s not added" % ip
                 continue
 
             print "DNS forwarder %s added" % ip
-            addrs.append(ip)
+            addrs.append(str(ip_parsed))
 
     if not addrs:
         print "No DNS forwarders configured"
diff --git a/tests/test_ipapython/__init__.py b/tests/test_ipapython/__init__.py
new file mode 100644
index 0000000..fa0e44b
--- /dev/null
+++ b/tests/test_ipapython/__init__.py
@@ -0,0 +1,22 @@
+# Authors:
+#   Jan Cholasta <jchol...@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/>.
+
+"""
+Sub-package containing unit tests for `ipapython` package.
+"""
diff --git a/tests/test_ipapython/test_ipautil.py b/tests/test_ipapython/test_ipautil.py
new file mode 100644
index 0000000..03f5f7b
--- /dev/null
+++ b/tests/test_ipapython/test_ipautil.py
@@ -0,0 +1,56 @@
+# Authors:
+#   Jan Cholasta <jchol...@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/>.
+"""
+Test the `ipapython/ipautil.py` module.
+"""
+
+import nose
+
+from ipapython import ipautil
+
+class CheckIPAddress:
+    def __init__(self, addr):
+        self.description = "Test IP address parsing and verification (%s)" % addr
+
+    def __call__(self, addr, words=None, prefixlen=None):
+        try:
+            ip = ipautil.CheckedIPAddress(addr, match_local=False)
+            assert ip.words == words and ip.prefixlen == prefixlen
+        except:
+            assert words is None and prefixlen is None
+
+def test_ip_address():
+    addrs = [
+        ('10.11.12.13',     (10, 11, 12, 13),   8),
+        ('10.11.12.13/14',  (10, 11, 12, 13),   14),
+        ('10.11.12.1337',),
+        ('10.11.12.13/33',),
+        ('127.0.0.1',),
+
+        ('2001::1',         (0x2001, 0, 0, 0, 0, 0, 0, 1), 64),
+        ('2001::1/72',      (0x2001, 0, 0, 0, 0, 0, 0, 1), 72),
+        ('2001::1beef',),
+        ('2001::1/129',),
+        ('::1',),
+
+        ('junk',)
+    ]
+
+    for addr in addrs:
+        yield (CheckIPAddress(addr[0]),) + addr
-- 
1.7.4.4

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

Reply via email to