Petr Viktorin wrote:
On 03/11/2013 05:00 PM, Rob Crittenden wrote:
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.


[...]
Gotcha. I moved where the replica acis are loaded.

Thanks! Everything works now, I just found two issues in error reporting.

I set up three masters like this:

$ ipa-replica-manage dnarange-show
vm-084.idm.lab.eng.brq.redhat.com: 1109050002-1109099999
vm-081.idm.lab.eng.brq.redhat.com: 1109012501-1109024999
vm-079.idm.lab.eng.brq.redhat.com: 1109025001-1109049999
$ ipa-replica-manage dnanextrange-show
vm-084.idm.lab.eng.brq.redhat.com: 1109000000-1109012499
vm-081.idm.lab.eng.brq.redhat.com: 1109190000-1109190001
vm-079.idm.lab.eng.brq.redhat.com: No on-deck range set

vm-079 is git master, the other two have the patch applied.

Now when I deleted vm-081, there was no indication which ranges I lost:

vm-084$ ipa-replica-manage del vm-081.idm.lab.eng.brq.redhat.com
Deleting a master is irreversible.
To reconnect to the remote master you will need to prepare a new replica
file
and re-install.
Continue to delete? [no]: y
Deleting replication agreements between
vm-081.idm.lab.eng.brq.redhat.com and vm-084.idm.lab.eng.brq.redhat.com
ipa: INFO: Setting agreement
cn=meTovm-084.idm.lab.eng.brq.redhat.com,cn=replica,cn=dc\=idm\,dc\=lab\,dc\=eng\,dc\=brq\,dc\=redhat\,dc\=com,cn=mapping
tree,cn=config schedule to 2358-2359 0 to force synch
ipa: INFO: Deleting schedule 2358-2359 0 from agreement
cn=meTovm-084.idm.lab.eng.brq.redhat.com,cn=replica,cn=dc\=idm\,dc\=lab\,dc\=eng\,dc\=brq\,dc\=redhat\,dc\=com,cn=mapping
tree,cn=config
ipa: INFO: Replication Update in progress: FALSE: status: 0 Replica
acquired successfully: Incremental update succeeded: start: 0: end: 0
Unable to remove agreement on vm-081.idm.lab.eng.brq.redhat.com:
Insufficient access: Insufficient 'write' privilege to the
'dnaNextRange' attribute of entry 'cn=posix ids,cn=distributed numeric
assignment plugin,cn=plugins,cn=config'.
Forcing removal on 'vm-084.idm.lab.eng.brq.redhat.com'
Any DNA range on 'vm-081.idm.lab.eng.brq.redhat.com' will be lost
Deleted replication agreement from 'vm-084.idm.lab.eng.brq.redhat.com'
to 'vm-081.idm.lab.eng.brq.redhat.com'
Background task created to clean replication data. This may take a while.
This may be safely interrupted with Ctrl+C

Fixed.

One more detail: Ranges where start==end are invalid. We should fail the
same way as for start>end.

$ ipa-replica-manage dnanextrange-set vm-081.idm.lab.eng.brq.redhat.com
677100401-677100401
ipa: INFO: Unhandled LDAPError: {'info': 'Changes result in an invalid
DNA configuration.', 'desc': 'Server is unwilling to perform'}
Updating next range failed: Server is unwilling to perform: Changes
result in an invalid DNA configuration.



done

rob
>From 2fb6da167cb5cb8ab366c9d2a7b701f903cddb78 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       | 288 ++++++++++++++++++++++++++++++++-
 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, 453 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..32ffb19da37d0c008ec7e6ec7588ec1bcef0be9b 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 store_DNA_range(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 store_DNA_range(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,254 @@ 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 store_DNA_range(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:
+            try:
+                return repl2.save_DNA_next_range(range_start, range_max)
+            except Exception, e:
+                print '%s: %s' % (candidate, e)
+
+    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 +1183,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 25cac6c27143b2d17e19cb05d93666130d155c8b..b3767ecb8f2861f803261984f374cfcec48f432c 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)
@@ -257,7 +258,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)
@@ -288,7 +288,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 88c6fcc9d422cd5127612173f4914aa12044dc33..f231301401d66d4daaa98f086756e5867e7e43e2 100644
--- a/ipaserver/ipaldap.py
+++ b/ipaserver/ipaldap.py
@@ -1796,6 +1796,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