Imported Upstream version 4.8.10

This commit is contained in:
Mario Fetka
2021-10-03 11:06:28 +02:00
parent 10dfc9587b
commit 03a8170b15
2361 changed files with 1883897 additions and 338759 deletions

View File

@@ -0,0 +1,3 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#

View File

@@ -0,0 +1,29 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
"""
Automount installer module
"""
from ipalib.install import service
from ipalib.install.service import enroll_only
from ipapython.install.core import group, knob
@group
class AutomountInstallInterface(service.ServiceInstallInterface):
"""
Interface of the automount installer
Knobs defined here will be available in:
* ipa-client-install
* ipa-client-automount
"""
description = "Automount"
automount_location = knob(
str, None,
description="Automount location",
)
automount_location = enroll_only(automount_location)

3879
ipaclient/install/client.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,226 @@
# Authors: Jan Cholasta <jcholast@redhat.com>
#
# Copyright (C) 2014 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/>.
#
from __future__ import absolute_import
import logging
import os
import tempfile
import shutil
from urllib.parse import urlsplit
from ipalib.install import certmonger, certstore
from ipalib.facts import is_ipa_configured
from ipalib.install.kinit import kinit_keytab
from ipapython import admintool, certdb, ipaldap, ipautil
from ipaplatform import services
from ipaplatform.paths import paths
from ipaplatform.tasks import tasks
from ipalib import api, errors, x509
from ipalib.constants import IPA_CA_NICKNAME, RENEWAL_CA_NAME
from ipalib.util import check_client_configuration
logger = logging.getLogger(__name__)
class CertUpdate(admintool.AdminTool):
command_name = 'ipa-certupdate'
usage = "%prog [options]"
description = ("Update local IPA certificate databases with certificates "
"from the server.")
def validate_options(self):
super(CertUpdate, self).validate_options(needs_root=True)
def run(self):
check_client_configuration()
api.bootstrap(context='cli_installer', confdir=paths.ETC_IPA)
api.finalize()
api.Backend.rpcclient.connect()
run_with_args(api)
api.Backend.rpcclient.disconnect()
def run_with_args(api):
"""
Run the certupdate procedure with the given API object.
:param api: API object with ldap2/rpcclient backend connected
(such that Commands can be invoked)
"""
server = urlsplit(api.env.jsonrpc_uri).hostname
ldap = ipaldap.LDAPClient.from_hostname_secure(server)
tmpdir = tempfile.mkdtemp(prefix="tmp-")
ccache_name = os.path.join(tmpdir, 'ccache')
old_krb5ccname = os.environ.get('KRB5CCNAME')
try:
principal = str('host/%s@%s' % (api.env.host, api.env.realm))
kinit_keytab(principal, paths.KRB5_KEYTAB, ccache_name)
os.environ['KRB5CCNAME'] = ccache_name
try:
result = api.Command.ca_is_enabled(version=u'2.107')
ca_enabled = result['result']
except (errors.CommandError, errors.NetworkError):
result = api.Command.env(server=True, version=u'2.0')
ca_enabled = result['result']['enable_ra']
ldap.gssapi_bind()
certs = certstore.get_ca_certs(
ldap, api.env.basedn, api.env.realm, ca_enabled)
if ca_enabled:
lwcas = api.Command.ca_find()['result']
else:
lwcas = []
finally:
if old_krb5ccname is None:
del os.environ['KRB5CCNAME']
else:
os.environ['KRB5CCNAME'] = old_krb5ccname
shutil.rmtree(tmpdir)
if is_ipa_configured():
update_server(certs)
# pylint: disable=import-error,ipa-forbidden-import
from ipaserver.install import cainstance
# pylint: enable=import-error,ipa-forbidden-import
# Add LWCA tracking requests. Only execute if *this server*
# has CA installed (ca_enabled indicates CA-ful topology).
if cainstance.CAInstance().is_configured():
try:
cainstance.add_lightweight_ca_tracking_requests(lwcas)
except Exception:
logger.exception(
"Failed to add lightweight CA tracking requests")
update_client(certs)
def update_client(certs):
update_file(paths.IPA_CA_CRT, certs)
update_file(paths.KDC_CA_BUNDLE_PEM, certs)
update_file(paths.CA_BUNDLE_PEM, certs)
ipa_db = certdb.NSSDatabase(api.env.nss_dir)
# Remove old IPA certs from /etc/ipa/nssdb
for nickname in ('IPA CA', 'External CA cert'):
while ipa_db.has_nickname(nickname):
try:
ipa_db.delete_cert(nickname)
except ipautil.CalledProcessError as e:
logger.error(
"Failed to remove %s from %s: %s",
nickname, ipa_db.secdir, e)
break
update_db(ipa_db.secdir, certs)
tasks.remove_ca_certs_from_systemwide_ca_store()
tasks.insert_ca_certs_into_systemwide_ca_store(certs)
def update_server(certs):
instance = '-'.join(api.env.realm.split('.'))
update_db(paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % instance, certs)
if services.knownservices.dirsrv.is_running():
services.knownservices.dirsrv.restart(instance)
if services.knownservices.httpd.is_running():
services.knownservices.httpd.restart()
criteria = {
'cert-database': paths.PKI_TOMCAT_ALIAS_DIR,
'cert-nickname': IPA_CA_NICKNAME,
'ca-name': RENEWAL_CA_NAME,
}
request_id = certmonger.get_request_id(criteria)
if request_id is not None:
timeout = api.env.startup_timeout + 60
# The dogtag-ipa-ca-renew-agent-reuse Certmonger CA never
# actually renews the certificate; it only pulls it from the
# ca_renewal LDAP cert store.
#
# Why is this needed? If the CA cert gets renewed long
# before its notAfter (expiry) date (e.g. to switch from
# self-signed to external, or to switch to new external CA),
# then the other (i.e. not caRenewalMaster) CA replicas will
# not promptly pick up the new CA cert. So we make
# ipa-certupdate always check for an updated CA cert.
#
logger.debug("resubmitting certmonger request '%s'", request_id)
certmonger.resubmit_request(
request_id, ca='dogtag-ipa-ca-renew-agent-reuse', profile='')
try:
state = certmonger.wait_for_request(request_id, timeout)
except RuntimeError:
raise admintool.ScriptError(
"Resubmitting certmonger request '%s' timed out, "
"please check the request manually" % request_id)
ca_error = certmonger.get_request_value(request_id, 'ca-error')
if state != 'MONITORING' or ca_error:
raise admintool.ScriptError(
"Error resubmitting certmonger request '%s', "
"please check the request manually" % request_id)
logger.debug("modifying certmonger request '%s'", request_id)
certmonger.modify(request_id, ca='dogtag-ipa-ca-renew-agent')
update_file(paths.CA_CRT, certs)
update_file(paths.CACERT_PEM, certs)
def update_file(filename, certs, mode=0o644):
certs = (c[0] for c in certs if c[2] is not False)
try:
x509.write_certificate_list(certs, filename, mode=mode)
except Exception as e:
logger.error("failed to update %s: %s", filename, e)
def update_db(path, certs):
"""Drop all CA certs from db then add certs from list provided
This may result in some churn as existing certs are dropped
and re-added but this also provides the ability to change
the trust flags.
"""
db = certdb.NSSDatabase(path)
for name, flags in db.list_certs():
if flags.ca:
db.delete_cert(name)
for cert, nickname, trusted, eku in certs:
trust_flags = certstore.key_policy_to_trust_flags(trusted, True, eku)
try:
db.add_cert(cert, nickname, trust_flags)
except ipautil.CalledProcessError as e:
logger.error("failed to update %s in %s: %s", nickname, path, e)

View File

