#!/usr/bin/python3 # Authors: Rob Crittenden # # Based on ipa-replica-manage by Karl MacMillan # # Copyright (C) 2011 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import logging import sys import os from ipaplatform.paths import paths from ipaserver.install import (replication, installutils, bindinstance, cainstance) from ipalib import api, errors from ipalib.util import has_managed_topology from ipapython import ipautil, ipaldap, version from ipapython.admintool import ScriptError from ipapython.dn import DN logger = logging.getLogger(os.path.basename(__file__)) # dict of command name and tuples of min/max num of args needed commands = { "list": (0, 1, "[master fqdn]", ""), "connect": (1, 2, " [other master fqdn]", "must provide the name of the servers to connect"), "disconnect": (1, 2, " [other master fqdn]", "must provide the name of the server to disconnect"), "del": (1, 1, "", "must provide hostname of master to delete"), "re-initialize": (0, 0, "", ""), "force-sync": (0, 0, "", ""), "set-renewal-master": (0, 1, "[master fqdn]", "") } def parse_options(): from optparse import OptionParser # pylint: disable=deprecated-module parser = OptionParser(version=version.VERSION) parser.add_option("-H", "--host", dest="host", help="starting host") parser.add_option("-p", "--password", dest="dirman_passwd", help="Directory Manager password") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, help="provide additional information") parser.add_option("-f", "--force", dest="force", action="store_true", default=False, help="ignore some types of errors") parser.add_option("--from", dest="fromhost", help="Host to get data from") options, args = parser.parse_args() valid_syntax = False if len(args): n = len(args) - 1 for cmd in commands: if cmd == args[0]: v = commands[cmd] err = None if n < v[0]: err = v[3] elif n > v[1]: err = "too many arguments" else: valid_syntax = True if err: parser.error("Invalid syntax: %s\nUsage: %s [options] %s" % (err, cmd, v[2])) if not valid_syntax: cmdstr = " | ".join(commands.keys()) parser.error("must provide a command [%s]" % cmdstr) return options, args def list_replicas(realm, host, replica, dirman_passwd, verbose): peers = {} try: # connect to main IPA LDAP server ldap_uri = ipaldap.get_ldap_uri(host, 636, cacert=paths.IPA_CA_CRT) conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, bind_password=dirman_passwd) dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL) for ent in entries: try: cadn = DN(('cn', 'CA'), DN(ent.dn)) entry = conn.get_entry(cadn) peers[ent.single_value['cn']] = ['master', ''] except errors.NotFound: peers[ent.single_value['cn']] = ['CA not configured', ''] except Exception as e: sys.exit( "Failed to get data from '%s' while trying to list replicas: %s" % (host, e)) finally: conn.unbind() if not replica: for k, p in peers.items(): print('%s: %s' % (k, p[0])) return try: repl = replication.get_cs_replication_manager(realm, replica, dirman_passwd) except Exception as e: sys.exit(str(e)) entries = repl.find_replication_agreements() for entry in entries: print('%s' % entry.single_value.get('nsds5replicahost')) if verbose: initstatus = entry.single_value.get('nsds5replicalastinitstatus') if initstatus is not None: print(" last init status: %s" % initstatus) print(" last init ended: %s" % str( ipautil.parse_generalized_time( entry.single_value['nsds5replicalastinitend']))) print(" last update status: %s" % entry.single_value.get( 'nsds5replicalastupdatestatus')) print(" last update ended: %s" % str( ipautil.parse_generalized_time( entry.single_value['nsds5replicalastupdateend']))) def del_link(realm, replica1, replica2, dirman_passwd, force=False): repl2 = None try: repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd) repl1.hostnames = [replica1, replica2] repl_list1 = repl1.find_replication_agreements() # Find the DN of the replication agreement to remove replica1_dn = None for e in repl_list1: if e.single_value.get('nsDS5ReplicaHost') == replica2: replica1_dn = e.dn break if replica1_dn is None: sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) repl1.hostnames = [replica1, replica2] except errors.NetworkError as e: sys.exit("Unable to connect to %s: %s" % (replica1, e)) except Exception as e: sys.exit("Failed to get data from '%s': %s" % (replica1, e)) try: repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd) repl2.hostnames = [replica1, replica2] repl_list = repl2.find_replication_agreements() # Now that we've confirmed that both hostnames are vaild, make sure # that we aren't removing the last link from either side. if not force and len(repl_list) <= 1: print("Cannot remove the last replication link of '%s'" % replica2) print("Please use the 'del' command to remove it from the domain") sys.exit(1) if not force and len(repl_list1) <= 1: print("Cannot remove the last replication link of '%s'" % replica1) print("Please use the 'del' command to remove it from the domain") sys.exit(1) # Find the DN of the replication agreement to remove replica2_dn = None for entry in repl_list: if entry.single_value.get('nsDS5ReplicaHost') == replica1: replica2_dn = entry.dn break # This should never happen if replica2_dn is None: sys.exit("'%s' has no replication agreement for '%s'" % (replica1, replica2)) except errors.NotFound: print("'%s' has no replication agreement for '%s'" % (replica2, replica1)) if not force: return except Exception as exc: print("Failed to get data from '%s': %s" % (replica2, exc)) if not force: sys.exit(1) if repl2: failed = False try: repl2.delete_agreement(replica1, replica2_dn) repl2.delete_referral(replica1, repl1.port) except Exception as exc: print("Unable to remove agreement on %s: %s" % (replica2, exc)) failed = True if failed: if force: print("Forcing removal on '%s'" % replica1) else: sys.exit(1) if not repl2 and force: print("Forcing removal on '%s'" % replica1) repl1.delete_agreement(replica2, replica1_dn) repl1.delete_referral(replica2, repl2.port) print("Deleted replication agreement from '%s' to '%s'" % (replica1, replica2)) def del_master(realm, hostname, options): delrepl = None # 1. Connect to the local dogtag DS server try: thisrepl = replication.get_cs_replication_manager(realm, options.host, options.dirman_passwd) except Exception as e: sys.exit("Failed to connect to server %s: %s" % (options.host, e)) # 2. Ensure we have an agreement with the master if thisrepl.get_replication_agreement(hostname) is None: sys.exit("'%s' has no replication agreement for '%s'" % (options.host, hostname)) # 3. Connect to the dogtag DS to be removed. try: delrepl = replication.get_cs_replication_manager(realm, hostname, options.dirman_passwd) except Exception as e: if not options.force: print("Unable to delete replica %s: %s" % (hostname, e)) sys.exit(1) else: print("Unable to connect to replica %s, forcing removal" % hostname) # 4. Get list of agreements. if delrepl is None: # server not up, just remove it from this server replica_names = [options.host] else: replica_entries = delrepl.find_ipa_replication_agreements() replica_names = [rep.single_value.get('nsds5replicahost') for rep in replica_entries] # 5. Remove each agreement for r in replica_names: try: del_link(realm, r, hostname, options.dirman_passwd, force=True) except Exception as e: sys.exit("There were issues removing a connection: %s" % e) # 6. Pick CA renewal master ca = cainstance.CAInstance(api.env.realm) if ca.is_renewal_master(hostname): ca.set_renewal_master(options.host) # 7. And clean up the removed replica DNS entries if any. try: if bindinstance.dns_container_exists(api.env.basedn): bind = bindinstance.BindInstance() bind.update_system_records() except Exception as e: print("Failed to cleanup %s DNS entries: %s" % (hostname, e)) print("You may need to manually remove them from the tree") def add_link(realm, replica1, replica2, dirman_passwd, options): try: repl2 = replication.get_cs_replication_manager(realm, replica2, dirman_passwd) except Exception as e: sys.exit(str(e)) try: ldap_uri = ipaldap.get_ldap_uri(replica2, 636, cacert=paths.IPA_CA_CRT) conn = ipaldap.LDAPClient(ldap_uri, cacert=paths.IPA_CA_CRT) conn.simple_bind(bind_dn=ipaldap.DIRMAN_DN, bind_password=dirman_passwd) dn = DN(('cn', 'CA'), ('cn', replica2), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ipautil.realm_to_suffix(realm)) conn.get_entries(dn, conn.SCOPE_BASE) conn.unbind() except errors.NotFound: sys.exit('%s does not have a CA configured.' % replica2) except errors.NetworkError as e: sys.exit("Unable to connect to %s: %s" % (ipautil.format_netloc(replica2, 636), str(e))) except Exception as e: sys.exit("Failed to get data while trying to bind to '%s': %s" % (replica1, str(e))) try: repl1 = replication.get_cs_replication_manager(realm, replica1, dirman_passwd) entries = repl1.find_replication_agreements() for e in entries: if e.single_value.get('nsDS5ReplicaHost') == replica2: sys.exit('This replication agreement already exists.') repl1.hostnames = [replica1, replica2] except errors.NotFound: sys.exit("Cannot find replica '%s'" % replica1) except errors.NetworkError as e: sys.exit("Unable to connect to %s: %s" % (replica1, e)) except Exception as e: sys.exit( "Failed to get data from '%s' while trying to get current " "agreements: %s" % (replica1, e)) repl1.setup_replication( replica2, repl2.port, 0, DN(('cn', 'Directory Manager')), dirman_passwd, is_cs_replica=True, local_port=repl1.port) print("Connected '%s' to '%s'" % (replica1, replica2)) def re_initialize(realm, options): if not options.fromhost: sys.exit("re-initialize requires the option --from ") thishost = installutils.get_fqdn() try: repl = replication.get_cs_replication_manager(realm, options.fromhost, options.dirman_passwd) thisrepl = replication.get_cs_replication_manager(realm, thishost, options.dirman_passwd) except Exception as e: sys.exit(str(e)) filter = repl.get_agreement_filter(host=thishost) try: entry = repl.conn.get_entries( DN(('cn', 'config')), repl.conn.SCOPE_SUBTREE, filter) except errors.NotFound: logger.error("Unable to find %s -> %s replication agreement", options.fromhost, thishost) sys.exit(1) if len(entry) > 1: logger.error("Found multiple agreements for %s. Only initializing the " "first one returned: %s", thishost, entry[0].dn) repl.hostnames = thisrepl.hostnames = [thishost, options.fromhost] thisrepl.enable_agreement(options.fromhost) repl.enable_agreement(thishost) repl.initialize_replication(entry[0].dn, repl.conn) repl.wait_for_repl_init(repl.conn, entry[0].dn) def force_sync(realm, thishost, fromhost, dirman_passwd): try: repl = replication.get_cs_replication_manager(realm, fromhost, dirman_passwd) repl.force_sync(repl.conn, thishost) except Exception as e: sys.exit(str(e)) def set_renewal_master(realm, replica): if not replica: replica = installutils.get_fqdn() ca = cainstance.CAInstance(realm) if ca.is_renewal_master(replica): sys.exit("%s is already the renewal master" % replica) try: ca.set_renewal_master(replica) except Exception as e: sys.exit("Failed to set renewal master to %s: %s" % (replica, e)) print("%s is now the renewal master" % replica) def exit_on_managed_topology(what, hint="topologysegment"): if hint == "topologysegment": hinttext = ("Please use `ipa topologysegment-*` commands to manage " "the topology.") elif hint == "ipa-replica-manage-del": hinttext = ("Please use the `ipa-replica-manage del` command.") else: assert False, "Unexpected value" sys.exit("{0} is deprecated with managed IPA replication topology. {1}" .format(what, hinttext)) def main(): installutils.check_server_configuration() options, args = parse_options() # Just initialize the environment. This is so the installer can have # access to the plugin environment api_env = {} if os.getegid() != 0: api_env['log'] = None # turn off logging for non-root api.bootstrap( context='cli', in_server=True, verbose=options.verbose, confdir=paths.ETC_IPA, **api_env ) api.finalize() dirman_passwd = None realm = api.env.realm if options.host: host = options.host else: host = installutils.get_fqdn() options.host = host if options.dirman_passwd: dirman_passwd = options.dirman_passwd else: dirman_passwd = installutils.read_password("Directory Manager", confirm=False, validate=False, retry=False) if dirman_passwd is None: sys.exit("Directory Manager password required") options.dirman_passwd = dirman_passwd api.Backend.ldap2.connect(bind_pw=options.dirman_passwd) if args[0] == "list": replica = None if len(args) == 2: replica = args[1] list_replicas(realm, host, replica, dirman_passwd, options.verbose) elif args[0] == "del": if has_managed_topology(api): exit_on_managed_topology( "Removal of IPA CS replication agreement and replication data", hint="ipa-replica-manage-del") del_master(realm, args[1], options) elif args[0] == "re-initialize": re_initialize(realm, options) elif args[0] == "force-sync": if not options.fromhost: sys.exit("force-sync requires the option --from ") force_sync(realm, host, options.fromhost, options.dirman_passwd) elif args[0] == "connect": if has_managed_topology(api): exit_on_managed_topology("Creation of IPA CS replication agreement") if len(args) == 3: replica1 = args[1] replica2 = args[2] elif len(args) == 2: replica1 = host replica2 = args[1] add_link(realm, replica1, replica2, dirman_passwd, options) elif args[0] == "disconnect": if has_managed_topology(api): exit_on_managed_topology("Removal of IPA CS replication agreement") if len(args) == 3: replica1 = args[1] replica2 = args[2] elif len(args) == 2: replica1 = host replica2 = args[1] del_link(realm, replica1, replica2, dirman_passwd, options.force) elif args[0] == 'set-renewal-master': replica = None if len(args) > 1: replica = args[1] set_renewal_master(realm, replica) api.Backend.ldap2.disconnect() try: main() except KeyboardInterrupt: sys.exit(1) except (SystemExit, ScriptError) as e: sys.exit(e) except Exception as e: sys.exit("unexpected error: %s" % e)