Petr Viktorin wrote:
On 03/07/2013 08:27 PM, Rob Crittenden wrote:
Petr Viktorin wrote:
On 03/06/2013 09:52 PM, Rob Crittenden wrote:
Petr Viktorin wrote:
[...]
On new installs, the ACI on cn=Posix IDs,cn=Distributed Numeric
Assignment Plugin,cn=plugins,cn=config is added before the entry
itself.
I didn't test everything as I didn't get the access.


It shouldn't make a difference. What isn't working?

I get a CRITICAL message in the log:

add aci:
         (targetattr=dnaNextRange || dnaNextValue ||
dnaMaxValue)(version 3.0;acl "permission:Modify DNA Range";allow (write)
groupdn = "ldap:///cn=Modify DNA
Range,cn=permissions,cn=pbac,dc=idm,dc=lab,dc=eng,dc=brq,dc=redhat,dc=com";)



modifying entry "cn=Posix IDs,cn=Distributed Numeric Assignment
Plugin,cn=plugins,cn=config"

2013-03-07T11:01:48Z DEBUG stderr=ldap_initialize(
ldap://vm-081.idm.lab.eng.brq.redhat.com:389/??base )
ldap_modify: No such object (32)

2013-03-07T11:01:48Z CRITICAL Failed to load replica-acis.ldif: Command
'/usr/bin/ldapmodify -v -f /tmp/tmpT55upM -H
ldap://vm-081.idm.lab.eng.brq.redhat.com:389 -x -D cn=Directory Manager
-y /tmp/tmplFeere' returned non-zero exit status 32


Gotcha. I moved where the replica acis are loaded.

Please attach the patch :)

[...]
+        failed = 0
+        for ent in entries:


This loops more than necessary and is somewhat hard to follow.
Consider
using for-else here:

for ...:
     ...
     if okay:
         break
else:
     raise error

I simplified things a bit but a for/else won't work here as we need to
check all ranges all the time. It is perfectly fine to not fit into a
range, as long as it fits into SOME range.

Well, that's how for's (not if's) else clause works -- it's executed
after all the looping's done if you didn't `break` out.
http://docs.python.org/2/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops



Maybe I'm just used to it and it's too esoteric to the average reader,
though.

Thanks for the vote of confidence. Like I said, I wanted it to check all
the ranges. A for/else quits on the break, which I guess is really
probably ok assuming we trust that nothing else is going to stuff bad
ranges in. I can go ahead and make the change.

Your code also breaks out as soon as a good range is found.
Anyway this is a small nitpick; the loop works fine as it is.

[...]
Ok, I'll drop this since it doesn't affect things with the new LDAP
backend.

Please do, you left it in by mistake.

Yeah, there it is sitting unsquashed in my tree :-(
 >
I also added one change related to the LDAP core changes. In the
past if
you did not have a ticket it would prompt for DM password. This was
broken after the updates. I added an additional except in
test_connection().

This should also be fixed soon in ipaldap. Thanks for putting up with
the changes.


So should I drop this in my patch then? I don't really like having to
import ldap.

You can if you're fine with waiting until my patches are pushed.
Otherwise it's covered by https://fedorahosted.org/freeipa/ticket/3499


I left it in for now. Updated patch attached.

rob
>From ee189db2037b4a0ea3267b0ccdc9d8c7665ffbf3 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Fri, 1 Mar 2013 15:02:14 -0500
Subject: [PATCH] Extend ipa-replica-manage to be able to manage DNA ranges.

Attempt to automatically save DNA ranges when a master is removed.
This is done by trying to find a master that does not yet define
a DNA on-deck range. If one can be found then the range on the deleted
master is added.

If one cannot be found then it is reported as an error.

Some validation of the ranges are done to ensure that they do overlap
an IPA local range and do not overlap existing DNA ranges configured
on other masters.

http://freeipa.org/page/V3/Recover_DNA_Ranges

https://fedorahosted.org/freeipa/ticket/3321
---
 install/share/delegation.ldif          |   9 ++
 install/share/replica-acis.ldif        |   5 +
 install/tools/ipa-replica-manage       | 285 ++++++++++++++++++++++++++++++++-
 install/tools/man/ipa-replica-manage.1 |  45 +++++-
 install/updates/40-replication.update  |  12 ++
 ipaserver/install/dsinstance.py        |   3 +-
 ipaserver/install/replication.py       |  98 ++++++++++++
 ipaserver/ipaldap.py                   |   2 +
 8 files changed, 450 insertions(+), 9 deletions(-)

diff --git a/install/share/delegation.ldif b/install/share/delegation.ldif
index f62062fe498634d56128ebf78874c3ba91d7d09b..14069586cf1f1021d281a3e86133de1535b62559 100644
--- a/install/share/delegation.ldif
+++ b/install/share/delegation.ldif
@@ -545,6 +545,15 @@ cn: Remove Replication Agreements
 ipapermissiontype: SYSTEM
 member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
 
+dn: cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: ipapermission
+cn: Modify DNA Range
+ipapermissiontype: SYSTEM
+member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
+
 # Entitlement management
 
 dn: cn=Register Entitlements,cn=permissions,cn=pbac,$SUFFIX
diff --git a/install/share/replica-acis.ldif b/install/share/replica-acis.ldif
index 65dfb7a669965731dfd2c6ac1efd99209a2ea404..f4e96139f356826b1c6e07f7dfdfad2de42aafbd 100644
--- a/install/share/replica-acis.ldif
+++ b/install/share/replica-acis.ldif
@@ -20,6 +20,11 @@ changetype: modify
 add: aci
 aci: (targetattr=*)(targetfilter="(|(objectclass=nsds5replicationagreement)(objectclass=nsDSWindowsReplicationAgreement))")(version 3.0;acl "permission:Remove Replication Agreements";allow (delete) groupdn = "ldap:///cn=Remove Replication Agreements,cn=permissions,cn=pbac,$SUFFIX";)
 
+dn: cn=Posix IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
+changetype: modify
+add: aci
+aci: (targetattr=dnaNextRange || dnaNextValue || dnaMaxValue)(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";)
+
 dn: cn=userRoot,cn=ldbm database,cn=plugins,cn=config
 changetype: modify
 add: aci
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index 82648bd526d714dff45614e096820571ad51b9f6..5ec3678e845cd827eda428e72cf2f6a3fdceac10 100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
@@ -23,6 +23,7 @@ import os
 import re, krbV
 import traceback
 from urllib2 import urlparse
+import ldap
 
 from ipapython import ipautil
 from ipaserver.install import replication, dsinstance, installutils
@@ -34,6 +35,7 @@ from ipapython.ipa_log_manager import *
 from ipapython.dn import DN
 from ipapython.config import IPAOptionParser
 from ipaclient import ipadiscovery
+from xmlrpclib import MAXINT
 
 CACERT = "/etc/ipa/ca.crt"
 
@@ -52,6 +54,10 @@ commands = {
     "clean-ruv":(1, 1, "Replica ID of to clean", "must provide replica ID to clean"),
     "abort-clean-ruv":(1, 1, "Replica ID to abort cleaning", "must provide replica ID to abort cleaning"),
     "list-clean-ruv":(0, 0, "", ""),
+    "dnarange-show":(0, 1, "[master fqdn]", ""),
+    "dnanextrange-show":(0, 1, "", ""),
+    "dnarange-set":(2, 2, "<master fqdn> <range>", "must provide a master and ID range"),
+    "dnanextrange-set":(2, 2, "<master fqdn> <range>", "must provide a master and ID range"),
 }
 
 
@@ -124,6 +130,9 @@ def test_connection(realm, host):
         # We do a search in cn=config. NotFound in this case means no
         # permission
         return False
+    except ldap.LOCAL_ERROR:
+        # more than likely a GSSAPI error
+        return False
 
 def list_replicas(realm, host, replica, dirman_passwd, verbose):
 
@@ -147,7 +156,7 @@ def list_replicas(realm, host, replica, dirman_passwd, verbose):
     dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm))
     try:
         entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL)
-    except:
+    except Exception:
         print "Failed to read master data from '%s': %s" % (host, str(e))
         return
     else:
@@ -157,7 +166,7 @@ def list_replicas(realm, host, replica, dirman_passwd, verbose):
     dn = DN(('cn', 'replicas'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm))
     try:
         entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL)