@@ -0,0 +1,606 @@
#
# Authors:
# Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2012, 2019 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/>.
#
# Configure the automount client for ldap.
from __future__ import print_function
import logging
import sys
import os
import shutil
import time
import tempfile
import gssapi
import warnings
try:
from xml.etree import cElementTree as etree
except ImportError:
from xml.etree import ElementTree as etree
import SSSDConfig
# pylint: disable=import-error
from six.moves.urllib.parse import urlsplit
# pylint: enable=import-error
from optparse import OptionParser # pylint: disable=deprecated-module
from ipapython import ipachangeconf
from ipaclient.install import ipadiscovery
from ipaclient.install.client import (
CLIENT_NOT_CONFIGURED,
CLIENT_ALREADY_CONFIGURED,
)
from ipalib import api, errors
from ipalib.install import sysrestore
from ipalib.install.kinit import kinit_keytab
from ipalib.util import check_client_configuration
from ipapython import ipautil
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.dn import DN
from ipaplatform.constants import constants
from ipaplatform.tasks import tasks
from ipaplatform import services
from ipaplatform.paths import paths
from ipapython.admintool import ScriptError
logger = logging.getLogger(os.path.basename(__file__))
def parse_options():
usage = "%prog [options]\n"
parser = OptionParser(usage=usage)
parser.add_option("--server", dest="server", help="FQDN of IPA server")
parser.add_option(
"--location",
dest="location",
default="default",
help="Automount location",
)
parser.add_option(
"-S",
"--no-sssd",
dest="sssd",
action="store_false",
default=True,
help="Do not configure the client to use SSSD for automount",
)
parser.add_option(
"--idmap-domain",
dest="idmapdomain",
default=None,
help="nfs domain for idmapd.conf",
)
parser.add_option(
"--debug",
dest="debug",
action="store_true",
default=False,
help="enable debugging",
)
parser.add_option(
"-U",
"--unattended",
dest="unattended",
action="store_true",
default=False,
help="unattended installation never prompts the user",
)
parser.add_option(
"--uninstall",
dest="uninstall",
action="store_true",
default=False,
help="Unconfigure automount",
)
options, args = parser.parse_args()
return options, args
def wait_for_sssd():
"""
It takes a bit for sssd to get going, lets loop until it is
serving data.
This function returns nothing.
"""
n = 0
found = False
time.sleep(1)
while n < 10 and not found:
try:
ipautil.run([paths.GETENT, "passwd", "admin@%s" % api.env.realm])
found = True
except Exception:
time.sleep(1)
n = n + 1
# This should never happen but if it does, may as well warn the user
if not found:
err_msg = (
"Unable to find 'admin' user with "
"'getent passwd admin@%s'!" % api.env.realm
)
logger.debug('%s', err_msg)
print(err_msg)
print(
"This may mean that sssd didn't re-start properly after "
"the configuration changes."
)
def configure_xml(fstore):
authconf = paths.AUTOFS_LDAP_AUTH_CONF
fstore.backup_file(authconf)
try:
tree = etree.parse(authconf)
except IOError as e:
logger.debug('Unable to open file %s', e)
logger.debug('Creating new from template')
tree = etree.ElementTree(
element=etree.Element('autofs_ldap_sasl_conf')
)
element = tree.getroot()
if element.tag != 'autofs_ldap_sasl_conf':
raise RuntimeError('Invalid XML root in file %s' % authconf)
element.set('usetls', 'no')
element.set('tlsrequired', 'no')
element.set('authrequired', 'yes')
element.set('authtype', 'GSSAPI')
element.set('clientprinc', 'host/%s@%s' % (api.env.host, api.env.realm))
try:
tree.write(authconf, xml_declaration=True, encoding='UTF-8')
except IOError as e:
print("Unable to write %s: %s" % (authconf, e))
else:
print("Configured %s" % authconf)
def configure_nsswitch(statestore, options):
"""
This function was deprecated. Use ipaplatform.tasks.
Point automount to ldap in nsswitch.conf.
This function is for non-SSSD setups only.
"""
warnings.warn(
"Use ipaplatform.tasks.tasks.enable_ldap_automount",
DeprecationWarning,
stacklevel=2
)
return tasks.enable_ldap_automount(statestore)
def configure_autofs_sssd(fstore, statestore, autodiscover, options):
try:
sssdconfig = SSSDConfig.SSSDConfig()
sssdconfig.import_config()
domains = sssdconfig.list_active_domains()
except Exception as e:
sys.exit(e)
try:
sssdconfig.new_service('autofs')
except SSSDConfig.ServiceAlreadyExists:
pass
except SSSDConfig.ServiceNotRecognizedError:
logger.error("Unable to activate the Autofs service in SSSD config.")
logger.info(
"Please make sure you have SSSD built with autofs support "
"installed."
)
logger.info(
"Configure autofs support manually in /etc/sssd/sssd.conf."
)
sys.exit("Cannot create the autofs service in sssd.conf")
sssdconfig.activate_service('autofs')
domain = None
for name in domains:
domain = sssdconfig.get_domain(name)
try:
provider = domain.get_option('id_provider')
except SSSDConfig.NoOptionError:
continue
if provider == "ipa":
domain.add_provider('ipa', 'autofs')
try:
domain.get_option('ipa_automount_location')
print('An automount location is already configured')
sys.exit(CLIENT_ALREADY_CONFIGURED)
except SSSDConfig.NoOptionError:
domain.set_option('ipa_automount_location', options.location)
break
if domain is None:
sys.exit('SSSD is not configured.')
sssdconfig.save_domain(domain)
sssdconfig.write(paths.SSSD_CONF)
statestore.backup_state('autofs', 'sssd', True)
sssd = services.service('sssd', api)
sssd.restart()
print("Restarting sssd, waiting for it to become available.")
wait_for_sssd()
def configure_autofs(fstore, statestore, autodiscover, server, options):
"""
fstore: the FileStore to back up files in
options.server: the IPA server to use
options.location: the Automount location to use
"""
if not autodiscover:
ldap_uri = "ldap://%s" % server
else:
ldap_uri = "ldap:///%s" % api.env.basedn
search_base = str(
DN(
('cn', options.location),
api.env.container_automount,
api.env.basedn,
)
)
replacevars = {
'MAP_OBJECT_CLASS': 'automountMap',
'ENTRY_OBJECT_CLASS': 'automount',
'MAP_ATTRIBUTE': 'automountMapName',
'ENTRY_ATTRIBUTE': 'automountKey',
'VALUE_ATTRIBUTE': 'automountInformation',
'SEARCH_BASE': search_base,
'LDAP_URI': ldap_uri,
}
ipautil.backup_config_and_replace_variables(
fstore, paths.SYSCONFIG_AUTOFS, replacevars=replacevars
)
tasks.restore_context(paths.SYSCONFIG_AUTOFS)
statestore.backup_state('autofs', 'sssd', False)
print("Configured %s" % paths.SYSCONFIG_AUTOFS)
def configure_autofs_common(fstore, statestore, options):
autofs = services.knownservices.autofs
statestore.backup_state('autofs', 'enabled', autofs.is_enabled())
statestore.backup_state('autofs', 'running', autofs.is_running())
try:
autofs.restart()
print("Started %s" % autofs.service_name)
except Exception as e:
logger.error("%s failed to restart: %s", autofs.service_name, e)
try:
autofs.enable()
except Exception as e:
print(
"Failed to configure automatic startup of the %s daemon"
% (autofs.service_name)
)
logger.error(
"Failed to enable automatic startup of the %s daemon: %s",
autofs.service_name,
str(e),
)
def uninstall(fstore, statestore):
RESTORE_FILES = [
paths.SYSCONFIG_AUTOFS,
paths.AUTOFS_LDAP_AUTH_CONF,
paths.SYSCONFIG_NFS,
paths.IDMAPD_CONF,
]
STATES = ['autofs', 'rpcidmapd', 'rpcgssd']
if not statestore.get_state('autofs', 'sssd'):
tasks.disable_ldap_automount(statestore)
if not any(fstore.has_file(f) for f in RESTORE_FILES) or not any(
statestore.has_state(s) for s in STATES
):
print("IPA automount is not configured on this system")
return CLIENT_NOT_CONFIGURED
print("Restoring configuration")
for filepath in RESTORE_FILES:
if fstore.has_file(filepath):
fstore.restore_file(filepath)
if statestore.has_state('autofs'):
enabled = statestore.restore_state('autofs', 'enabled')
running = statestore.restore_state('autofs', 'running')
sssd = statestore.restore_state('autofs', 'sssd')
autofs = services.knownservices.autofs
if not enabled:
autofs.disable()
if not running:
autofs.stop()
if sssd:
try:
sssdconfig = SSSDConfig.SSSDConfig()
sssdconfig.import_config()
sssdconfig.deactivate_service('autofs')
domains = sssdconfig.list_active_domains()
for name in domains:
domain = sssdconfig.get_domain(name)
try:
provider = domain.get_option('id_provider')
except SSSDConfig.NoOptionError:
continue
if provider == "ipa":
domain.remove_option('ipa_automount_location')
sssdconfig.save_domain(domain)
domain.remove_provider('autofs')
sssdconfig.save_domain(domain)
break
sssdconfig.write(paths.SSSD_CONF)
sssd = services.service('sssd', api)
sssd.restart()
wait_for_sssd()
except Exception as e:
print('Unable to restore SSSD configuration: %s' % str(e))
logger.debug(
'Unable to restore SSSD configuration: %s', str(e)
)
# rpcidmapd and rpcgssd are static units now
if statestore.has_state('rpcidmapd'):
statestore.delete_state('rpcidmapd', 'enabled')
statestore.delete_state('rpcidmapd', 'running')
if statestore.has_state('rpcgssd'):
statestore.delete_state('rpcgssd', 'enabled')
statestore.delete_state('rpcgssd', 'running')
nfsutils = services.knownservices['nfs-utils']
try:
nfsutils.restart()
except Exception as e:
logger.error("Failed to restart nfs client services (%s)", str(e))
return 1
return 0
def configure_nfs(fstore, statestore, options):
"""
Configure secure NFS
"""
# Newer Fedora releases ship /etc/nfs.conf instead of /etc/sysconfig/nfs
# and do not require changes there. On these, SECURE_NFS_VAR == None
if constants.SECURE_NFS_VAR:
replacevars = {constants.SECURE_NFS_VAR: 'yes'}
ipautil.backup_config_and_replace_variables(
fstore, paths.SYSCONFIG_NFS, replacevars=replacevars
)
tasks.restore_context(paths.SYSCONFIG_NFS)
print("Configured %s" % paths.SYSCONFIG_NFS)
# Prepare the changes
# We need to use IPAChangeConf as simple regexp substitution
# does not cut it here
conf = ipachangeconf.IPAChangeConf("IPA automount installer")
conf.case_insensitive_sections = False
conf.setOptionAssignment(" = ")
conf.setSectionNameDelimiters(("[", "]"))
if options.idmapdomain is None:
# Set NFSv4 domain to the IPA domain
changes = [conf.setOption('Domain', api.env.domain)]
elif options.idmapdomain == 'DNS':
# Rely on idmapd auto-detection (DNS)
changes = [conf.rmOption('Domain')]
else:
# Set NFSv4 domain to what was provided
changes = [conf.setOption('Domain', options.idmapdomain)]
if changes is not None:
section_with_changes = [conf.setSection('General', changes)]
# Backup the file and apply the changes
fstore.backup_file(paths.IDMAPD_CONF)
conf.changeConf(paths.IDMAPD_CONF, section_with_changes)
tasks.restore_context(paths.IDMAPD_CONF)
print("Configured %s" % paths.IDMAPD_CONF)
rpcgssd = services.knownservices.rpcgssd
try:
rpcgssd.restart()
except Exception as e:
logger.error("Failed to restart rpc-gssd (%s)", str(e))
nfsutils = services.knownservices['nfs-utils']
try:
nfsutils.restart()
except Exception as e:
logger.error("Failed to restart nfs client services (%s)", str(e))
def configure_automount():
try:
check_client_configuration()
except ScriptError as e:
print(e.msg)
sys.exit(e.rval)
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
options, _args = parse_options()
standard_logging_setup(
paths.IPACLIENT_INSTALL_LOG,
verbose=False,
debug=options.debug,
filemode='a',
console_format='%(message)s',
)
cfg = dict(
context='cli_installer',
confdir=paths.ETC_IPA,
in_server=False,
debug=options.debug,
verbose=0,
)
# Bootstrap API early so that env object is available
api.bootstrap(**cfg)
if options.uninstall:
return uninstall(fstore, statestore)
ca_cert_path = None
if os.path.exists(paths.IPA_CA_CRT):
ca_cert_path = paths.IPA_CA_CRT
if statestore.has_state('autofs'):
print('An automount location is already configured')
sys.exit(CLIENT_ALREADY_CONFIGURED)
autodiscover = False
ds = ipadiscovery.IPADiscovery()
if not options.server:
print("Searching for IPA server...")
ret = ds.search(ca_cert_path=ca_cert_path)
logger.debug('Executing DNS discovery')
if ret == ipadiscovery.NO_LDAP_SERVER:
logger.debug('Autodiscovery did not find LDAP server')
s = urlsplit(api.env.xmlrpc_uri)
server = [s.netloc]
logger.debug('Setting server to %s', s.netloc)
else:
autodiscover = True
if not ds.servers:
sys.exit(
'Autodiscovery was successful but didn\'t return a server'
)
logger.debug(
'Autodiscovery success, possible servers %s',
','.join(ds.servers),
)
server = ds.servers[0]
else:
server = options.server
logger.debug("Verifying that %s is an IPA server", server)
ldapret = ds.ipacheckldap(server, api.env.realm, ca_cert_path)
if ldapret[0] == ipadiscovery.NO_ACCESS_TO_LDAP:
print("Anonymous access to the LDAP server is disabled.")
print("Proceeding without strict verification.")
print(
"Note: This is not an error if anonymous access has been "
"explicitly restricted."
)
elif ldapret[0] == ipadiscovery.NO_TLS_LDAP:
logger.warning("Unencrypted access to LDAP is not supported.")
elif ldapret[0] != 0:
sys.exit('Unable to confirm that %s is an IPA server' % server)
if not autodiscover:
print("IPA server: %s" % server)
logger.debug('Using fixed server %s', server)
else:
print("IPA server: DNS discovery")
logger.debug('Configuring to use DNS discovery')
print("Location: %s" % options.location)
logger.debug('Using automount location %s', options.location)
ccache_dir = tempfile.mkdtemp()
ccache_name = os.path.join(ccache_dir, 'ccache')
try:
try:
host_princ = str('host/%s@%s' % (api.env.host, api.env.realm))
kinit_keytab(host_princ, paths.KRB5_KEYTAB, ccache_name)
os.environ['KRB5CCNAME'] = ccache_name
except gssapi.exceptions.GSSError as e:
sys.exit("Failed to obtain host TGT: %s" % e)
# Finalize API when TGT obtained using host keytab exists
api.finalize()
# Now we have a TGT, connect to IPA
try:
api.Backend.rpcclient.connect()
except errors.KerberosError as e:
sys.exit('Cannot connect to the server due to ' + str(e))
try:
# Use the RPC directly so older servers are supported
api.Backend.rpcclient.forward(
'automountlocation_show',
ipautil.fsdecode(options.location),
version=u'2.0',
)
except errors.VersionError as e:
sys.exit('This client is incompatible: ' + str(e))
except errors.NotFound:
sys.exit(
"Automount location '%s' does not exist" % options.location
)
except errors.PublicError as e:
sys.exit(
"Cannot connect to the server due to generic error: %s"
% str(e)
)
finally:
shutil.rmtree(ccache_dir)
if not options.unattended and not ipautil.user_input(
"Continue to configure the system with these values?", False
):
sys.exit("Installation aborted")
try:
if not options.sssd:
tasks.enable_ldap_automount(statestore)
configure_nfs(fstore, statestore, options)
if options.sssd:
configure_autofs_sssd(fstore, statestore, autodiscover, options)
else:
configure_xml(fstore)
configure_autofs(
fstore, statestore, autodiscover, server, options
)
configure_autofs_common(fstore, statestore, options)
except Exception as e:
logger.debug('Raised exception %s', e)
print("Installation failed. Rolling back changes.")
uninstall(fstore, statestore)
return 1
return 0
def main():
try:
if not os.geteuid() == 0:
sys.exit("\nMust be run as root\n")
configure_automount()
except SystemExit as e:
sys.exit(e)
except RuntimeError as e:
sys.exit(e)
except (KeyboardInterrupt, EOFError):
sys.exit(1)

