mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-29 10:21:18 -06:00
50a2c45760
Install tools may fail with unexpected error when IPA server is not installed on a system. Improve user experience by implementing a check to affected tools. https://fedorahosted.org/freeipa/ticket/1327 https://fedorahosted.org/freeipa/ticket/1347
504 lines
18 KiB
Python
Executable File
504 lines
18 KiB
Python
Executable File
#! /usr/bin/python -E
|
|
# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
|
|
#
|
|
# Copyright (C) 2007 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
import sys
|
|
import os
|
|
|
|
import getpass, ldap, re, krbV
|
|
import traceback, logging
|
|
|
|
from ipapython import ipautil
|
|
from ipaserver.install import replication, dsinstance, installutils
|
|
from ipaserver.install import bindinstance
|
|
from ipaserver import ipaldap
|
|
from ipapython import version
|
|
from ipalib import api, errors, util
|
|
|
|
CACERT = "/etc/ipa/ca.crt"
|
|
|
|
# dict of command name and tuples of min/max num of args needed
|
|
commands = {
|
|
"list":(0, 1, "[master fqdn]", ""),
|
|
"connect":(1, 2, "<master fqdn> [other master fqdn]",
|
|
"must provide the name of the servers to connect"),
|
|
"disconnect":(1, 2, "<master fqdn> [other master fqdn]",
|
|
"must provide the name of the server to disconnect"),
|
|
"del":(1, 1, "<master fqdn>",
|
|
"must provide hostname of master to delete"),
|
|
"re-initialize":(0, 0, "", ""),
|
|
"force-sync":(0, 0, "", "")
|
|
}
|
|
|
|
def parse_options():
|
|
from optparse import OptionParser
|
|
|
|
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("--binddn", dest="binddn", default=None,
|
|
help="Bind DN to use with remote server")
|
|
parser.add_option("--bindpw", dest="bindpw", default=None,
|
|
help="Password for Bind DN to use with remote server")
|
|
parser.add_option("--winsync", dest="winsync", action="store_true", default=False,
|
|
help="This is a Windows Sync Agreement")
|
|
parser.add_option("--cacert", dest="cacert", default=None,
|
|
help="Full path and filename of CA certificate to use with TLS/SSL to the remote server")
|
|
parser.add_option("--win-subtree", dest="win_subtree", default=None,
|
|
help="DN of Windows subtree containing the users you want to sync (default cn=Users,<domain suffix)")
|
|
parser.add_option("--passsync", dest="passsync", default=None,
|
|
help="Password for the Windows PassSync user")
|
|
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
|
|
k = commands.keys()
|
|
for cmd in k:
|
|
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)
|
|
|
|
# set log level
|
|
if options.verbose:
|
|
# if verbose, output events at INFO level if not already
|
|
mylogger = logging.getLogger()
|
|
if mylogger.getEffectiveLevel() > logging.INFO:
|
|
mylogger.setLevel(logging.INFO)
|
|
# else user has already configured logging externally lower
|
|
return options, args
|
|
|
|
def test_connection(realm, host):
|
|
"""
|
|
Make a GSSAPI connection to the remote LDAP server to test out credentials.
|
|
|
|
This is used so we can fall back to promping for the DM password.
|
|
|
|
returns True if connection successful, False otherwise
|
|
"""
|
|
try:
|
|
replman = replication.ReplicationManager(realm, host, None)
|
|
ents = replman.find_replication_agreements()
|
|
del replman
|
|
return True
|
|
except ldap.LOCAL_ERROR:
|
|
return False
|
|
|
|
def list_replicas(realm, host, replica, dirman_passwd, verbose):
|
|
|
|
is_replica = False
|
|
winsync_peer = None
|
|
peers = {}
|
|
|
|
try:
|
|
conn = ipaldap.IPAdmin(host, 636, cacert=CACERT)
|
|
if dirman_passwd:
|
|
conn.do_simple_bind(bindpw=dirman_passwd)
|
|
else:
|
|
conn.do_sasl_gssapi_bind()
|
|
|
|
dn = 'cn=masters,cn=ipa,cn=etc,%s' % util.realm_to_suffix(realm)
|
|
entries = conn.search_s(dn, ldap.SCOPE_ONELEVEL)
|
|
|
|
for ent in entries:
|
|
peers[ent.cn] = ['master', '']
|
|
|
|
dn = 'cn=replicas,cn=ipa,cn=etc,%s' % util.realm_to_suffix(realm)
|
|
entries = conn.search_s(dn, ldap.SCOPE_ONELEVEL)
|
|
|
|
for ent in entries:
|
|
peers[ent.cn] = ent.ipaconfigstring.split(':')
|
|
|
|
except Exception, e:
|
|
print "Failed to get data from '%s': %s" % (host, str(e))
|
|
return
|
|
|
|
|
|
if not replica:
|
|
for k, p in peers.iteritems():
|
|
print '%s: %s' % (k, p[0])
|
|
return
|
|
|
|
# ok we are being ask for info about a specific replica
|
|
for k, p in peers.iteritems():
|
|
if replica == k:
|
|
is_replica = True
|
|
if p[0] == 'winsync':
|
|
winsync_peer = p[1]
|
|
|
|
if not is_replica:
|
|
print "Cannot find %s in public server list" % replica
|
|
return
|
|
|
|
try:
|
|
if winsync_peer:
|
|
repl = replication.ReplicationManager(realm, winsync_peer,
|
|
dirman_passwd)
|
|
cn, dn = repl.agreement_dn(replica)
|
|
entries = repl.conn.search_s(dn, ldap.SCOPE_BASE,
|
|
"(objectclass=nsDSWindowsReplicationAgreement)")
|
|
ent_type = 'winsync'
|
|
else:
|
|
repl = replication.ReplicationManager(realm, replica,
|
|
dirman_passwd)
|
|
entries = repl.find_replication_agreements()
|
|
ent_type = 'replica'
|
|
except Exception, e:
|
|
print "Failed to get data from '%s': %s" % (replica, str(e))
|
|
return
|
|
|
|
for entry in entries:
|
|
print '%s: %s' % (entry.nsds5replicahost, ent_type)
|
|
|
|
if verbose:
|
|
print " last init status: %s" % entry.nsds5replicalastinitstatus
|
|
print " last init ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastinitend))
|
|
print " last update status: %s" % entry.nsds5replicalastupdatestatus
|
|
print " last update ended: %s" % str(ipautil.parse_generalized_time(entry.nsds5replicalastupdateend))
|
|
|
|
def del_link(realm, replica1, replica2, dirman_passwd, force=False):
|
|
|
|
repl2 = None
|
|
|
|
try:
|
|
repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd)
|
|
|
|
type1 = repl1.get_agreement_type(replica2)
|
|
|
|
repl_list = repl1.find_ipa_replication_agreements()
|
|
if not force and len(repl_list) <= 1 and type1 == replication.IPA_REPLICA:
|
|
print "Cannot remove the last replication link of '%s'" % replica1
|
|
print "Please use the 'del' command to remove it from the domain"
|
|
return
|
|
|
|
except ldap.NO_SUCH_OBJECT:
|
|
print "'%s' has no replication agreement for '%s'" % (replica1, replica2)
|
|
return
|
|
except errors.NotFound:
|
|
print "'%s' has no replication agreement for '%s'" % (replica1, replica2)
|
|
return
|
|
except Exception, e:
|
|
print "Failed to get data from '%s': %s" % (replica1, str(e))
|
|
return
|
|
|
|
if type1 == replication.IPA_REPLICA:
|
|
try:
|
|
repl2 = replication.ReplicationManager(realm, replica2, dirman_passwd)
|
|
|
|
repl_list = repl1.find_ipa_replication_agreements()
|
|
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"
|
|
return
|
|
|
|
except ldap.NO_SUCH_OBJECT:
|
|
print "'%s' has no replication agreement for '%s'" % (replica2, replica1)
|
|
if not force:
|
|
return
|
|
except errors.NotFound:
|
|
print "'%s' has no replication agreement for '%s'" % (replica2, replica1)
|
|
if not force:
|
|
return
|
|
except Exception, e:
|
|
print "Failed to get data from '%s': %s" % (replica2, str(e))
|
|
if not force:
|
|
return
|
|
|
|
if repl2 and type1 == replication.IPA_REPLICA:
|
|
failed = False
|
|
try:
|
|
repl2.delete_agreement(replica1)
|
|
repl2.delete_referral(replica1)
|
|
except ldap.LDAPError, e:
|
|
desc = e.args[0]['desc'].strip()
|
|
info = e.args[0].get('info', '').strip()
|
|
print "Unable to remove agreement on %s: %s: %s" % (replica2, desc, info)
|
|
failed = True
|
|
except Exception, e:
|
|
print "Unable to remove agreement on %s: %s" % (replica2, str(e))
|
|
failed = True
|
|
|
|
if failed:
|
|
if force:
|
|
print "Forcing removal on '%s'" % replica1
|
|
else:
|
|
return
|
|
|
|
if not repl2 and force:
|
|
print "Forcing removal on '%s'" % replica1
|
|
|
|
repl1.delete_agreement(replica2)
|
|
repl1.delete_referral(replica2)
|
|
|
|
if type1 == replication.WINSYNC:
|
|
try:
|
|
dn = 'cn=%s,cn=replicas,cn=ipa,cn=etc,%s' % (replica2,
|
|
util.realm_to_suffix(realm))
|
|
entries = repl1.conn.search_s(dn, ldap.SCOPE_SUBTREE)
|
|
if len(entries) != 0:
|
|
dnset = repl1.conn.get_dns_sorted_by_length(entries,
|
|
reverse=True)
|
|
for dns in dnset:
|
|
for dn in dns:
|
|
repl1.conn.deleteEntry(dn)
|
|
except Exception, e:
|
|
print "Error deleting winsync replica shared info: %s" % str(e)
|
|
|
|
|
|
def del_master(realm, hostname, options):
|
|
|
|
force_del = False
|
|
|
|
# 1. Connect to the master to be removed.
|
|
try:
|
|
delrepl = replication.ReplicationManager(realm, hostname, options.dirman_passwd)
|
|
except Exception, e:
|
|
if not options.force:
|
|
print "Unable to delete replica %s: %s" % (hostname, str(e))
|
|
sys.exit(1)
|
|
else:
|
|
print "Unable to connect to replica %s, forcing removal" % hostname
|
|
force_del = True
|
|
|
|
# 2. Connect to the local server
|
|
try:
|
|
thisrepl = replication.ReplicationManager(realm, options.host,
|
|
options.dirman_passwd)
|
|
except Exception, e:
|
|
print "Failed to connect to server %s: %s" % (options.host, str(e))
|
|
sys.exit(1)
|
|
|
|
if force_del:
|
|
dn = 'cn=masters,cn=ipa,cn=etc,%s' % thisrepl.suffix
|
|
res = thisrepl.conn.search_s(dn, ldap.SCOPE_ONELEVEL)
|
|
replica_names = []
|
|
for entry in res:
|
|
replica_names.append(entry.cn)
|
|
else:
|
|
# 2. Get list of agreements.
|
|
replica_names = delrepl.find_ipa_replication_agreements()
|
|
|
|
# 3. Remove each agreement
|
|
for r in replica_names:
|
|
try:
|
|
del_link(realm, r, hostname, options.dirman_passwd, force=True)
|
|
except Exception, e:
|
|
print "There were issues removing a connection: %s" % str(e)
|
|
|
|
# 4. Finally clean up the removed replica common entries.
|
|
try:
|
|
thisrepl.replica_cleanup(hostname, realm, force=True)
|
|
except Exception, e:
|
|
print "Failed to cleanup %s entries: %s" % (hostname, str(e))
|
|
print "You may need to manually remove them from the tree"
|
|
|
|
# 5. And clean up the removed replica DNS entries if any.
|
|
try:
|
|
if bindinstance.dns_container_exists(options.host, thisrepl.suffix):
|
|
if options.dirman_passwd:
|
|
api.Backend.ldap2.connect(bind_dn='cn=Directory Manager',
|
|
bind_pw=options.dirman_passwd)
|
|
else:
|
|
ccache = krbV.default_context().default_ccache().name
|
|
api.Backend.ldap2.connect(ccache=ccache)
|
|
bind = bindinstance.BindInstance()
|
|
bind.remove_master_dns_records(hostname, realm, realm.lower())
|
|
except Exception, e:
|
|
print "Failed to cleanup %s DNS entries: %s" % (hostname, str(e))
|
|
print "You may need to manually remove them from the tree"
|
|
|
|
def add_link(realm, replica1, replica2, dirman_passwd, options):
|
|
|
|
if options.winsync:
|
|
if not options.binddn or not options.bindpw or not options.cacert or not options.passsync:
|
|
logging.error("The arguments --binddn, --bindpw, --passsync and --cacert are required to create a winsync agreement")
|
|
sys.exit(1)
|
|
|
|
if options.cacert:
|
|
# have to install the given CA cert before doing anything else
|
|
ds = dsinstance.DsInstance(realm_name = realm,
|
|
dm_password = dirman_passwd)
|
|
if not ds.add_ca_cert(options.cacert):
|
|
print "Could not load the required CA certificate file [%s]" % options.cacert
|
|
return
|
|
else:
|
|
print "Added CA certificate %s to certificate database for %s" % (options.cacert, replica1)
|
|
|
|
# need to wait until cacert is installed as that command may restart
|
|
# the directory server and kill the connection
|
|
try:
|
|
repl1 = replication.ReplicationManager(realm, replica1, dirman_passwd)
|
|
|
|
except ldap.NO_SUCH_OBJECT:
|
|
print "Cannot find replica '%s'" % replica1
|
|
return
|
|
except errors.NotFound:
|
|
print "Cannot find replica '%s'" % replica1
|
|
return
|
|
except Exception, e:
|
|
print "Failed to get data from '%s': %s" % (replica1, str(e))
|
|
return
|
|
|
|
if options.winsync:
|
|
repl1.setup_winsync_replication(replica2,
|
|
options.binddn, options.bindpw,
|
|
options.passsync, options.win_subtree,
|
|
options.cacert)
|
|
else:
|
|
repl1.setup_gssapi_replication(replica2, "cn=Directory Manager", dirman_passwd)
|
|
print "Connected '%s' to '%s'" % (replica1, replica2)
|
|
|
|
def re_initialize(realm, options):
|
|
|
|
if not options.fromhost:
|
|
print "re-initialize requires the option --from <host name>"
|
|
sys.exit(1)
|
|
|
|
repl = replication.ReplicationManager(realm, options.fromhost, options.dirman_passwd)
|
|
|
|
thishost = installutils.get_fqdn()
|
|
|
|
filter = "(&(nsDS5ReplicaHost=%s)(|(objectclass=nsDSWindowsReplicationAgreement)(objectclass=nsds5ReplicationAgreement)))" % thishost
|
|
entry = repl.conn.search_s("cn=config", ldap.SCOPE_SUBTREE, filter)
|
|
if len(entry) == 0:
|
|
logging.error("Unable to find %s -> %s replication agreement" % (options.fromhost, thishost))
|
|
sys.exit(1)
|
|
if len(entry) > 1:
|
|
logging.error("Found multiple agreements for %s. Only initializing the first one returned: %s" % (thishost, entry[0].dn))
|
|
|
|
repl.initialize_replication(entry[0].dn, repl.conn)
|
|
repl.wait_for_repl_init(repl.conn, entry[0].dn)
|
|
|
|
ds = dsinstance.DsInstance(realm_name = realm, dm_password = options.dirman_passwd)
|
|
ds.init_memberof()
|
|
|
|
def force_sync(realm, thishost, fromhost, dirman_passwd):
|
|
|
|
repl = replication.ReplicationManager(realm, fromhost, dirman_passwd)
|
|
repl.force_sync(repl.conn, thishost)
|
|
|
|
def main():
|
|
if os.getegid() == 0:
|
|
installutils.check_server_configuration()
|
|
elif not os.path.exists('/etc/ipa/default.conf'):
|
|
sys.exit("IPA is not configured on this system.")
|
|
|
|
options, args = parse_options()
|
|
|
|
# Just initialize the environment. This is so the installer can have
|
|
# access to the plugin environment
|
|
api_env = {}
|
|
api_env['in_server'] = True
|
|
|
|
if os.getegid() != 0:
|
|
api_env['log'] = None # turn off logging for non-root
|
|
|
|
api.bootstrap(**api_env)
|
|
api.finalize()
|
|
|
|
dirman_passwd = None
|
|
realm = krbV.default_context().default_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:
|
|
if not test_connection(realm, host):
|
|
dirman_passwd = getpass.getpass("Directory Manager password: ")
|
|
|
|
options.dirman_passwd = 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":
|
|
del_master(realm, args[1], options)
|
|
elif args[0] == "re-initialize":
|
|
re_initialize(realm, options)
|
|
elif args[0] == "force-sync":
|
|
if not options.fromhost:
|
|
print "force-sync requires the option --from <host name>"
|
|
sys.exit(1)
|
|
force_sync(realm, host, options.fromhost, options.dirman_passwd)
|
|
elif args[0] == "connect":
|
|
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 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)
|
|
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
sys.exit(1)
|
|
except SystemExit, e:
|
|
sys.exit(e)
|
|
except RuntimeError, e:
|
|
sys.exit(e)
|
|
except ldap.INVALID_CREDENTIALS:
|
|
print "Invalid password"
|
|
sys.exit(1)
|
|
except ldap.INSUFFICIENT_ACCESS:
|
|
print "Insufficient access"
|
|
sys.exit(1)
|
|
except ldap.LOCAL_ERROR, e:
|
|
print e.args[0]['info']
|
|
sys.exit(1)
|
|
except ldap.SERVER_DOWN, e:
|
|
print e.args[0]['desc']
|
|
except Exception, e:
|
|
print "unexpected error: %s" % str(e)
|
|
sys.exit(1)
|