-    except:
+    except Exception:
         pass
     else:
         for ent in entries:
@@ -272,6 +281,15 @@ def del_link(realm, replica1, replica2, dirman_passwd, force=False):
             repl2.force_sync(repl2.conn, replica1)
             cn, dn = repl2.agreement_dn(repl1.conn.host)
             repl2.wait_for_repl_update(repl2.conn, dn, 30)
+            (range_start, range_max) = repl2.get_DNA_range(repl2.conn.host)
+            (next_start, next_max) = repl2.get_DNA_next_range(repl2.conn.host)
+            if range_start is not None:
+                if not storeDNARange(repl1, range_start, range_max, repl2.conn.host, realm, dirman_passwd):
+                    print "Unable to save DNA range %d-%d" % (range_start, range_max)
+            if next_start is not None:
+                if not storeDNARange(repl1, next_start, next_max, repl2.conn.host, realm, dirman_passwd):
+                    print "Unable to save DNA range %d-%d" % (next_start, next_max)
+            repl2.set_readonly(readonly=False)
             repl2.delete_agreement(replica1)
             repl2.delete_referral(replica1)
             repl2.set_readonly(readonly=False)
@@ -282,11 +300,13 @@ def del_link(realm, replica1, replica2, dirman_passwd, force=False):
         if failed:
             if force:
                 print "Forcing removal on '%s'" % replica1