View File

@@ -0,0 +1,69 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from __future__ import absolute_import
from ipaclient.install import client
from ipaplatform.paths import paths
from ipapython.install import cli
from ipapython.install.core import knob, extend_knob
class StandaloneClientInstall(client.ClientInstall):
no_host_dns = False
no_wait_for_dns = False
principal = client.ClientInstall.principal
principal = extend_knob(
principal,
cli_names=list(principal.cli_names) + ['-p'],
)
password = knob(
str, None,
sensitive=True,
description="password to join the IPA realm (assumes bulk password "
"unless principal is also set)",
cli_names=[None, '-w'],
)
@property
def admin_password(self):
if self.principal:
return self.password
return super(StandaloneClientInstall, self).admin_password
@property
def host_password(self):
if not self.principal:
return self.password
return super(StandaloneClientInstall, self).host_password
prompt_password = knob(
None,
description="Prompt for a password to join the IPA realm",
cli_names='-W',
)
on_master = knob(
None,
deprecated=True,
)
ClientInstall = cli.install_tool(
StandaloneClientInstall,
command_name='ipa-client-install',
log_file_name=paths.IPACLIENT_INSTALL_LOG,
debug_option=True,
verbose=True,
console_format='%(message)s',
uninstall_log_file_name=paths.IPACLIENT_UNINSTALL_LOG,
)
def run():
ClientInstall.run_cli()

View File

