Petr Viktorin wrote:
On 03/01/2013 11:57 PM, Rob Crittenden wrote:
Implement the design at http://freeipa.org/page/V3/Recover_DNA_Ranges

Could you add the link to the commit message?

done


Note that this required some new ACIs in cn=config which is not
replicated so the range-set commands won't work against older instances.
It should be gracefully handled though.

I think noting this in the man page would be helpful.

sure


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?

It also doesn't work so well if you try it using a delegated
administrator, see ticket https://fedorahosted.org/freeipa/ticket/3480

rob

It should be possible to shrink existing ranges, i.e. ones that overlap
with themselves:
$ ipa-replica-manage dnarange-show
vm-075.idm.lab.eng.brq.redhat.com: 1401000005-1401100499
$ ipa-replica-manage dnarange-set vm-075.idm.lab.eng.brq.redhat.com
1401000005-1401100498
New range overlaps the DNA range on vm-075.idm.lab.eng.brq.redhat.com

fixed


freeipa-rcrit-1088-dnarange.patch


From 9a654b0b3730f8d9058dfbf25a93a58e1f4939e7 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.

The patch adds some trailing whitespace, please trim it.
I also found some nitpicks, see below.


https://fedorahosted.org/freeipa/ticket/3321
---
[...]
diff --git a/install/tools/ipa-replica-manage
b/install/tools/ipa-replica-manage
index
859809bf1c301913c3eb7fc92d1ed58675609b8c..6c0b45620dd2deabfc11ef2249b18205fb23b1fd
100755
--- a/install/tools/ipa-replica-manage
+++ b/install/tools/ipa-replica-manage
[...]

+def showDNARanges(hostname, master, realm, dirman_passwd,
nextrange=False):

Style issue: please don't use camel case for functions.

Sure. I kept DNA as upper.

[...]
+    try:
+        repl = replication.ReplicationManager(realm, hostname,
dirman_passwd)
+    except Exception, e:
+        sys.exit("Connection failed: %s" %
ipautil.convert_ldap_error(e))

ipaldap should convert LDAP errors to IPA ones, there's no need to call
convert_ldap_error. Same in other places.

It does in some but it isn't consistent. I removed my calls though.

$ ipa-replica-manage dnarange-show -p badpassword
Connection failed: {'desc': 'Invalid credentials'}

+    dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
repl.suffix)
+    try:
+        entries = repl.conn.get_entries(dn, repl.conn.SCOPE_ONELEVEL)
+    except:

Don't use a bare except. Same in other places.

Fixed.


[...]
+def setDNARange(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

Please also mention next_range.

ok.


[...]
+    def range_intersection(s1, s2, r1, r2):
+        overlap = xrange(max(s1, r1), min(s2, r2) + 1)
+        return len(overlap) > 0

That looks complicated. How about:
def ranges_intersect(s1, s2, r1, r2):
      return max(s1, r1) <= min(s2, r2)

[...]

Sure. My original intention was to return the overlapping range but then decided against it since it is probably fairly obvious anyway.

+    # Normalize the range
+    (dna_next, dna_max) = range.split('-', 1)

Can this be done before validate_range, so id doesn't have to be
duplicated there?

No because then we'd still have to validate things and it would be split into two places. I can live with these 3 lines of duplicated code, esp since we need no error handling around them because it is assumed correct by the time it reaches it.


[...]
+        dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'),
ipautil.realm_to_suffix(realm))

I think you should use repl.suffix instead of generating it again.

Done. I had missed this one.


[...]
+        # Verify that this is within one of the IPA domain ranges.
+        dn = DN(('cn','ranges'),('cn','etc'),repl.suffix)

Style issue: no spaces after commas

+        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)
+
+        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.

+            entry_start = int(ent.single_value('ipabaseid'))
+            entry_max = entry_start +
int(ent.single_value('ipaidrangesize'))
+            if not range_intersection(dna_next, dna_max, entry_start,
entry_max):