+                print "Any DNA range on '%s' will be lost" % replica2
             else:
                 return False
 
     if not repl2 and force:
         print "Forcing removal on '%s'" % replica1
+        print "Any DNA range on '%s' will be lost" % replica2
 
     repl1.delete_agreement(replica2)
     repl1.delete_referral(replica2)
@@ -833,6 +853,251 @@ def force_sync(realm, thishost, fromhost, dirman_passwd):
         repl = replication.ReplicationManager(realm, fromhost, dirman_passwd)
         repl.force_sync(repl.conn, thishost)
 
+def show_DNA_ranges(hostname, master, realm, dirman_passwd, nextrange=False):
+    """
+    Display the DNA ranges for all current masters.
+
+    hostname: hostname of the master we're listing from
+    master: specific master to show, or None for all
+    realm: our realm, needed to create a connection
+    dirman_passwd: the DM password, needed to create a connection
+    nextrange: if False then show main range, if True then show next
+
+    Returns nothing
+    """
+    for check_host in [hostname, master]:
+        enforce_host_existence(check_host)
+
+    try:
+        repl = replication.ReplicationManager(realm, hostname, dirman_passwd)
+    except Exception, e:
+        sys.exit("Connection failed: %s" % e)
+    dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix)
+    try:
+        entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL)
+    except Exception:
+        return False
+
+    for ent in entries:
+        remote = ent.single_value('cn')
+        if master is not None and remote != master:
+            continue
+        try:
+            repl2 = replication.ReplicationManager(realm, remote, dirman_passwd)
+        except Exception, e:
+            print "%s: Connection failed: %s" % (remote, ipautil.convert_ldap_error(e))
+            continue
+        if not nextrange:
+            try:
+                (start, max) = repl2.get_DNA_range(remote)
+            except errors.NotFound:
+                print "%s: No permission to read DNA configuration" % remote
+                continue
+            if start is None:
+                print "%s: No range set" % remote
+            else:
+                print "%s: %s-%s" % (remote, start, max)
+        else:
+            try:
+                (next_start, next_max) = repl2.get_DNA_next_range(remote)
+            except errors.NotFound:
+                print "%s: No permission to read DNA configuration" % remote
+                continue
+            if next_start is None:
+                print "%s: No on-deck range set" % remote
+            else:
+                print "%s: %s-%s" % (remote, next_start, next_max)
+
+    return False
+
+
+def storeDNARange(repl, range_start, range_max, deleted_master, realm,
+                 dirman_passwd):
+    """
+    Given a DNA range try to save it in a remaining master in the
+    on-deck (dnaNextRange) value.
+
+    Return True if range was saved, False if not
+
+    This function focuses on finding an available master.
+
+    repl: ReplicaMaster object for the master we're deleting from
+    range_start: The DNA next value
+    range_max: The DNA max value
+    deleted_master: The hostname of the master to be deleted
+    realm: our realm, needed to create a connection
+    dirman_passwd: the DM password, needed to create a connection
+    """
+    dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix)
+    try:
+        entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL)
+    except Exception:
+        return False
+
+    for ent in entries:
+        candidate = ent.single_value('cn')
+        if candidate == deleted_master:
+            continue
+        try:
+            repl2 = replication.ReplicationManager(realm, candidate, dirman_passwd)
+        except Exception, e:
+            print "Connection failed: %s" % ipautil.convert_ldap_error(e)
+            continue
+        (next_start, next_max) = repl2.get_DNA_next_range(candidate)
+        if next_start is None:
+            return repl2.save_DNA_next_range(range_start, range_max)
+
+    return False
+
+
+def set_DNA_range(hostname, range, realm, dirman_passwd, next_range=False):
+    """
+    Given a DNA range try to change it on the designated master.
+
+    The range must not overlap with any other ranges and must be within
+    one of the IPA local ranges as defined in cn=ranges.
+
+    Setting an on-deck range of 0-0 removes the range.
+
+    Return True if range was saved, False if not
+
+    hostname: hostname of the master to set the range on
+    range: The DNA range to set
+    realm: our realm, needed to create a connection
+    dirman_passwd: the DM password, needed to create a connection
+    next_range: if True then setting a next-range, otherwise a DNA range.
+    """
+    def validate_range(range, allow_all_zero=False):
+        """
+        Do some basic sanity checking on the range.
+
+        Returns None if ok, a string if an error.
+        """
+        try:
+            (dna_next, dna_max) = range.split('-', 1)
+        except ValueError, e:
+            return "Invalid range, must be the form x-y"
+
+        try:
+            dna_next = int(dna_next)
+            dna_max = int(dna_max)
+        except ValueError:
+            return "The range must consist of integers"
+
+        if dna_next == 0 and dna_max == 0 and allow_all_zero:
+            return None
+
+        if dna_next <= 0 or dna_max <= 0 or dna_next >= MAXINT or dna_max >= MAXINT:
+            return "The range must consist of positive integers between 1 and %d" % MAXINT
+
+        if dna_next > dna_max:
+            return "Invalid range"
+
+        return None
+
+    def range_intersection(s1, s2, r1, r2):
+        return max(s1, r1) <= min(s2, r2)
+
+    enforce_host_existence(hostname)
+
+    err = validate_range(range, allow_all_zero=next_range)
+    if err is not None:
+        sys.exit(err)
+
+    # Normalize the range
+    (dna_next, dna_max) = range.split('-', 1)
+    dna_next = int(dna_next)
+    dna_max = int(dna_max)
+
+    try:
+        repl = replication.ReplicationManager(realm, hostname, dirman_passwd)
+    except Exception, e:
+        sys.exit("Connection failed: %s" % e)
+    if dna_next > 0:
+        # Verify that the new range doesn't overlap with an existing range
+        dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), repl.suffix)
+        try:
+            entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL)
+        except Exception, e:
+            sys.exit("Failed to read master data from '%s': %s" % (repl.conn.host, str(e)))
+        else:
+            for ent in entries:
+                master = ent.single_value('cn')
+                if master == hostname and not next_range:
+                    continue
+                try:
+                    repl2 = replication.ReplicationManager(realm, master, dirman_passwd)
+                except Exception, e:
+                    print "Connection to %s failed: %s" % (master, e)
+                    print "Overlap not checked."
+                    continue
+                try:
+                    (entry_start, entry_max) = repl2.get_DNA_range(master)
+                except errors.NotFound:
+                    print "%s: No permission to read DNA configuration" % master
+                    continue
+                if (entry_start is not None and
+                    range_intersection(entry_start, entry_max,
+                                       dna_next, dna_max)):
+                    sys.exit("New range overlaps the DNA range on %s" % master)
+                (entry_start, entry_max) = repl2.get_DNA_next_range(master)
+                if (entry_start is not None and
+                    range_intersection(entry_start, entry_max,
+                                       dna_next, dna_max)):
+                    sys.exit("New range overlaps the DNA next range on %s" % master)
+                del(repl2)
+
+        # Verify that this is within one of the IPA domain ranges.
+        dn = DN(('cn','ranges'), ('cn','etc'), repl.suffix)
+        try:
+            entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL,
+                                        "(objectclass=ipaDomainIDRange)")
+        except errors.NotFound, e:
+            sys.exit('Unable to load IPA ranges: %s' % e.message)
+
+        for ent in entries:
+            entry_start = int(ent.single_value('ipabaseid'))
+            entry_max = entry_start + int(ent.single_value('ipaidrangesize'))
+            if dna_next >= entry_start and dna_max <= entry_max:
+                break
+        else:
+            sys.exit("New range does not fit within existing IPA ranges. See ipa help idrange command")
+
+        # If this falls within any of the AD ranges then it fails.
+        try:
+            entries = repl.conn.get_entries(dn, repl.conn.SCOPE_BASE,
+                                            "(objectclass=ipatrustedaddomainrange)")
+        except errors.NotFound:
+            entries = []
+
+        for ent in entries:
+            entry_start = int(ent.single_value('ipabaseid'))
+            entry_max = entry_start + int(ent.single_value('ipaidrangesize'))
+            if range_intersection(dna_next, dna_max, entry_start, entry_max):
+                sys.exit("New range overlaps with a Trust range. See ipa help idrange command")
+
+    if next_range:
+        try:
+            if not repl.save_DNA_next_range(dna_next, dna_max):
+                sys.exit("Updating next range failed")
+        except errors.EmptyModlist:
+            sys.exit("No changes to make")
+        except errors.NotFound:
+                sys.exit("No permission to update ranges")
+        except Exception, e:
+            sys.exit("Updating next range failed: %s" % e)
+    else:
+        try:
+            if not repl.save_DNA_range(dna_next, dna_max):
+                sys.exit("Updating range failed")
+        except errors.EmptyModlist:
+            sys.exit("No changes to make")
+        except errors.NotFound:
+                sys.exit("No permission to update ranges")
+        except Exception, e:
+            sys.exit("Updating range failed: %s" % e)
+
+
 def main():
     if os.getegid() == 0:
         installutils.check_server_configuration()