@@ -0,0 +1,767 @@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
# Configure the Samba suite to operate as domain member in IPA domain
from __future__ import print_function
import logging
import os
import gssapi
from urllib.parse import urlsplit
from optparse import OptionParser # pylint: disable=deprecated-module
from contextlib import contextmanager
from ipaclient import discovery
from ipaclient.install.client import (
CLIENT_NOT_CONFIGURED,
CLIENT_ALREADY_CONFIGURED,
)
from ipalib import api, errors
from ipalib.install import sysrestore
from ipalib.util import check_client_configuration
from ipalib.request import context
from ipapython import ipautil
from ipapython.errors import SetseboolError
from ipapython.ipa_log_manager import standard_logging_setup
from ipapython.dnsutil import DNSName
from ipaplatform.tasks import tasks
from ipaplatform.paths import paths
from ipaplatform.constants import constants
from ipaplatform import services
from ipapython.admintool import ScriptError
from samba import generate_random_password
logger = logging.getLogger(os.path.basename(__file__))
logger.setLevel(logging.DEBUG)
@contextmanager
def use_api_as_principal(principal, keytab):
with ipautil.private_ccache() as ccache_file:
try:
old_principal = getattr(context, "principal", None)
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
store = {"ccache": ccache_file, "client_keytab": keytab}
gssapi.Credentials(name=name, usage="initiate", store=store)
# Finalize API when TGT obtained using host keytab exists
if not api.isdone("finalize"):
api.finalize()
# Now we have a TGT, connect to IPA
try:
if api.Backend.rpcclient.isconnected():
api.Backend.rpcclient.disconnect()
api.Backend.rpcclient.connect()
yield
except gssapi.exceptions.GSSError as e:
raise Exception(
"Unable to bind to IPA server. Error initializing "
"principal %s in %s: %s" % (principal, keytab, str(e))
)
finally:
if api.Backend.rpcclient.isconnected():
api.Backend.rpcclient.disconnect()
setattr(context, "principal", old_principal)
def parse_options():
usage = "%prog [options]\n"
parser = OptionParser(usage=usage)
parser.add_option(
"--server",
dest="server",
help="FQDN of IPA server to connect to",
)
parser.add_option(
"--netbios-name",
dest="netbiosname",
help="NetBIOS name of this machine",
default=None,
)
parser.add_option(
"--no-homes",
dest="no_homes",
action="store_true",
default=False,
help="Do not add [homes] share to the generated Samba configuration",
)
parser.add_option(
"--no-nfs",
dest="no_nfs",
action="store_true",
default=False,
help="Do not allow NFS integration (SELinux booleans)",
)
parser.add_option(
"--force",
dest="force",
action="store_true",
default=False,
help="force installation by redoing all steps",
)
parser.add_option(
"--debug",
dest="debug",
action="store_true",
default=False,
help="print debugging information",
)
parser.add_option(
"-U",
"--unattended",
dest="unattended",
action="store_true",
default=False,
help="unattended installation never prompts the user",
)
parser.add_option(
"--uninstall",
dest="uninstall",
action="store_true",
default=False,
help="Revert configuration and remove SMB service",
)
options, args = parser.parse_args()
return options, args
domain_information_template = """
Domain name: {domain_name}
NetBIOS name: {netbios_name}
SID: {domain_sid}
ID range: {range_id_min} - {range_id_max}
"""
def pretty_print_domain_information(info):
result = []
for domain in info:
result.append(domain_information_template.format(**domain))
return "\n".join(result)
trust_keymap = {
"netbios_name": "ipantflatname",
"domain_sid": "ipantsecurityidentifier",
"domain_name": "cn",
}
trust_keymap_trustdomain = {
"netbios_name": "ipantflatname",
"domain_sid": "ipanttrusteddomainsid",
"domain_name": "cn",
}
def retrieve_domain_information(api):
# Pull down default domain configuration
# IPA master might be missing freeipa-server-trust-ad package
# or `ipa-adtrust-install` was never run. In such case return
# empty list to report an error
try:
tc_command = api.Command.trustconfig_show
except AttributeError:
return []
try:
result = tc_command()["result"]
except errors.PublicError:
return []
l_domain = dict()
for key in trust_keymap:
l_domain[key] = result.get(trust_keymap[key], [None])[0]
# Pull down ID range and other details of our domain
#
# TODO: make clear how to handle multiple ID ranges for ipa-local range
# In Samba only one range can belong to the same idmap domain,
# otherwise winbindd's _wbint_Sids2UnixIDs function will not be able
# to accept that a mapped Unix ID belongs to the specified domain
idrange_local = "{realm}_id_range".format(realm=api.env.realm)
result = api.Command.idrange_show(idrange_local)["result"]
l_domain["range_id_min"] = int(result["ipabaseid"][0])
l_domain["range_id_max"] = (
int(result["ipabaseid"][0]) + int(result["ipaidrangesize"][0]) - 1
)
domains = [l_domain]
# Retrieve list of trusted domains, if they exist
#
# We flatten the whole trust list because it should be non-overlapping
result = api.Command.trust_find()["result"]
for forest in result:
r = api.Command.trustdomain_find(forest["cn"][0], all=True, raw=True)[
"result"
]
# We don't need to process forest root info separately
# as trustdomain_find() returns it as well
for dom in r:
r_dom = dict()
for key in trust_keymap:
r_dom[key] = dom.get(trust_keymap_trustdomain[key], [None])[0]
r_idrange_name = "{realm}_id_range".format(
realm=r_dom["domain_name"].upper()
)
# TODO: support ipa-ad-trust-posix range as well
r_idrange = api.Command.idrange_show(r_idrange_name)["result"]
r_dom["range_id_min"] = int(r_idrange["ipabaseid"][0])
r_dom["range_id_max"] = (
int(r_idrange["ipabaseid"][0]) +
int(r_idrange["ipaidrangesize"][0]) - 1
)
domains.append(r_dom)
return domains
smb_conf_template = """
[global]
# Limit number of forked processes to avoid SMBLoris attack
max smbd processes = 1000
# Use dedicated Samba keytab. The key there must be synchronized
# with Samba tdb databases or nothing will work
dedicated keytab file = FILE:${samba_keytab}
kerberos method = dedicated keytab
# Set up logging per machine and Samba process
log file = /var/log/samba/log.%m
log level = 1
# We force 'member server' role to allow winbind automatically
# discover what is supported by the domain controller side
server role = member server
realm = ${realm}
netbios name = ${machine_name}
workgroup = ${netbios_name}
# Local writable range for IDs not coming from IPA or trusted domains
idmap config * : range = 0 - 0
idmap config * : backend = tdb
"""
idmap_conf_domain_snippet = """
idmap config ${netbios_name} : range = ${range_id_min} - ${range_id_max}
idmap config ${netbios_name} : backend = sss
"""
homes_conf_snippet = """
# Default homes share
[homes]
read only = no
"""
def configure_smb_conf(fstore, statestore, options, domains):
sub_dict = {
"samba_keytab": paths.SAMBA_KEYTAB,
"realm": api.env.realm,
"machine_name": options.netbiosname,
}
# First domain in the list is ours, pull our domain name from there
sub_dict["netbios_name"] = domains[0]["netbios_name"]
# Construct elements of smb.conf by pre-rendering idmap configuration
template = [smb_conf_template]
for dom in domains:
template.extend([ipautil.template_str(idmap_conf_domain_snippet, dom)])
# Add default homes share so that users can log into Samba
if not options.no_homes:
template.extend([homes_conf_snippet])
fstore.backup_file(paths.SMB_CONF)
with open(paths.SMB_CONF, "w") as f:
f.write(ipautil.template_str("\n".join(template), sub_dict))
tasks.restore_context(paths.SMB_CONF)
def generate_smb_machine_account(fstore, statestore, options, domain):
# Ideally, we should be using generate_random_machine_password()
# from samba but it uses munged UTF-16 which is not decodable
# by the code called from 'net changesecretpw -f'. Thus, we'd limit
# password to ASCII only.
return generate_random_password(128, 255)
def retrieve_service_principal(
fstore, statestore, options, domain, principal, password
):
# Use explicit encryption types. SMB service must have arcfour-hmac
# generated to allow domain member to authenticate to the domain controller
args = [
paths.IPA_GETKEYTAB,
"-p",
principal,
"-k",
paths.SAMBA_KEYTAB,
"-P",
"-e",
"aes128-cts-hmac-sha1-96,aes256-cts-hmac-sha1-96,arcfour-hmac",
]
try:
ipautil.run(args, stdin=password + "\n" + password, encoding="utf-8")
except ipautil.CalledProcessError as e:
logger.error(
"Cannot set machine account password at IPA DC. Error: %s",
e,
)
raise
# Once we fetched the keytab, we also need to set ipaNTHash attribute
# Use ipa-pwd-extop plugin to regenerate it from the Kerberos key
value = "ipaNTHash=MagicRegen"
try:
api.Command.service_mod(principal, addattr=value)
except errors.PublicError as e:
logger.error(
"Cannot update %s principal NT hash value due to an error: %s",
principal,
e,
)
raise
def populate_samba_databases(fstore, statestore, options, domain, password):
# First, set domain SID in Samba
args = [paths.NET, "setdomainsid", domain["domain_sid"]]
try:
ipautil.run(args)
except ipautil.CalledProcessError as e:
logger.error("Cannot set domain SID in Samba. Error: %s", e)
raise
# Next, make sure we can set machine account credentials
# the workaround with tdbtool is temporary until 'net' utility
# will not provide us a way to perform 'offline join' procedure
secrets_key = "SECRETS/MACHINE_LAST_CHANGE_TIME/{}".format(
domain["netbios_name"]
)
args = [paths.TDBTOOL, paths.SECRETS_TDB, "store", secrets_key, "2\\00"]
try:
ipautil.run(args)
except ipautil.CalledProcessError as e:
logger.error(
"Cannot prepare machine account creds in Samba. Error: %s", e,
)
raise
secrets_key = "SECRETS/MACHINE_PASSWORD/{}".format(domain["netbios_name"])
args = [paths.TDBTOOL, paths.SECRETS_TDB, "store", secrets_key, "2\\00"]
try:
ipautil.run(args)
except ipautil.CalledProcessError as e:
logger.error(
"Cannot prepare machine account creds in Samba. Error: %s", e,
)
raise
# Finally, set actual machine account's password
args = [paths.NET, "changesecretpw", "-f"]
try:
ipautil.run(args, stdin=password, encoding="utf-8")
except ipautil.CalledProcessError as e:
logger.error(
"Cannot set machine account creds in Samba. Error: %s", e,
)
raise
def configure_default_groupmap(fstore, statestore, options, domain):
args = [
paths.NET,
"groupmap",
"add",
"sid=S-1-5-32-546",
"unixgroup=nobody",
"type=builtin",
]
logger.info("Map BUILTIN\\Guests to a group 'nobody'")
try:
ipautil.run(args)
except ipautil.CalledProcessError as e:
if "already mapped to SID S-1-5-32-546" not in e.stdout:
logger.error(
'Cannot map BUILTIN\\Guests to a group "nobody". Error: %s',
e
)
raise
def set_selinux_booleans(booleans, statestore, backup=True):
def default_backup_func(name, value):
statestore.backup_state("selinux", name, value)
backup_func = default_backup_func if backup else None
try:
tasks.set_selinux_booleans(booleans, backup_func=backup_func)
except SetseboolError as e:
print("WARNING: " + str(e))
logger.info("WARNING: %s", e)
def harden_configuration(fstore, statestore, options, domain):
# Add default homes share so that users can log into Samba
if not options.no_homes:
set_selinux_booleans(
constants.SELINUX_BOOLEAN_SMBSERVICE["share_home_dirs"], statestore
)
# Allow Samba to access NFS-shared content
if not options.no_nfs:
set_selinux_booleans(
constants.SELINUX_BOOLEAN_SMBSERVICE["reshare_nfs_with_samba"],
statestore,
)
def uninstall(fstore, statestore, options):
# Shut down Samba services and disable them
smb = services.service("smb", api)
winbind = services.service("winbind", api)
for svc in (smb, winbind):
if svc.is_running():
svc.stop()
svc.disable()
# Restore the state of affected selinux booleans
boolean_states = {}
for usecase in constants.SELINUX_BOOLEAN_SMBSERVICE:
for name in usecase:
boolean_states[name] = statestore.restore_state("selinux", name)
if boolean_states:
set_selinux_booleans(boolean_states, statestore, backup=False)
# Remove samba's credentials cache
ipautil.remove_ccache(ccache_path=paths.KRB5CC_SAMBA)
# Remove samba's configuration file
if fstore.has_file(paths.SMB_CONF):
ipautil.remove_file(paths.SMB_CONF)
fstore.restore_file(paths.SMB_CONF)
# Remove samba's persistent and temporary tdb files
tdb_files = [
tdb_file
for tdb_file in os.listdir(paths.SAMBA_DIR)
if tdb_file.endswith(".tdb")
]
for tdb_file in tdb_files:
ipautil.remove_file(tdb_file)
# Remove our keys from samba's keytab
if os.path.exists(paths.SAMBA_KEYTAB):
try:
ipautil.run(
[
paths.IPA_RMKEYTAB,
"--principal",
api.env.smb_princ,
"-k",
paths.SAMBA_KEYTAB,
]
)
except ipautil.CalledProcessError as e:
if e.returncode != 5:
logger.critical("Failed to remove old key for %s",
api.env.smb_princ)
with use_api_as_principal(api.env.host_princ, paths.KRB5_KEYTAB):
try:
api.Command.service_del(api.env.smb_princ)
except errors.VersionError as e:
print("This client is incompatible: " + str(e))
except errors.NotFound:
logger.debug("No SMB service principal exists, OK to proceed")
except errors.PublicError as e:
logger.error(
"Cannot connect to the server due to "
"a generic error: %s", e,
)
def run():
try:
check_client_configuration()
except ScriptError as e:
print(e.msg)
return e.rval
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
options, _args = parse_options()
logfile = paths.IPACLIENTSAMBA_INSTALL_LOG
if options.uninstall:
logfile = paths.IPACLIENTSAMBA_UNINSTALL_LOG
standard_logging_setup(
logfile,
verbose=False,
debug=options.debug,
filemode="a",
console_format="%(message)s",
)
cfg = dict(
context="cli_installer",
confdir=paths.ETC_IPA,
in_server=False,
debug=options.debug,
verbose=0,
)
# Bootstrap API early so that env object is available
api.bootstrap(**cfg)
local_config = dict(
host_princ=str("host/%s@%s" % (api.env.host, api.env.realm)),
smb_princ=str("cifs/%s@%s" % (api.env.host, api.env.realm)),
)
# Until api.finalize() is called, we can add our own configuration
api.env._merge(**local_config)
if options.uninstall:
if statestore.has_state("domain_member"):
uninstall(fstore, statestore, options)
try:
keys = (
"configured", "hardening", "groupmap", "tdb",
"service.principal", "smb.conf"
)
for key in keys:
statestore.delete_state("domain_member", key)
except Exception as e:
print(
"Error: Failed to remove the domain_member statestores: "
"%s" % e
)
return 1
else:
print(
"Samba configuration is reverted. "
"However, Samba databases were fully cleaned and "
"old configuration file will not be usable anymore."
)
else:
print("Samba domain member is not configured yet")
return 0
ca_cert_path = None
if os.path.exists(paths.IPA_CA_CRT):
ca_cert_path = paths.IPA_CA_CRT
if statestore.has_state("domain_member") and not options.force:
print("Samba domain member is already configured")
return CLIENT_ALREADY_CONFIGURED
if not os.path.exists(paths.SMBD):
print("Samba suite is not installed")
return CLIENT_NOT_CONFIGURED
autodiscover = False
ds = discovery.IPADiscovery()
if not options.server:
print("Searching for IPA server...")
ret = ds.search(ca_cert_path=ca_cert_path)
logger.debug("Executing DNS discovery")
if ret == discovery.NO_LDAP_SERVER:
logger.debug("Autodiscovery did not find LDAP server")
s = urlsplit(api.env.xmlrpc_uri)
server = [s.netloc]
logger.debug("Setting server to %s", s.netloc)
else:
autodiscover = True
if not ds.servers:
print(
"Autodiscovery was successful but didn't return a server"
)
return 1
logger.debug(
"Autodiscovery success, possible servers %s",
",".join(ds.servers),
)
server = ds.servers[0]
else:
server = options.server
logger.debug("Verifying that %s is an IPA server", server)
ldapret = ds.ipacheckldap(server, api.env.realm, ca_cert_path)
if ldapret[0] == discovery.NO_ACCESS_TO_LDAP:
print("Anonymous access to the LDAP server is disabled.")
print("Proceeding without strict verification.")
print(
"Note: This is not an error if anonymous access has been "
"explicitly restricted."
)
elif ldapret[0] == discovery.NO_TLS_LDAP:
logger.warning("Unencrypted access to LDAP is not supported.")
elif ldapret[0] != 0:
print("Unable to confirm that %s is an IPA server" % server)
return 1
if not autodiscover:
print("IPA server: %s" % server)
logger.debug("Using fixed server %s", server)
else:
print("IPA server: DNS discovery")
logger.info("Configured to use DNS discovery")
if api.env.host == server:
logger.error(
"Cannot run on IPA master. "
"Cannot configure Samba as a domain member on a domain "
"controller. Please use ipa-adtrust-install for that!"
)
return 1
if not options.netbiosname:
options.netbiosname = DNSName.from_text(api.env.host)[0].decode()
options.netbiosname = options.netbiosname.upper()
with use_api_as_principal(api.env.host_princ, paths.KRB5_KEYTAB):
try:
# Try to access 'service_add_smb' command, if it throws
# AttributeError exception, the IPA server doesn't support
# setting up Samba as a domain member.
service_add_smb = api.Command.service_add_smb
# Now try to see if SMB principal already exists
api.Command.service_show(api.env.smb_princ)
# If no exception was raised, the object exists.
# We cannot continue because we would break existing configuration
print(
"WARNING: SMB service principal %s already exists. "
"Please remove it before proceeding." % (api.env.smb_princ)
)
if not options.force:
return 1
# For --force, we should then delete cifs/.. service object
api.Command.service_del(api.env.smb_princ)
except AttributeError:
logger.error(
"Chosen IPA master %s does not have support to "
"set up Samba domain members", server,
)
return 1
except errors.VersionError as e:
print("This client is incompatible: " + str(e))
return 1
except errors.NotFound:
logger.debug("No SMB service principal exists, OK to proceed")
except errors.PublicError as e:
logger.error(
"Cannot connect to the server due to "
"a generic error: %s", e,
)
return 1
# At this point we have proper setup:
# - we connected to IPA API end-point as a host principal
# - no cifs/... principal exists so we can create it
print("Chosen IPA master: %s" % server)
print("SMB principal to be created: %s" % api.env.smb_princ)
print("NetBIOS name to be used: %s" % options.netbiosname)
logger.info("Chosen IPA master: %s", server)
logger.info("SMB principal to be created: %s", api.env.smb_princ)
logger.info("NetBIOS name to be used: %s", options.netbiosname)
# 1. Pull down ID range and other details of known domains
domains = retrieve_domain_information(api)
if len(domains) == 0:
# logger.error() produces both log file and stderr output
logger.error("No configured trust controller detected "
"on IPA masters. Use ipa-adtrust-install on an IPA "
"master to configure trust controller role.")
return 1
str_info = pretty_print_domain_information(domains)
logger.info("Discovered domains to use:\n%s", str_info)
print("Discovered domains to use:\n%s" % str_info)
if not options.unattended and not ipautil.user_input(
"Continue to configure the system with these values?", False
):
print("Installation aborted")
return 1
# 2. Create SMB service principal, if we are here, the command exists
if (
not statestore.get_state("domain_member", "service.principal") or
options.force
):
service_add_smb(api.env.host, options.netbiosname)
statestore.backup_state(
"domain_member", "service.principal", "configured"
)
# 3. Generate machine account password for reuse
password = generate_smb_machine_account(
fstore, statestore, options, domains[0]
)
# 4. Now that we have all domains retrieved, we can generate smb.conf
if (
not statestore.get_state("domain_member", "smb.conf") or
options.force
):
configure_smb_conf(fstore, statestore, options, domains)
statestore.backup_state("domain_member", "smb.conf", "configured")
# 5. Create SMB service
if statestore.get_state("domain_member",
"service.principal") == "configured":
retrieve_service_principal(
fstore, statestore, options, domains[0],
api.env.smb_princ, password
)
statestore.backup_state(
"domain_member", "service.principal", "configured"
)
# 6. Configure databases to contain proper details
if not statestore.get_state("domain_member", "tdb") or options.force:
populate_samba_databases(
fstore, statestore, options, domains[0], password
)
statestore.backup_state("domain_member", "tdb", "configured")
# 7. Configure default group mapping
if (
not statestore.get_state("domain_member", "groupmap") or
options.force
):
configure_default_groupmap(fstore, statestore, options, domains[0])
statestore.backup_state("domain_member", "groupmap", "configured")
# 8. Enable SELinux policies
if (
not statestore.get_state("domain_member", "hardening") or
options.force
):
harden_configuration(fstore, statestore, options, domains[0])
statestore.backup_state("domain_member", "hardening", "configured")
# 9. Finally, store the state of upgrade
statestore.backup_state("domain_member", "configured", True)
# Suggest service start only after validating smb.conf
print(
"Samba domain member is configured. "
"Please check configuration at %s and "
"start smb and winbind services" % paths.SMB_CONF
)
logger.info(
"Samba domain member is configured. "
"Please check configuration at %s and "
"start smb and winbind services",
paths.SMB_CONF,
)
return 0

