freeipa/ipaserver/install/service.py
Petr Viktorin f866186239 ipaserver.install.service: Don't show error message on SystemExit(0)
Additional fix for: https://fedorahosted.org/freeipa/ticket/4499

Reviewed-By: Martin Kosek <mkosek@redhat.com>
2014-09-26 16:55:54 +02:00

517 lines
18 KiB
Python

# 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, socket
import tempfile
import time
import datetime
import traceback
from ipapython import sysrestore, ipautil, dogtag, ipaldap
from ipapython.dn import DN
from ipapython.ipa_log_manager import *
from ipalib import errors, certstore
from ipaplatform import services
from ipaplatform.paths import paths
# The service name as stored in cn=masters,cn=ipa,cn=etc. In the tuple
# the first value is the *nix service name, the second the start order.
SERVICE_LIST = {
'KDC': ('krb5kdc', 10),
'KPASSWD': ('kadmin', 20),
'DNS': ('named', 30),
'MEMCACHE': ('ipa_memcached', 39),
'HTTP': ('httpd', 40),
'CA': ('%sd' % dogtag.configured_constants().PKI_INSTANCE_NAME, 50),
'ADTRUST': ('smb', 60),
'EXTID': ('winbind', 70),
'OTPD': ('ipa-otpd', 80),
}
def print_msg(message, output_fd=sys.stdout):
root_logger.debug(message)
output_fd.write(message)
output_fd.write("\n")
output_fd.flush()
def format_seconds(seconds):
"""Format a number of seconds as an English minutes+seconds message"""
parts = []
minutes, seconds = divmod(seconds, 60)
if minutes:
parts.append('%d minute' % minutes)
if minutes != 1:
parts[-1] += 's'
if seconds or not minutes:
parts.append('%d second' % seconds)
if seconds != 1:
parts[-1] += 's'
return ' '.join(parts)
class Service(object):
def __init__(self, service_name, service_desc=None, sstore=None, dm_password=None, ldapi=True,
autobind=ipaldap.AUTOBIND_AUTO):
self.service_name = service_name
self.service_desc = service_desc
self.service = services.service(service_name)
self.steps = []
self.output_fd = sys.stdout
self.dm_password = dm_password
self.ldapi = ldapi
self.autobind = autobind
self.fqdn = socket.gethostname()
self.admin_conn = None
if sstore:
self.sstore = sstore
else:
self.sstore = sysrestore.StateFile(paths.SYSRESTORE)
self.realm = None
self.suffix = DN()
self.principal = None
self.dercert = None
def ldap_connect(self):
# If DM password is provided, we use it
# If autobind was requested, attempt autobind when root and ldapi
# If autobind was disabled or not succeeded, go with GSSAPI
# LDAPI can be used with either autobind or GSSAPI
# LDAPI requires realm to be set
try:
if self.ldapi:
if not self.realm:
raise errors.NotFound(reason="realm is missing for %s" % (self))
conn = ipaldap.IPAdmin(ldapi=self.ldapi, realm=self.realm)
else:
conn = ipaldap.IPAdmin(self.fqdn, port=389)
conn.do_bind(self.dm_password, autobind=self.autobind)
except Exception, e:
root_logger.debug("Could not connect to the Directory Server on %s: %s" % (self.fqdn, str(e)))
raise
self.admin_conn = conn
def ldap_disconnect(self):
self.admin_conn.unbind()
self.admin_conn = None
def _ldap_mod(self, ldif, sub_dict=None):
pw_name = None
fd = None
path = ipautil.SHARE_DIR + ldif
nologlist = []
if sub_dict is not None:
txt = ipautil.template_file(path, sub_dict)
fd = ipautil.write_tmp_file(txt)
path = fd.name
# do not log passwords
if 'PASSWORD' in sub_dict:
nologlist.append(sub_dict['PASSWORD'])
if 'RANDOM_PASSWORD' in sub_dict:
nologlist.append(sub_dict['RANDOM_PASSWORD'])
args = [paths.LDAPMODIFY, "-v", "-f", path]
# As we always connect to the local host,
# use URI of admin connection
if not self.admin_conn:
self.ldap_connect()
args += ["-H", self.admin_conn.ldap_uri]
# If DM password is available, use it
if self.dm_password:
[pw_fd, pw_name] = tempfile.mkstemp()
os.write(pw_fd, self.dm_password)
os.close(pw_fd)
auth_parms = ["-x", "-D", "cn=Directory Manager", "-y", pw_name]
# Use GSSAPI auth when not using DM password or not being root
elif os.getegid() != 0:
auth_parms = ["-Y", "GSSAPI"]
# Default to EXTERNAL auth mechanism
else:
auth_parms = ["-Y", "EXTERNAL"]
args += auth_parms
try:
try:
ipautil.run(args, nolog=nologlist)
except ipautil.CalledProcessError, e:
root_logger.critical("Failed to load %s: %s" % (ldif, str(e)))
finally:
if pw_name:
os.remove(pw_name)
if fd is not None:
fd.close()
def move_service(self, principal):
"""
Used to move a principal entry created by kadmin.local from
cn=kerberos to cn=services
"""
dn = DN(('krbprincipalname', principal), ('cn', self.realm), ('cn', 'kerberos'), self.suffix)
try:
entry = self.admin_conn.get_entry(dn)
except errors.NotFound:
# There is no service in the wrong location, nothing to do.
# This can happen when installing a replica
return None
newdn = DN(('krbprincipalname', principal), ('cn', 'services'), ('cn', 'accounts'), self.suffix)
hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix)
self.admin_conn.delete_entry(entry)
entry.dn = newdn
classes = entry.get("objectclass")
classes = classes + ["ipaobject", "ipaservice", "pkiuser"]
entry["objectclass"] = list(set(classes))
entry["ipauniqueid"] = ['autogenerate']
entry["managedby"] = [hostdn]
self.admin_conn.add_entry(entry)
return newdn
def add_simple_service(self, principal):
"""
Add a very basic IPA service.
The principal needs to be fully-formed: service/host@REALM
"""
if not self.admin_conn:
self.ldap_connect()
dn = DN(('krbprincipalname', principal), ('cn', 'services'), ('cn', 'accounts'), self.suffix)
hostdn = DN(('fqdn', self.fqdn), ('cn', 'computers'), ('cn', 'accounts'), self.suffix)
entry = self.admin_conn.make_entry(
dn,
objectclass=[
"krbprincipal", "krbprincipalaux", "krbticketpolicyaux",
"ipaobject", "ipaservice", "pkiuser"],
krbprincipalname=[principal],
ipauniqueid=['autogenerate'],
managedby=[hostdn],
)
self.admin_conn.add_entry(entry)
return dn
def add_cert_to_service(self):
"""
Add a certificate to a service
This server cert should be in DER format.
"""
# add_cert_to_service() is relatively rare operation
# we actually call it twice during ipa-server-install, for different
# instances: ds and cs. Unfortunately, it may happen that admin
# connection was created well before add_cert_to_service() is called
# If there are other operations in between, it will become stale and
# since we are using SimpleLDAPObject, not ReconnectLDAPObject, the
# action will fail. Thus, explicitly disconnect and connect again.
# Using ReconnectLDAPObject instead of SimpleLDAPObject was considered
# but consequences for other parts of the framework are largely
# unknown.
if self.admin_conn:
self.ldap_disconnect()
self.ldap_connect()
dn = DN(('krbprincipalname', self.principal), ('cn', 'services'),
('cn', 'accounts'), self.suffix)
entry = self.admin_conn.get_entry(dn)
entry.setdefault('userCertificate', []).append(self.dercert)
try:
self.admin_conn.update_entry(entry)
except Exception, e:
root_logger.critical("Could not add certificate to service %s entry: %s" % (self.principal, str(e)))
def import_ca_certs(self, db, ca_is_configured, conn=None):
if conn is None:
if not self.admin_conn:
self.ldap_connect()
conn = self.admin_conn
try:
ca_certs = certstore.get_ca_certs_nss(
conn, self.suffix, self.realm, ca_is_configured)
except errors.NotFound:
pass
else:
for cert, nickname, trust_flags in ca_certs:
db.add_cert(cert, nickname, trust_flags)
def is_configured(self):
return self.sstore.has_state(self.service_name)
def set_output(self, fd):
self.output_fd = fd
def stop(self, instance_name="", capture_output=True):
self.service.stop(instance_name, capture_output=capture_output)
def start(self, instance_name="", capture_output=True, wait=True):
self.service.start(instance_name, capture_output=capture_output, wait=wait)
def restart(self, instance_name="", capture_output=True, wait=True):
self.service.restart(instance_name, capture_output=capture_output, wait=wait)
def is_running(self):
return self.service.is_running()
def install(self):
self.service.install()
def remove(self):
self.service.remove()
def enable(self):
self.service.enable()
def disable(self):
self.service.disable()
def is_enabled(self):
return self.service.is_enabled()
def backup_state(self, key, value):
self.sstore.backup_state(self.service_name, key, value)
def restore_state(self, key):
return self.sstore.restore_state(self.service_name, key)
def get_state(self, key):
return self.sstore.get_state(self.service_name, key)
def print_msg(self, message):
print_msg(message, self.output_fd)
def step(self, message, method, run_after_failure=False):
self.steps.append((message, method, run_after_failure))
def start_creation(self, start_message=None, end_message=None,
show_service_name=True, runtime=-1):
"""
Starts creation of the service.
Use start_message and end_message for explicit messages
at the beggining / end of the process. Otherwise they are generated
using the service description (or service name, if the description has
not been provided).
Use show_service_name to include service name in generated descriptions.
"""
if start_message is None:
# no other info than mandatory service_name provided, use that
if self.service_desc is None:
start_message = "Configuring %s" % self.service_name
# description should be more accurate than service name
else:
start_message = "Configuring %s" % self.service_desc
if show_service_name:
start_message = "%s (%s)" % (start_message, self.service_name)
if end_message is None:
if self.service_desc is None:
if show_service_name:
end_message = "Done configuring %s." % self.service_name
else:
end_message = "Done."
else:
if show_service_name:
end_message = "Done configuring %s (%s)." % (
self.service_desc, self.service_name)
else:
end_message = "Done configuring %s." % self.service_desc
if runtime > 0:
self.print_msg('%s: Estimated time %s' % (start_message,
format_seconds(runtime)))
else:
self.print_msg(start_message)
def run_step(message, method):
self.print_msg(message)
s = datetime.datetime.now()
method()
e = datetime.datetime.now()
d = e - s
root_logger.debug(" duration: %d seconds" % d.seconds)
step = 0
steps_iter = iter(self.steps)
try:
for message, method, run_after_failure in steps_iter:
full_msg = " [%d/%d]: %s" % (step+1, len(self.steps), message)
run_step(full_msg, method)
step += 1
except BaseException as e:
if not (isinstance(e, SystemExit) and e.code == 0):
# show the traceback, so it's not lost if cleanup method fails
root_logger.debug("%s" % traceback.format_exc())
self.print_msg(' [error] %s: %s' % (type(e).__name__, e))
# run through remaining methods marked run_after_failure
for message, method, run_after_failure in steps_iter:
if run_after_failure:
run_step(" [cleanup]: %s" % message, method)
raise
self.print_msg(end_message)
self.steps = []
def ldap_enable(self, name, fqdn, dm_password, ldap_suffix, config=[]):
assert isinstance(ldap_suffix, DN)
self.disable()
if not self.admin_conn:
self.ldap_connect()
entry_name = DN(('cn', name), ('cn', fqdn), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), ldap_suffix)
# enable disabled service
try:
entry = self.admin_conn.get_entry(entry_name, ['ipaConfigString'])
except errors.NotFound:
pass
else:
if any(u'enabledservice' == val.lower()
for val in entry.get('ipaConfigString', [])):
root_logger.debug("service %s startup entry already enabled", name)
return
entry.setdefault('ipaConfigString', []).append(u'enabledService')
try:
self.admin_conn.update_entry(entry)
except errors.EmptyModlist:
root_logger.debug("service %s startup entry already enabled", name)
return
except:
root_logger.debug("failed to enable service %s startup entry", name)
raise
root_logger.debug("service %s startup entry enabled", name)
return
order = SERVICE_LIST[name][1]
entry = self.admin_conn.make_entry(
entry_name,
objectclass=["nsContainer", "ipaConfigObject"],
cn=[name],
ipaconfigstring=[
"enabledService", "startOrder " + str(order)] + config,
)
try:
self.admin_conn.add_entry(entry)
except (errors.DuplicateEntry), e:
root_logger.debug("failed to add service %s startup entry", name)
raise e
def ldap_disable(self, name, fqdn, ldap_suffix):
assert isinstance(ldap_suffix, DN)
if not self.admin_conn:
self.ldap_connect()
entry_dn = DN(('cn', name), ('cn', fqdn), ('cn', 'masters'),
('cn', 'ipa'), ('cn', 'etc'), ldap_suffix)
search_kw = {'ipaConfigString': u'enabledService'}
filter = self.admin_conn.make_filter(search_kw)
try:
entries, truncated = self.admin_conn.find_entries(
filter=filter,
attrs_list=['ipaConfigString'],
base_dn=entry_dn,
scope=self.admin_conn.SCOPE_BASE)
except errors.NotFound:
root_logger.debug("service %s startup entry already disabled", name)
return
assert len(entries) == 1 # only one entry is expected
entry = entries[0]
# case insensitive
for value in entry.get('ipaConfigString', []):
if value.lower() == u'enabledservice':
entry['ipaConfigString'].remove(value)
break
try:
self.admin_conn.update_entry(entry)
except errors.EmptyModlist:
pass
except:
root_logger.debug("failed to disable service %s startup entry", name)
raise
root_logger.debug("service %s startup entry disabled", name)
class SimpleServiceInstance(Service):
def create_instance(self, gensvc_name=None, fqdn=None, dm_password=None, ldap_suffix=None, realm=None):
self.gensvc_name = gensvc_name
self.fqdn = fqdn
self.dm_password = dm_password
self.suffix = ldap_suffix
self.realm = realm
if not realm:
self.ldapi = False
self.step("starting %s " % self.service_name, self.__start)
self.step("configuring %s to start on boot" % self.service_name, self.__enable)
self.start_creation("Configuring %s" % self.service_name)
suffix = ipautil.dn_attribute_property('_ldap_suffix')
def __start(self):
self.backup_state("running", self.is_running())
self.restart()
def __enable(self):
self.enable()
self.backup_state("enabled", self.is_enabled())
if self.gensvc_name == None:
self.enable()
else:
self.ldap_enable(self.gensvc_name, self.fqdn,
self.dm_password, self.suffix)
def uninstall(self):
if self.is_configured():
self.print_msg("Unconfiguring %s" % self.service_name)
running = self.restore_state("running")
enabled = not self.restore_state("enabled")
if not running is None and not running:
self.stop()
if not enabled is None and not enabled:
self.disable()
self.remove()