@@ -915,6 +1180,22 @@ def main():
         abort_clean_ruv(realm, args[1], options)
     elif args[0] == "list-clean-ruv":
         list_clean_ruv(realm, host, dirman_passwd, options.verbose)
+    elif args[0] == "dnarange-show":
+        if len(args) == 2:
+            master = args[1]
+        else:
+            master = None
+        show_DNA_ranges(host, master, realm, dirman_passwd, False)
+    elif args[0] == "dnanextrange-show":
+        if len(args) == 2:
+            master = args[1]
+        else:
+            master = None
+        show_DNA_ranges(host, master, realm, dirman_passwd, True)
+    elif args[0] == "dnarange-set":
+        set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=False)
+    elif args[0] == "dnanextrange-set":
+        set_DNA_range(args[1], args[2], realm, dirman_passwd, next_range=True)
 
 try:
     main()
diff --git a/install/tools/man/ipa-replica-manage.1 b/install/tools/man/ipa-replica-manage.1
index 836743902278ec2273f3ce7a7fbf3992370c4828..d00101990d1d61c3cf81cf07574a478c005de35f 100644
--- a/install/tools/man/ipa-replica-manage.1
+++ b/install/tools/man/ipa-replica-manage.1
@@ -16,13 +16,13 @@
 .\"
 .\" Author: Rob Crittenden <rcrit...@redhat.com>
 .\"
