freeipa/ipaserver/masters.py
Rob Crittenden 45fa43540f Configure affinity during server installation
Write a new krb5.conf in case any values changed finding the
right server to configure against (e.g. for CA, KRA) and
ensure the API connection is to the remote server that
will be installed against.

When finding a CA or KRA during initial replica installation
set the remote master as well. The order is:

 - existing server value in /etc/ipa/default.conf
 - the chosen CA host if the server doesn't provide one
 - the chosen KRA host if the server doesn't provide one

This is more or less heirarchical. If a server is provided
then that is considered first. If it provides all the
optional services needed (CA and/or KRA) then it will
be used. Otherwise it will fall back to a server that provides
all the required services.

In short, providing --server either at client install or
with ipa-replica-install is no guarantee that it will
define all topology. This may be unexpected behavior.

For the case of adding a CA or KRA things are effectively
unchanged. This type of install does not appear to be
impacted by affinity issues.

Fixes: https://pagure.io/freeipa/issue/9289

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
2023-09-01 13:13:02 -04:00

205 lines
6.2 KiB
Python

#
# Copyright (C) 2018 FreeIPA Contributors see COPYING for license
#
"""Helpers services in for cn=masters,cn=ipa,cn=etc
"""
from __future__ import absolute_import
import collections
import logging
import random
from ipapython.dn import DN
from ipalib import api
from ipalib import errors
logger = logging.getLogger(__name__)
# constants for ipaConfigString
CONFIGURED_SERVICE = u'configuredService'
ENABLED_SERVICE = u'enabledService'
HIDDEN_SERVICE = u'hiddenService'
PAC_TKT_SIGN_SUPPORTED = u'pacTktSignSupported'
PKINIT_ENABLED = u'pkinitEnabled'
# The service name as stored in cn=masters,cn=ipa,cn=etc. The values are:
# 0: systemd service name
# 1: start order for system service
# 2: LDAP server entry CN, also used as SERVICE_LIST key
service_definition = collections.namedtuple(
"service_definition",
"systemd_name startorder service_entry"
)
SERVICES = [
service_definition('krb5kdc', 10, 'KDC'),
service_definition('kadmin', 20, 'KPASSWD'),
service_definition('named', 30, 'DNS'),
service_definition('httpd', 40, 'HTTP'),
service_definition('ipa-custodia', 41, 'KEYS'),
service_definition('pki-tomcatd', 50, 'CA'),
service_definition('pki-tomcatd', 51, 'KRA'),
service_definition('smb', 60, 'ADTRUST'),
service_definition('winbind', 70, 'EXTID'),
service_definition('ipa-otpd', 80, 'OTPD'),
service_definition('ipa-ods-exporter', 90, 'DNSKeyExporter'),
service_definition('ods-enforcerd', 100, 'DNSSEC'),
service_definition('ipa-dnskeysyncd', 110, 'DNSKeySync'),
]
SERVICE_LIST = {s.service_entry: s for s in SERVICES}
def find_providing_servers(svcname, conn=None, preferred_hosts=(), api=api):
"""Find servers that provide the given service.
:param svcname: The service to find
:param preferred_hosts: preferred servers
:param conn: a connection to the LDAP server
:param api: ipalib.API instance
:return: list of host names in randomized order (possibly empty)
Preferred servers are moved to the front of the list if and only if they
are found as providing servers.
"""
assert isinstance(preferred_hosts, (tuple, list))
if svcname not in SERVICE_LIST:
raise ValueError("Unknown service '{}'.".format(svcname))
if conn is None:
conn = api.Backend.ldap2
dn = DN(api.env.container_masters, api.env.basedn)
query_filter = conn.combine_filters(
[
conn.make_filter(
{
'objectClass': 'ipaConfigObject',
'cn': svcname
},
rules=conn.MATCH_ALL,
),
conn.make_filter(
{
'ipaConfigString': [ENABLED_SERVICE, HIDDEN_SERVICE]
},
rules=conn.MATCH_ANY
),
],
rules=conn.MATCH_ALL
)
try:
entries, _trunc = conn.find_entries(
filter=query_filter,
attrs_list=['ipaConfigString'],
base_dn=dn
)
except errors.NotFound:
return []
# DNS is case insensitive
preferred_hosts = list(host_name.lower() for host_name in preferred_hosts)
servers = []
for entry in entries:
servername = entry.dn[1].value.lower()
cfgstrings = entry.get('ipaConfigString', [])
# always consider enabled services
if ENABLED_SERVICE in cfgstrings:
servers.append(servername)
# use hidden services on preferred hosts
elif HIDDEN_SERVICE in cfgstrings and servername in preferred_hosts:
servers.append(servername)
# unique list of host names
servers = list(set(servers))
# shuffle the list like DNS SRV would randomize it
random.shuffle(servers)
# Move preferred hosts to front
for host_name in reversed(preferred_hosts):
try:
servers.remove(host_name)
except ValueError:
# preferred server not found, log and ignore
logger.warning(
"Lookup failed: Preferred host %s does not provide %s.",
host_name, svcname
)
else:
servers.insert(0, host_name)
logger.debug("Discovery: available servers for service '%s' are %s",
svcname, ', '.join(servers))
return servers
def find_providing_server(svcname, conn=None, preferred_hosts=(), api=api):
"""Find a server that provides the given service.
:param svcname: The service to find
:param conn: a connection to the LDAP server
:param host_name: the preferred server
:param api: ipalib.API instance
:return: the selected host name or None
"""
servers = find_providing_servers(
svcname, conn=conn, preferred_hosts=preferred_hosts, api=api
)
if not servers:
logger.debug("Discovery: no '%s' service found.", svcname)
return None
else:
logger.debug("Discovery: using %s for '%s' service",
servers[0], svcname)
return servers[0]
def get_masters(conn=None, api=api):
"""Get all master hostnames
:param conn: a connection to the LDAP server
:param api: ipalib.API instance
:return: list of hostnames
"""
if conn is None:
conn = api.Backend.ldap2
dn = DN(api.env.container_masters, api.env.basedn)
entries = conn.get_entries(dn, conn.SCOPE_ONELEVEL, None, ['cn'])
return list(e['cn'][0] for e in entries)
def is_service_enabled(svcname, conn=None, api=api):
"""Check if service is enabled on any master
The check function only looks for presence of service entries. It
ignores enabled/hidden flags.
:param svcname: The service to find
:param conn: a connection to the LDAP server
:param api: ipalib.API instance
:return: True/False
"""
if svcname not in SERVICE_LIST:
raise ValueError("Unknown service '{}'.".format(svcname))
if conn is None:
conn = api.Backend.ldap2
dn = DN(api.env.container_masters, api.env.basedn)
query_filter = conn.make_filter(
{
'objectClass': 'ipaConfigObject',
'cn': svcname
},
rules='&'
)
try:
conn.find_entries(
filter=query_filter,
attrs_list=[],
base_dn=dn
)
except errors.NotFound:
return False
else:
return True