View File

@@ -0,0 +1,827 @@
#
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
# 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/>.
"""This tool prepares then sends email notifications to users
whose passwords are expiring in the near future.
"""
from __future__ import absolute_import, print_function
import ast
import grp
import json
import os
import pwd
import logging
import smtplib
import time
from collections import deque
from datetime import datetime, timedelta
from email.utils import formataddr, formatdate
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.utils import make_msgid
from socket import error as socketerror
from ipaplatform.paths import paths
from ipalib import api, errors
from ipalib.facts import is_ipa_client_configured
from ipapython import admintool, ipaldap
from ipapython.dn import DN
from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError
EPN_CONF = "/etc/ipa/epn.conf"
EPN_CONFIG = {
"smtp_server": "localhost",
"smtp_port": 25,
"smtp_user": None,
"smtp_password": None,
"smtp_timeout": 60,
"smtp_security": "none",
"smtp_admin": "root@localhost",
"smtp_delay": None,
"mail_from": None,
"notify_ttls": "28,14,7,3,1",
"msg_charset": "utf8",
"msg_subtype": "plain",
"msg_subject": "Your password will expire soon.",
}
logger = logging.getLogger(__name__)
def drop_privileges(new_username="daemon", new_groupname="daemon"):
"""Drop privileges, defaults to daemon:daemon.
"""
try:
if os.getuid() != 0:
return
os.setgroups([])
os.setgid(pwd.getpwnam(new_username).pw_uid)
os.setuid(grp.getgrnam(new_groupname).gr_gid)
if os.getuid() == 0:
raise Exception()
logger.debug(
"Dropped privileges to user=%s, group=%s",
new_username,
new_groupname,
)
except Exception as e:
logger.error(
"Failed to drop privileges to %s, %s: %s",
new_username,
new_groupname,
e,
)
class EPNUserList:
"""Maintains a list of users whose passwords are expiring.
Provides add(), check(), pop(), and json_print().
From the outside, the list is considered always sorted:
* displaying the list results in a sorted JSON representation thereof
* pop() returns the "most urgent" item from the list.
Internal implementation notes:
* Uses a deque instead of a list for efficiency reasons
* all add()-style methods MUST set _sorted to False.
* all print() and pop-like methods MUST call _sort() first.
"""
def __init__(self):
self._sorted = False
self._expiring_password_user_dq = deque()
def __bool__(self):
"""If it quacks like a container...
"""
return bool(self._expiring_password_user_dq)
def __len__(self):
"""Return len(self)."""
return len(self._expiring_password_user_dq)
def get_ldap_attr(self, entry, attr):
"""Get a single value from a multi-valued attr in a safe way"""
return str(entry.get(attr, [""]).pop(0))
def add(self, entry):
"""Parses and appends an LDAP user entry with the uid, cn,
givenname, sn, krbpasswordexpiration and mail attributes.
"""
try:
self._sorted = False
if entry.get("mail") is None:
logger.error("IPA-EPN: No mail address defined for: %s",
entry.dn)
return
self._expiring_password_user_dq.append(
dict(
uid=self.get_ldap_attr(entry, "uid"),
cn=self.get_ldap_attr(entry, "cn"),
givenname=self.get_ldap_attr(entry, "givenname"),
sn=self.get_ldap_attr(entry, "sn"),
krbpasswordexpiration=(
self.get_ldap_attr(entry,"krbpasswordexpiration")
),
mail=str(entry.get("mail")),
)
)
except IndexError as e:
logger.info("IPA-EPN: Could not parse entry: %s", e)
def pop(self):
"""Returns the "most urgent" user to notify.
In fact: popleft()
"""
self._sort()
try:
return self._expiring_password_user_dq.popleft()
except IndexError:
return False
def check(self):
self.json_print(really_print=False)
def json_print(self, really_print=True):
"""Dump self._expiring_password_user_dq to JSON.
Check that the result can be re-rencoded to UTF-8.
If really_print, print the result.
"""
try:
self._sort()
temp_str = json.dumps(
list(self._expiring_password_user_dq),
indent=4,
ensure_ascii=False,
)
temp_str.encode("utf8")
if really_print:
print(temp_str)
except Exception as e:
logger.error("IPA-EPN: unexpected error: %s", e)
def _sort(self):
if not self._sorted:
if isinstance(self._expiring_password_user_dq, deque):
self._expiring_password_user_dq = deque(
sorted(
self._expiring_password_user_dq,
key=lambda item: item["krbpasswordexpiration"],
)
)
self._sorted = True
class EPN(admintool.AdminTool):
command_name = "IPA-EPN"
log_file_name = paths.IPAEPN_LOG
usage = "%prog [options]"
description = "Expiring Password Notifications (EPN)"
def __init__(self, options, args):
super(EPN, self).__init__(options, args)
self._conn = None
self._expiring_password_user_list = EPNUserList()
self._ldap_data = []
self._date_ranges = []
self._mailer = None
self.env = None
self.default_email_domain = None
@classmethod
def add_options(cls, parser):
super(EPN, cls).add_options(parser, debug_option=True)
parser.add_option(
"--from-nbdays",
dest="from_nbdays",
action="store",
default=None,
help="minimal number of days",
)
parser.add_option(
"--to-nbdays",
dest="to_nbdays",
action="store",
default=None,
help="maximal number of days",
)
parser.add_option(
"--dry-run",
dest="dry_run",
action="store_true",
default=False,
help="Dry run mode. JSON ouput only.",
)
parser.add_option(
"--mail-test",
dest="mailtest",
action="store_true",
default=False,
help="Send a test e-mail",
)
def validate_options(self):
super(EPN, self).validate_options(needs_root=True)
if self.options.to_nbdays is not None:
try:
if int(self.options.to_nbdays) < 0:
raise RuntimeError('Input is negative.')
except Exception as e:
self.option_parser.error(
"--to-nbdays must be a positive integer. "
"{error}".format(error=e)
)
self.options.dry_run = True
if self.options.from_nbdays is not None:
try:
if int(self.options.from_nbdays) < 0:
raise RuntimeError('Input is negative.')
except Exception as e:
self.option_parser.error(
"--from-nbdays must be a positive integer. "
"{error}".format(error=e)
)
if self.options.from_nbdays is not None and \
self.options.to_nbdays is not None:
if int(self.options.from_nbdays) >= int(self.options.to_nbdays):
self.option_parser.error(
"--from-nbdays must be smaller than --to-nbdays."
)
if self.options.from_nbdays is not None and \
self.options.to_nbdays is None:
self.option_parser.error(
"You cannot specify --from-nbdays without --to-nbdays"
)
if self.options.mailtest and self.options.dry_run:
self.option_parser.error(
"You cannot specify --mail-test and --dry-run together"
)
def setup_logging(self, log_file_mode="a"):
super(EPN, self).setup_logging(log_file_mode="a")
def run(self):
super(EPN, self).run()
if not is_ipa_client_configured():
logger.error("IPA client is not configured on this system.")
raise admintool.ScriptError()
self._get_krb5_ticket()
self._read_configuration()
self._validate_configuration()
self._parse_configuration()
self._get_connection()
self._read_ipa_configuration()
drop_privileges()
if self.options.mailtest:
self._gentestdata()
else:
if self.options.to_nbdays:
self._build_cli_date_ranges()
for date_range in self._date_ranges:
self._fetch_data_from_ldap(date_range)
self._parse_ldap_data()
if self.options.dry_run:
self._pretty_print_data()
else:
self._mailer = MailUserAgent(
security_protocol=api.env.smtp_security,
smtp_hostname=api.env.smtp_server,
smtp_port=api.env.smtp_port,
smtp_timeout=api.env.smtp_timeout,
smtp_username=api.env.smtp_user,
smtp_password=api.env.smtp_password,
x_mailer=self.command_name,
msg_subtype=api.env.msg_subtype,
msg_charset=api.env.msg_charset,
)
self._send_emails()
def _get_date_range_from_nbdays(self, nbdays_end, nbdays_start=None):
"""Detects current time and returns a date range, given a number
of days in the future.
If only nbdays_end is specified, the range is 1d long.
"""
now = datetime.utcnow()
today_at_midnight = datetime.combine(now, datetime.min.time())
range_end = today_at_midnight + timedelta(days=nbdays_end)
if nbdays_start is not None:
range_start = today_at_midnight + timedelta(days=nbdays_start)
else:
range_start = range_end - timedelta(days=1)
logger.debug(
"IPA-EPN: Current date: %s \n"
"IPA-EPN: Date & time, today at midnight: %s \n"
"IPA-EPN: Date range start: %s \n"
"IPA-EPN: Date range end: %s \n",
now,
today_at_midnight,
range_start,
range_end,
)
return (range_start, range_end)
def _datetime_to_generalized_time(self, dt):
"""Convert datetime to LDAP_GENERALIZED_TIME_FORMAT
Note: Consider moving into ipalib.
"""
dt = dt.timetuple()
generalized_time_str = str(dt.tm_year) + "".join(
"0" * (2 - len(str(item))) + str(item)
for item in (
dt.tm_mon,
dt.tm_mday,
dt.tm_hour,
dt.tm_min,
dt.tm_sec,
)
)
return generalized_time_str + "Z"
def _get_krb5_ticket(self):
"""Setup the environment to obtain a krb5 ticket for us using the
system keytab.
Uses CCACHE = MEMORY (limited to the current process).
"""
os.environ.setdefault("KRB5_CLIENT_KTNAME", "/etc/krb5.keytab")
os.environ["KRB5CCNAME"] = "MEMORY:"
def _read_configuration(self):
"""Merge in the EPN configuration from /etc/ipa/epn.conf"""
base_config = dict(
context="epn", confdir=paths.ETC_IPA, in_server=False,
)
api.bootstrap(**base_config)
api.env._merge(**EPN_CONFIG)
if not api.isdone("finalize"):
api.finalize()
def _validate_configuration(self):
"""Examine the user-provided configuration.
"""
if api.env.smtp_security.lower() not in ("none", "starttls", "ssl"):
raise RuntimeError(
"smtp_security must be one of: " "none, starttls or ssl"
)
if api.env.smtp_user is not None and api.env.smtp_password is None:
raise RuntimeError("smtp_user set and smtp_password is not")
if api.env.notify_ttls is None:
raise RuntimeError("notify_ttls must be set in %s" % EPN_CONF)
try:
[int(k) for k in str(api.env.notify_ttls).split(',')]
except ValueError as e:
raise RuntimeError('Failed to parse notify_ttls: \'%s\': %s' %
(api.env.notify_ttls, e))
if api.env.smtp_delay:
try:
float(api.env.smtp_delay)
except ValueError as e:
raise RuntimeError('smtp_delay is misformatted: %s' % e)
if float(api.env.smtp_delay) < 0:
raise RuntimeError('smtp_delay cannot be less than zero')
def _parse_configuration(self):
"""
"""
daylist = [int(k) for k in str(api.env.notify_ttls).split(',')]
daylist.sort()
for day in daylist:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=None, nbdays_end=day + 1
)
)
loader = FileSystemLoader(os.path.join(api.env.confdir, 'epn'))
self.env = Environment(loader=loader)
def _read_ipa_configuration(self):
"""Get the IPA configuration"""
api.Backend.rpcclient.connect()
result = api.Command.config_show()['result']
self.default_email_domain = result.get('ipadefaultemaildomain',
[None])[0]
api.Backend.rpcclient.disconnect()
def _get_connection(self):
"""Create a connection to LDAP and bind to it.
"""
if self._conn is not None:
return self._conn
try:
# LDAPI
self._conn = ipaldap.LDAPClient.from_realm(api.env.realm)
self._conn.external_bind()
except Exception:
try:
# LDAP + GSSAPI
self._conn = ipaldap.LDAPClient.from_hostname_secure(
api.env.server
)
self._conn.gssapi_bind()
except Exception as e:
logger.error(
"Unable to bind to LDAP server %s: %s",
self._conn.ldap_uri,
e,
)
return self._conn
def _fetch_data_from_ldap(self, date_range):
"""Run a LDAP query to fetch a list of user entries whose passwords
would expire in the near future. Store in self._ldap_data.
"""
if self._conn is None:
logger.error(
"IPA-EPN: Connection to LDAP not established. Exiting."
)
search_base = DN(api.env.container_user, api.env.basedn)
attrs_list = ["uid", "krbpasswordexpiration", "mail", "cn",
"givenname", "surname"]
search_filter = (
"(&(!(nsaccountlock=TRUE)) \
(krbpasswordexpiration<=%s) \
(krbpasswordexpiration>=%s))"
% (
self._datetime_to_generalized_time(date_range[1]),
self._datetime_to_generalized_time(date_range[0]),
)
)
try:
self._ldap_data = self._conn.get_entries(
search_base,
filter=search_filter,
attrs_list=attrs_list,
scope=self._conn.SCOPE_SUBTREE,
)
except errors.EmptyResult:
logger.debug("Empty Result.")
finally:
logger.debug("%d entries found", len(self._ldap_data))
def _parse_ldap_data(self):
"""Fill out self._expiring_password_user_list from data from ldap.
"""
if self._ldap_data:
for entry in self._ldap_data:
self._expiring_password_user_list.add(entry)
# Validate json.
try:
self._pretty_print_data(really_print=False)
except Exception as e:
logger.error("IPA-EPN: Could not create JSON: %s", e)
finally:
self._ldap_data = []
def _pretty_print_data(self, really_print=True):
"""Dump self._expiring_password_user_list to JSON.
"""
self._expiring_password_user_list.json_print(
really_print=really_print
)
def _send_emails(self):
if self._mailer is None:
logger.error("IPA-EPN: mailer was not configured.")
return
else:
try:
template = self.env.get_template("expire_msg.template")
except TemplateSyntaxError as e:
raise RuntimeError("Parsing template %s failed: %s" %
(e.filename, e))
if api.env.mail_from:
mail_from = api.env.mail_from
else:
mail_from = "noreply@%s" % self.default_email_domain
while self._expiring_password_user_list:
entry = self._expiring_password_user_list.pop()
body = template.render(
uid=entry["uid"],
first=entry["givenname"],
last=entry["sn"],
fullname=entry["cn"],
expiration=entry["krbpasswordexpiration"],
)
self._mailer.send_message(
mail_subject=api.env.msg_subject,
mail_body=body,
subscribers=ast.literal_eval(entry["mail"]),
mail_from=mail_from,
)
now = datetime.utcnow()
expdate = datetime.strptime(
entry["krbpasswordexpiration"],
'%Y-%m-%d %H:%M:%S')
logger.debug(
"Notified %s (%s). Password expiring in %d days at %s.",
entry["mail"], entry["uid"], (expdate - now).days,
expdate)
if api.env.smtp_delay:
time.sleep(float(api.env.smtp_delay) / 1000)
self._mailer.cleanup()
def _gentestdata(self):
"""Generate a sample user to process through the template.
"""
expdate = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
entry = dict(
uid=["SAUSER"],
cn=["SAMPLE USER"],
givenname=["SAMPLE"],
sn=["USER"],
krbpasswordexpiration=[expdate],
mail=[api.env.smtp_admin],
)
self._expiring_password_user_list.add(entry)
def _build_cli_date_ranges(self):
"""When self.options.to_nbdays is set, override the date ranges read
from the configuration file and build the date ranges from the CLI
options.
"""
self._date_ranges = []
logger.debug("IPA-EPN: Ignoring configuration file ranges.")
if self.options.from_nbdays is not None:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=int(self.options.from_nbdays),
nbdays_end=int(self.options.to_nbdays),
)
)
elif self.options.to_nbdays is not None:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=None, nbdays_end=int(self.options.to_nbdays)
)
)
class MTAClient:
"""MTA Client class. Originally done for EPN.
"""
def __init__(
self,
security_protocol="none",
smtp_hostname="localhost",
smtp_port=25,
smtp_timeout=60,
smtp_username=None,
smtp_password=None,
):
# We only support "none" (cleartext) for now.
# Future values: "ssl", "starttls"
self._security_protocol = security_protocol
self._smtp_hostname = smtp_hostname
self._smtp_port = smtp_port
self._smtp_timeout = smtp_timeout
self._username = smtp_username
self._password = smtp_password
# This should not be touched
self._conn = None
if (
self._security_protocol == "none"
and "localhost" not in self._smtp_hostname
):
logger.error(
"IPA-EPN: using cleartext for non-localhost SMTPd "
"is not supported."
)
self._connect()
def cleanup(self):
self._disconnect()
def send_message(self, message_str=None, subscribers=None):
result = None
try:
result = self._conn.sendmail(
api.env.smtp_admin, subscribers, message_str,
)
except Exception as e:
logger.info("IPA-EPN: Failed to send mail: %s", e)
finally:
if result:
for key in result:
logger.info(
"IPA-EPN: Failed to send mail to '%s': %s %s",
key,
result[key][0],
result[key][1],
)
logger.info(
"IPA-EPN: Failed to send mail to at least one recipient"
)
def _connect(self):
try:
if self._security_protocol.lower() in ["none", "starttls"]:
self._conn = smtplib.SMTP(
host=self._smtp_hostname,
port=self._smtp_port,
timeout=self._smtp_timeout,
)
else:
self._conn = smtplib.SMTP_SSL(
host=self._smtp_hostname,
port=self._smtp_port,
timeout=self._smtp_timeout,
)
except (socketerror, smtplib.SMTPException) as e:
msg = \
"IPA-EPN: Could not connect to the configured SMTP server: " \
"{host}:{port}: {error}".format(
host=self._smtp_hostname,
port=self._smtp_port,
error=e
)
raise admintool.ScriptError(msg)
try:
self._conn.ehlo()
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: EHLO failed for host %s:%s: %s",
self._smtp_hostname,
self._smtp_port,
e,
)
if (
self._conn.has_extn("STARTTLS")
and self._security_protocol.lower() == "starttls"
):
try:
self._conn.starttls()
self._conn.ehlo()
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: Unable to create an encrypted session to "
"%s:%s: %s",
self._smtp_hostname,
self._smtp_port,
e,
)
if self._username and self._password:
if self._conn.has_extn("AUTH"):
try:
self._conn.login(self._username, self._password)
if self._security_protocol == "none":
logger.warning(
"IPA-EPN: Username and Password "
"were sent in the clear."
)
except smtplib.SMTPAuthenticationError:
raise RuntimeError(
"IPA-EPN: Authentication to %s:%s failed, "
"please check your username and/or password:" %
(self._smtp_hostname,
self._smtp_port,)
)
except smtplib.SMTPException as e:
raise RuntimeError(
"IPA-EPN: SMTP Error at %s:%s:%s" %
(self._smtp_hostname,
self._smtp_port,
e,)
)
else:
err_str = (
"IPA-EPN: Server at %s:%s "
"does not support authentication." %
(self._smtp_hostname,
self._smtp_port,)
)
logger.error(err_str)
def _disconnect(self):
self._conn.quit()
class MailUserAgent:
"""The MUA class for EPN.
"""
def __init__(
self,
security_protocol="none",
smtp_hostname="localhost",
smtp_port=25,
smtp_timeout=60,
smtp_username=None,
smtp_password=None,
x_mailer=None,
msg_subtype="plain",
msg_charset="utf8",
):
self._x_mailer = x_mailer
self._subject = None
self._body = None
self._subscribers = None
self._subtype = msg_subtype
self._charset = msg_charset
self._msg = None
self._message_str = None
self._mta_client = MTAClient(
security_protocol=security_protocol,
smtp_hostname=smtp_hostname,
smtp_port=smtp_port,
smtp_timeout=smtp_timeout,
smtp_username=smtp_username,
smtp_password=smtp_password,
)
def cleanup(self):
self._mta_client.cleanup()
def send_message(
self, mail_subject=None, mail_body=None, subscribers=None,
mail_from=None
):
"""Given mail_subject, mail_body, and subscribers, composes
the message and sends it.
"""
if None in [mail_subject, mail_body, subscribers, mail_from]:
logger.error("IPA-EPN: Tried to send an empty message.")
return False
self._compose_message(
mail_subject=mail_subject,
mail_body=mail_body,
subscribers=subscribers,
mail_from=mail_from,
)
self._mta_client.send_message(
message_str=self._message_str, subscribers=subscribers
)
return True
def _compose_message(
self, mail_subject, mail_body, subscribers, mail_from
):
"""The composer creates a MIME multipart message.
"""
self._subject = mail_subject
self._body = mail_body
self._subscribers = subscribers
self._msg = MIMEMultipart(_charset=self._charset)
self._msg["From"] = formataddr(("IPA-EPN", mail_from))
self._msg["To"] = ", ".join(self._subscribers)
self._msg["Date"] = formatdate(localtime=True)
self._msg["Subject"] = Header(self._subject, self._charset)
self._msg["Message-Id"] = make_msgid()
self._msg.preamble = "Multipart message"
if "X-Mailer" not in self._msg and self._x_mailer:
self._msg.add_header("X-Mailer", self._x_mailer)
self._msg.attach(
MIMEText(
self._body + "\n\n",
_subtype=self._subtype,
_charset=self._charset,
)
)
self._message_str = self._msg.as_string()