-.TH "ipa-replica-manage" "1" "Mar 14 2008" "FreeIPA" "FreeIPA Manual Pages"
+.TH "ipa-replica-manage" "1" "Mar 1 2013" "FreeIPA" "FreeIPA Manual Pages"
 .SH "NAME"
 ipa\-replica\-manage \- Manage an IPA replica
 .SH "SYNOPSIS"
-ipa\-replica\-manage [\fIOPTION\fR]...  [connect|disconnect|del|list|re\-initialize|force\-sync]
+ipa\-replica\-manage [\fIOPTION\fR]... [COMMAND]
 .SH "DESCRIPTION"
-Manages the replication agreements of an IPA server.
+Manages the replication agreements of an IPA server. The available commands are:
 .TP
 \fBconnect\fR [SERVER_A] <SERVER_B>
 \- Adds a new replication agreement between SERVER_A/localhost and SERVER_B
@@ -54,6 +54,18 @@ Manages the replication agreements of an IPA server.
 \fBlist\-clean\-ruv\fR
 \- List all running CLEANALLRUV and abort CLEANALLRUV tasks.
 .TP
+\fBdnarange\-show [SERVER]\fR
+\- List the DNA ranges
+.TP
+\fBdnarange\-set SERVER START\-END\fR
+\- Set the DNA range on a master
+.TP
+\fBdnanextrange\-show [SERVER]\fR
+\- List the next DNA ranges
+.TP
+\fBdnanextrange\-set SERVER START\-END\fR
+\- Set the DNA next range on a master
+.TP
 The connect and disconnect options are used to manage the replication topology. When a replica is created it is only connected with the master that created it. The connect option may be used to connect it to other existing replicas.
 .TP
 The disconnect option cannot be used to remove the last link of a replica. To remove a replica from the topology use the del option.