I think we want the DNA range to be fully contained in the idrange,
rather than just overlap a part of it.

Good point, fixed.

Please also adjust the man page when you change this.

+                failed += 1
+
+        if failed == len(entries):
+            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)")

If we add more types of ranges in the future, should they also be
checked? Would (!(objectclass=ipaDomainIDRange)) be more appropriate here?

It depends on what the other range is used for. If it isn't for POSIX values then overlap may be perfectly acceptable.


[...]
diff --git a/install/tools/man/ipa-replica-manage.1
b/install/tools/man/ipa-replica-manage.1
index
836743902278ec2273f3ce7a7fbf3992370c4828..d23e2566eb9a22c70991cbdca0140eb1d268533c
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}

Please straighten the curly brace at the end

ok


  .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
+\fBipadnarange\-show [SERVER]\fR

The subcommand is dnarange-show, no ipa at the start. Same for the others.

ok


+\- List the DNA ranges
+.TP
+\fBipadnarange\-set SERVER x\-y\fR

I'd use START-END instead of x-y

ok, that's fine.


+\- Set the DNA range on a master
+.TP
+\fBipadnanextrange\-show [SERVER]\fR
+\- List the next DNA ranges
+.TP
+\fBipadnanextrange\-set SERVER x\-y\fR

here too

[...]
+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 overlap 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 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.

Also, a range can't overlap ranges of trusted AD domains.

Added


[...]
index
804d046bf2553daa4aded5c23436a98636e20da0..9076c8396041a95c7ea01ef15aa77991516d30e6
100644
--- a/ipaserver/install/replication.py
+++ b/ipaserver/install/replication.py
@@ -1308,3 +1308,123 @@ class ReplicationManager(object):
          print "This may be safely interrupted with Ctrl+C"

          wait_for_task(self.conn, dn)
+
+    def getDNARange(self, hostname):

Style issue: please don't use camel-case for methods.

Stuck with upper-case DNA again.


+        """
+        Return the DNA range on this server as a tuple, (next, max), or
+        (None, None) if no range has been assigned yet.
+        """
+        dn = DN(('cn', 'Posix IDs'), ('cn', 'Distributed Numeric
Assignment Plugin'), ('cn', 'plugins'), ('cn', 'config'))

I'd put this in a global constant.

sure


+        try:
+            entry = self.conn.get_entry(dn)
+        except Exception, e:
+            print "Unable to read DNA configuration: %s" % e.message

I think it's better to communicate the error by raising an exception,
rather than pretending the range hasn't been set yet.
With prints, the error won't appear in logs, and can't be checked by the
caller.
Same elsewhere.

Ok, I guess thinking forward to when this gets converted to the framework that makes sense. Currently there are no logs.


[...]
+        if (nextvalue > maxvalue and maxvalue == 1100 and
+            nextvalue == 1101 and remaining == 0):

What are the magic values? Also this redundantly checks if 1101 > 1100.

The magic values are the default values assigned to the DNA configured. The redundant check, and the check for remaining, is just to be absolutely sure that we are dealing with a default configuration and not some custom values. I dropped the 1101 > 1100 check.


I'd expect the DNS plugin to ensure that dnaRemainingValues == 0 if
nextvalue > maxvalue, do we need to check explicitly?

[...]
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))

I don't understand this change. AFAIK updateEntry/generateModList is
only used in ldapupdater now, and it's going away as soon as I can find
time to remove it. If you need to change it I'd like to know why.


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

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().

rob
>From 7639631aa1ff7e45e95c1dccd761cb52a9496897 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/replication.py       |  98 +++++++++++
 ipaserver/ipaldap.py                   |   2 +
 7 files changed, 452 insertions(+), 7 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..86e7b00d13fa273884623fad1656d6fb523b724e 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,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 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)
+
+        ok = False
+        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:
+                ok = True
+                break
+
+        if not ok:
+            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/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