View File

@@ -0,0 +1,35 @@
#
# ipachangeconf - configuration file manipulation classes and functions
# partially based on authconfig code
# Copyright (c) 1999-2007 Red Hat, Inc.
# Author: Simo Sorce <ssorce@redhat.com>
#
# 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 warnings
from ipapython.ipachangeconf import IPAChangeConf as realIPAChangeConf
class IPAChangeConf(realIPAChangeConf):
"""Advertise the old name"""
def __init__(self, name):
"""something"""
warnings.warn(
"Use 'ipapython.ipachangeconf.IPAChangeConfg'",
DeprecationWarning,
stacklevel=2
)
super(IPAChangeConf, self).__init__(name)

View File

@@ -0,0 +1,23 @@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
import warnings
from ipaclient.discovery import (
NOT_FQDN, NO_LDAP_SERVER, REALM_NOT_FOUND, NOT_IPA_SERVER,
NO_ACCESS_TO_LDAP, NO_TLS_LDAP, BAD_HOST_CONFIG,
UNKNOWN_ERROR, IPA_BASEDN_INFO, error_names, get_ipa_basedn,
IPADiscovery
)
__all__ = (
'NOT_FQDN', 'NO_LDAP_SERVER', 'REALM_NOT_FOUND',
'NOT_IPA_SERVER', 'NO_ACCESS_TO_LDAP', 'NO_TLS_LDAP',
'BAD_HOST_CONFIG', 'UNKNOWN_ERROR', 'IPA_BASEDN_INFO',
'error_names', 'get_ipa_basedn', 'IPADiscovery')
warnings.warn(
"ipaclient.install.ipadiscovery is deprecated, use ipaclient.discovery",
DeprecationWarning
)