@@ -90,7 +102,7 @@ Provide additional information
 Ignore some types of errors, don't prompt when deleting a master
 .TP
 \fB\-c\fR, \fB\-\-cleanup\fR
-When deleting a master with the --force flag, remove leftover references to an already deleted master.
+When deleting a master with the \-\-force flag, remove leftover references to an already deleted master.
 .TP
 \fB\-\-binddn\fR=\fIADMIN_DN\fR
 Bind DN to use with remote server (default is cn=Directory Manager) \- Be careful to quote this value on the command line
@@ -112,6 +124,29 @@ Password for the IPA system user used by the Windows PassSync plugin to synchron
 .TP
 \fB\-\-from\fR=\fISERVER\fR
 The server to pull the data from, used by the re\-initialize and force\-sync commands.
+.SH "RANGES"
+IPA uses the 389\-ds Distributed Numeric Assignment (DNA) Plugin to allocate POSIX ids for users and groups. A range is created when IPA is installed and half the range is assigned to the first IPA master for the purposes of allocation.
+.TP
+New IPA masters do not automatically get a DNA range assignment. A range assignment is done only when a user or POSIX group is added on that master.
+.TP
+The DNA plugin also supports an "on\-deck" or next range configuration. When the primary range is exhaused, rather than going to another master to ask for more, it will use its on\-deck range if one is defined. Each master can have only one range and one on\-deck range defined.
+.TP
+When a master is removed an attempt is made to save its DNA range(s) onto another master in its on\-deck range. IPA will not attempt to extend or merge ranges. If there are no available on\-deck range slots then this is reported to the user. The range is effectively lost unless it is manually merged into the range of another master.
+.TP
+The DNA range and on\-deck (next) values can be managed using the dnarange\-set and dnanextrange\-set commands. The rules for managing these ranges are:
+\- The range must be completely contained within a local range as defined by the ipa idrange command.
+
+\- The range cannot overlap the DNA range or on\-deck range on another IPA master.
+
+\- The range cannot overlap the ID range of an AD Trust.
+
+\- The primary DNA range cannot be removed.
+
+\- An on\-deck range range can be removed by setting it to 0\-0. The assumption is that the range will be manually moved or merged elsewhere.
+.TP
+The range and next range of a specific master can be displayed by passing the FQDN of that master to the dnarange\-show or dnanextrange\-show command.
+.TP
+Performing range changes as a delegated administrator (e.g. not using the Directory Manager password) requires additional 389\-ds ACIs. These are installed in upgraded masters but not existing ones. The changs are made in cn=config which is not replicated. The result is that DNA ranges cannot be managed on non\-upgraded masters as a delegated administrator.
 .SH "EXAMPLES"
 .TP
 List all masters:
