summaryrefslogtreecommitdiffstats
path: root/install/tools/ipa-replica-manage
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2013-03-01 15:02:14 -0500
committerRob Crittenden <rcritten@redhat.com>2013-03-13 10:32:36 -0400
commit9005b9bc8aac7c1381aadb7d17107ebbebae005d (patch)
tree15c0b3ce22d95e265130bf8fcb815bdf79e6b4dc /install/tools/ipa-replica-manage
parent63407ed477035765dda38fbead1353d4f47ac26a (diff)
downloadfreeipa-9005b9bc8aac7c1381aadb7d17107ebbebae005d.tar.gz
freeipa-9005b9bc8aac7c1381aadb7d17107ebbebae005d.tar.xz
freeipa-9005b9bc8aac7c1381aadb7d17107ebbebae005d.zip
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
Diffstat (limited to 'install/tools/ipa-replica-manage')
-rwxr-xr-xinstall/tools/ipa-replica-manage288
1 files changed, 286 insertions, 2 deletions
diff --git a/install/tools/ipa-replica-manage b/install/tools/ipa-replica-manage
index fb80ca752..27a7454ab 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, 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" % 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()