46
ipaclient/install/sssd.py Normal file
View File

@@ -0,0 +1,46 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from ipalib.install import service
from ipalib.install.service import enroll_only
from ipapython.install.core import group, knob
@group
class SSSDInstallInterface(service.ServiceInstallInterface):
description = "SSSD"
fixed_primary = knob(
None,
description="Configure sssd to use fixed server as primary IPA server",
)
fixed_primary = enroll_only(fixed_primary)
permit = knob(
None,
description="disable access rules by default, permit all access.",
)
permit = enroll_only(permit)
enable_dns_updates = knob(
None,
description="Configures the machine to attempt dns updates when the "
"ip address changes.",
)
enable_dns_updates = enroll_only(enable_dns_updates)
no_krb5_offline_passwords = knob(
None,
description="Configure SSSD not to store user password when the "
"server is offline",
)
no_krb5_offline_passwords = enroll_only(no_krb5_offline_passwords)
preserve_sssd = knob(
None,
description="Preserve old SSSD configuration if possible",
)
preserve_sssd = enroll_only(preserve_sssd)
no_sssd = False

View File

@@ -0,0 +1,235 @@
# Authors: Karl MacMillan <kmacmillan@redhat.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/>.
#
from __future__ import absolute_import
import logging
import os
import shutil
from augeas import Augeas
from ipalib import api
from ipapython import ipautil
from ipaplatform.tasks import tasks
from ipaplatform import services
from ipaplatform.paths import paths
from ipapython.ipautil import user_input
logger = logging.getLogger(__name__)
def __backup_config(path, fstore=None):
if fstore:
fstore.backup_file(path)
else:
shutil.copy(path, "%s.ipasave" % (path))
def get_time_source():
"""
While in interactive installation user has to specify NTP server or pool
to be used in chrony configuration. This method asks user input on these
values in case that they were not specified before installation start.
"""
ntp_servers = []
ntp_pool = ""
if ipautil.user_input("Do you want to configure chrony "
"with NTP server or pool address?", False):
servers = user_input("Enter NTP source server addresses separated by "
"comma, or press Enter to skip", allow_empty=True)
if servers: # if user input is not '' (empty)
logger.debug("User provided NTP server(s):")
# cut possible multiple servers separated by comma into list
for server in servers.split(","):
# users tend to separate servers by ", " so strip() whitespaces
server = server.strip()
ntp_servers.append(server)
logger.debug("\t%s", server)
ntp_pool = user_input("Enter a NTP source pool address, "
"or press Enter to skip", allow_empty=True)
if ntp_pool: # if user input is not '' (empty)
logger.debug("User provided NTP pool:\t%s", ntp_pool)
return ntp_servers, ntp_pool
def sync_chrony():
"""
This method enables chronyd service on boot and restarts it to reload
chrony configuration file /etc/chrony.conf
Then it tries to synchronize time with chrony's new or defaut configuration
"""
# Set the chronyd to start on boot
services.knownservices.chronyd.enable()
# Restart chronyd
services.knownservices.chronyd.restart()
sync_attempt_count = 3
# chrony attempt count to sync with configiured servers
# each next attempt is tried after 10seconds of timeot
# 3 attempts means: if first immidiate attempt fails
# there is 10s delay between next attempts
args = [paths.CHRONYC, 'waitsync', str(sync_attempt_count), '-d']
try:
logger.info('Attempting to sync time with chronyc.')
ipautil.run(args)
logger.info('Time synchronization was successful.')
return True
except ipautil.CalledProcessError:
logger.warning('Process chronyc waitsync failed to sync time!')
logger.warning(
"Unable to sync time with chrony server, assuming the time "
"is in sync. Please check that 123 UDP port is opened, "
"and any time server is on network.")
return False
def configure_chrony(ntp_servers, ntp_pool=None,
fstore=None, sysstore=None, debug=False):
"""
This method only configures chrony client with ntp_servers or ntp_pool
"""
module = "chrony"
if sysstore:
sysstore.backup_state(module, "enabled",
services.knownservices.chronyd.is_enabled())
aug = Augeas(flags=Augeas.NO_LOAD | Augeas.NO_MODL_AUTOLOAD,
loadpath=paths.USR_SHARE_IPA_DIR)
try:
logger.debug("Configuring chrony")
chrony_conf = os.path.abspath(paths.CHRONY_CONF)
aug.transform(module, chrony_conf) # loads chrony lens file
aug.load() # loads augeas tree
# augeas needs to prepend path with '/files'
path = '/files{path}'.format(path=chrony_conf)
# remove possible conflicting configuration of servers
aug.remove('{}/server'.format(path))
aug.remove('{}/pool'.format(path))
aug.remove('{}/peer'.format(path))
if ntp_pool:
logger.debug("Setting server pool:")
logger.debug("'%s'", ntp_pool)
aug.set('{}/pool[last()+1]'.format(path), ntp_pool)
aug.set('{}/pool[last()]/iburst'.format(path), None)
if ntp_servers:
logger.debug("Setting time servers:")
for server in ntp_servers:
aug.set('{}/server[last()+1]'.format(path), server)
aug.set('{}/server[last()]/iburst'.format(path), None)
logger.debug("'%s'", server)
# backup oginal conf file
logger.debug("Backing up '%s'", chrony_conf)
__backup_config(chrony_conf, fstore)
logger.debug("Writing configuration to '%s'", chrony_conf)
aug.save()
logger.info('Configuration of chrony was changed by installer.')
configured = True
except IOError:
logger.error("Augeas failed to configure file %s", chrony_conf)
configured = False
except RuntimeError as e:
logger.error("Configuration failed with: %s", e)
configured = False
finally:
aug.close()
tasks.restore_context(chrony_conf)
return configured
class NTPConfigurationError(Exception):
pass
class NTPConflictingService(NTPConfigurationError):
def __init__(self, message='', conflicting_service=None):
super(NTPConflictingService, self).__init__(self, message)
self.conflicting_service = conflicting_service
def check_timedate_services():
"""
System may contain conflicting services used for time&date synchronization.
As IPA server/client supports only chronyd, make sure that other services
are not enabled to prevent conflicts.
"""
for service in services.timedate_services:
if service == 'chronyd':
continue
# Make sure that the service is not enabled
instance = services.service(service, api)
if instance.is_enabled() or instance.is_running():
raise NTPConflictingService(
conflicting_service=instance.service_name)
def force_chrony(statestore):
"""
Force chronyd configuration and disable and stop any other conflicting
time&date service
"""
for service in services.timedate_services:
if service == 'chronyd':
continue
instance = services.service(service, api)
enabled = instance.is_enabled()
running = instance.is_running()
if enabled or running:
statestore.backup_state(instance.service_name, 'enabled', enabled)
statestore.backup_state(instance.service_name, 'running', running)
if running:
instance.stop()
if enabled:
instance.disable()
def restore_forced_timeservices(statestore, skip_service='chronyd'):
"""
Restore from installation and enable/start service that
were disabled/stopped during installation
"""
for service in services.timedate_services:
if service == skip_service:
continue
if statestore.has_state(service):
instance = services.service(service, api)
enabled = statestore.restore_state(instance.service_name,
'enabled')
running = statestore.restore_state(instance.service_name,
'running')
if enabled:
instance.enable()
if running:
instance.start()