@@ -162,7 +197,7 @@ The following examples use the AD administrator account as the synchronization u
 2. Remove any existing kerberos credentials
   # kdestroy
 .TP
-3) Add the winsync replication agreement
+3. Add the winsync replication agreement
   # ipa\-replica\-manage connect \-\-winsync \-\-passsync=<bindpwd_for_syncuser_that will_be_used_for_agreement> \-\-cacert=/path/to/adscacert/WIN\-CA.cer \-\-binddn "cn=administrator,cn=users,dc=ad,dc=example,dc=com" \-\-bindpw <ads_administrator_password> \-v <adserver.fqdn>
 .TP
 You will be prompted to supply the Directory Manager's password.
diff --git a/install/updates/40-replication.update b/install/updates/40-replication.update
index f9e0496be336ec7653e6b1688ad28245014ce6a0..619d14663eeb6f692864c960dfd3542fc22cb581 100644
--- a/install/updates/40-replication.update
+++ b/install/updates/40-replication.update
@@ -2,3 +2,15 @@
 # an agreement.
 dn: cn=userRoot,cn=ldbm database,cn=plugins,cn=config
 add:aci: '(targetattr=nsslapd-readonly)(version 3.0; acl "Allow marking the database readonly"; allow (write) groupdn = "ldap:///cn=Remove Replication Agreements,cn=permissions,cn=pbac,$SUFFIX";)'
+
+# Add rules to manage DNA ranges
+dn: cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: ipapermission
+default:cn: Modify DNA Range
+default:ipapermissiontype: SYSTEM
+default:member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
+
+dn: cn=Posix IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
+add:aci: '(targetattr=dnaNextRange || dnaNextValue || dnaMaxValue)(version 3.0;acl "permission:Modify DNA Range";allow (write) groupdn = "ldap:///cn=Modify DNA Range,cn=permissions,cn=pbac,$SUFFIX";)'
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 77d76a635df9db1cec819d34aeade7456e77d966..3e37843a9c8e47bd46bdea067d407c36c7183117 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -222,6 +222,7 @@ class DsInstance(service.Service):
         self.step("adding master entry", self.__add_master_entry)
         self.step("configuring Posix uid/gid generation",
                   self.__config_uidgid_gen)
+        self.step("adding replication acis", self.__add_replication_acis)
         self.step("enabling compatibility plugin",
                   self.__enable_compat_plugin)
         self.step("tuning directory server", self.__tuning)
@@ -250,7 +251,6 @@ class DsInstance(service.Service):
 
         self.step("adding default layout", self.__add_default_layout)
         self.step("adding delegation layout", self.__add_delegation_layout)
-        self.step("adding replication acis", self.__add_replication_acis)
         self.step("creating container for managed entries", self.__managed_entries)
         self.step("configuring user private groups", self.__user_private_groups)
         self.step("configuring netgroups from hostgroups", self.__host_nis_groups)
@@ -289,7 +289,6 @@ class DsInstance(service.Service):
         self.__common_setup(True)
 
         self.step("setting up initial replication", self.__setup_replica)
-        self.step("adding replication acis", self.__add_replication_acis)
         # See LDIFs for automember configuration during replica install
         self.step("setting Auto Member configuration", self.__add_replica_automember_config)
         self.step("enabling S4U2Proxy delegation", self.__setup_s4u2proxy)
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
index 804d046bf2553daa4aded5c23436a98636e20da0..4b627a2f4d01b068abd5a18561be55f16c9e0209 100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -38,6 +38,7 @@ IPA_USER_CONTAINER = DN(('cn', 'users'), ('cn', 'accounts'))
 PORT = 636
 TIMEOUT = 120
 REPL_MAN_DN = DN(('cn', 'replication manager'), ('cn', 'config'))
+DNA_DN = DN(('cn', 'Posix IDs'), ('cn', 'Distributed Numeric Assignment Plugin'), ('cn', 'plugins'), ('cn', 'config'))
 
 IPA_REPLICA = 1
 WINSYNC = 2
@@ -1308,3 +1309,100 @@ class ReplicationManager(object):
         print "This may be safely interrupted with Ctrl+C"
 
         wait_for_task(self.conn, dn)
+
+    def get_DNA_range(self, hostname):
+        """
+        Return the DNA range on this server as a tuple, (next, max), or
+        (None, None) if no range has been assigned yet.
+
+        Raises an exception on errors reading an entry.
+        """
+        entry = self.conn.get_entry(DNA_DN)
+
+        nextvalue = int(entry.single_value("dnaNextValue", 0))
+        maxvalue = int(entry.single_value("dnaMaxValue", 0))
+
+        sharedcfgdn = entry.single_value("dnaSharedCfgDN", None)
+        if sharedcfgdn is not None:
+            sharedcfgdn = DN(sharedcfgdn)
+
+            shared_entry = self.conn.get_entry(sharedcfgdn)
+            remaining = int(shared_entry.single_value("dnaRemainingValues", 0))
+        else:
+            remaining = 0
+
+        if nextvalue == 0 and maxvalue == 0:
+            return (None, None)
+
+        # Check the magic values for an unconfigured DNA entry
+        if maxvalue == 1100 and nextvalue == 1101 and remaining == 0:
+            return (None, None)
+        else:
+            return (nextvalue, maxvalue)
+
+    def get_DNA_next_range(self, hostname):
+        """
+        Return the DNA "on-deck" range on this server as a tuple, (next, max),
+        or
+        (None, None) if no range has been assigned yet.
+
+        Raises an exception on errors reading an entry.
+        """
+        entry = self.conn.get_entry(DNA_DN)
+
+        range = entry.single_value("dnaNextRange", None)
+
+        if range is None:
+            return (None, None)
+
+        try:
+            (next, max) = range.split('-')
+        except ValueError:
+            # Should not happen, malformed entry, return nothing.
+            return (None, None)
+
+        return (int(next), int(max))
+
+    def save_DNA_next_range(self, next_start, next_max):
+        """
+        Save a DNA range into the on-deck value.
+
+        This adds a dnaNextRange value to the DNA configuration. This
+        attribute takes the form of start-next.
+
+        Returns True on success.
+        Returns False if the range is already defined.
+        Raises an exception on failure.
+        """
+        entry = self.conn.get_entry(DNA_DN)
+
+        range = entry.single_value("dnaNextRange", None)
+
+        if range is not None and next_start != 0 and next_max != 0:
+            return False
+
+        if next_start == 0 and next_max == 0:
+            entry["dnaNextRange"] = None
+        else:
+            entry["dnaNextRange"] = "%s-%s" % (next_start, next_max)
+
+        self.conn.update_entry(entry)
+
+        return True
+
+    def save_DNA_range(self, next_start, next_max):
+        """
+        Save a DNA range.
+
+        This is potentially very dangerous.
+
+        Returns True on success. Raises an exception on failure.
+        """
+        entry = self.conn.get_entry(DNA_DN)
+
+        entry["dnaNextValue"] = next_start
+        entry["dnaMaxValue"] = next_max
+
+        self.conn.update_entry(entry)
+
+        return True
diff --git a/ipaserver/ipaldap.py b/ipaserver/ipaldap.py
index 4a46532642013204720ba467966c59de31a92301..cb9a7e98fd0c486abe5b8b92aff711fa69f23fa9 100644
--- a/ipaserver/ipaldap.py
+++ b/ipaserver/ipaldap.py
@@ -1775,6 +1775,8 @@ class IPAdmin(LDAPClient):
                 if removes:
                     if not force_replace:
                         modlist.append((ldap.MOD_DELETE, key, removes))
+                    elif new_values == []: # delete an empty value
+                        modlist.append((ldap.MOD_DELETE, key, removes))
 
         return modlist
 
-- 
1.8.1

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

Reply via email to