Imported Upstream version 4.0.5
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
@@ -1,680 +0,0 @@
|
||||
# Authors: Rob Crittenden <rcritten@redhat.com>
|
||||
# David Kupka <dkupka@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2010 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/>.
|
||||
#
|
||||
|
||||
# Some certmonger functions, mostly around updating the request file.
|
||||
# This is used so we can add tracking to the Apache and 389-ds
|
||||
# server certificates created during the IPA server installation.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import dbus
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
from ipalib import api
|
||||
from ipalib.constants import CA_DBUS_TIMEOUT
|
||||
from ipapython.dn import DN
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform import services
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DBUS_CM_PATH = '/org/fedorahosted/certmonger'
|
||||
DBUS_CM_IF = 'org.fedorahosted.certmonger'
|
||||
DBUS_CM_NAME = 'org.fedorahosted.certmonger'
|
||||
DBUS_CM_REQUEST_IF = 'org.fedorahosted.certmonger.request'
|
||||
DBUS_CM_CA_IF = 'org.fedorahosted.certmonger.ca'
|
||||
DBUS_PROPERTY_IF = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
|
||||
class _cm_dbus_object(object):
|
||||
"""
|
||||
Auxiliary class for convenient DBus object handling.
|
||||
"""
|
||||
def __init__(self, bus, parent, object_path, object_dbus_interface,
|
||||
parent_dbus_interface=None, property_interface=False):
|
||||
"""
|
||||
bus - DBus bus object, result of dbus.SystemBus() or dbus.SessionBus()
|
||||
Object is accesible over this DBus bus instance.
|
||||
object_path - path to requested object on DBus bus
|
||||
object_dbus_interface
|
||||
parent_dbus_interface
|
||||
property_interface - create DBus property interface? True or False
|
||||
"""
|
||||
if bus is None or object_path is None or object_dbus_interface is None:
|
||||
raise RuntimeError(
|
||||
"bus, object_path and dbus_interface must not be None.")
|
||||
if parent_dbus_interface is None:
|
||||
parent_dbus_interface = object_dbus_interface
|
||||
self.bus = bus
|
||||
self.parent = parent
|
||||
self.path = object_path
|
||||
self.obj_dbus_if = object_dbus_interface
|
||||
self.parent_dbus_if = parent_dbus_interface
|
||||
self.obj = bus.get_object(parent_dbus_interface, object_path)
|
||||
self.obj_if = dbus.Interface(self.obj, object_dbus_interface)
|
||||
if property_interface:
|
||||
self.prop_if = dbus.Interface(self.obj, DBUS_PROPERTY_IF)
|
||||
|
||||
|
||||
class _certmonger(_cm_dbus_object):
|
||||
"""
|
||||
Create a connection to certmonger.
|
||||
By default use SystemBus. When not available use private connection
|
||||
over Unix socket.
|
||||
This solution is really ugly and should be removed as soon as DBus
|
||||
SystemBus is available at system install time.
|
||||
"""
|
||||
timeout = 300
|
||||
|
||||
def _start_private_conn(self):
|
||||
sock_filename = os.path.join(tempfile.mkdtemp(), 'certmonger')
|
||||
self._proc = subprocess.Popen([paths.CERTMONGER, '-n', '-L', '-P',
|
||||
sock_filename])
|
||||
for _t in range(0, self.timeout, 5):
|
||||
if os.path.exists(sock_filename):
|
||||
return "unix:path=%s" % sock_filename
|
||||
time.sleep(5)
|
||||
self._stop_private_conn()
|
||||
raise RuntimeError("Failed to start certmonger: Timed out")
|
||||
|
||||
def _stop_private_conn(self):
|
||||
if self._proc:
|
||||
retcode = self._proc.poll()
|
||||
if retcode is not None:
|
||||
return
|
||||
self._proc.terminate()
|
||||
for _t in range(0, self.timeout, 5):
|
||||
retcode = self._proc.poll()
|
||||
if retcode is not None:
|
||||
return
|
||||
time.sleep(5)
|
||||
logger.error("Failed to stop certmonger.")
|
||||
|
||||
def __del__(self):
|
||||
self._stop_private_conn()
|
||||
|
||||
def __init__(self):
|
||||
self._proc = None
|
||||
self._bus = None
|
||||
try:
|
||||
self._bus = dbus.SystemBus()
|
||||
except dbus.DBusException as e:
|
||||
err_name = e.get_dbus_name()
|
||||
if err_name not in ['org.freedesktop.DBus.Error.NoServer',
|
||||
'org.freedesktop.DBus.Error.FileNotFound']:
|
||||
logger.error("Failed to connect to certmonger over "
|
||||
"SystemBus: %s", e)
|
||||
raise
|
||||
try:
|
||||
self._private_sock = self._start_private_conn()
|
||||
self._bus = dbus.connection.Connection(self._private_sock)
|
||||
except dbus.DBusException as e:
|
||||
logger.error("Failed to connect to certmonger over "
|
||||
"private socket: %s", e)
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self._bus.get_name_owner(DBUS_CM_NAME)
|
||||
except dbus.DBusException:
|
||||
try:
|
||||
services.knownservices.certmonger.start()
|
||||
except Exception as e:
|
||||
logger.error("Failed to start certmonger: %s", e)
|
||||
raise
|
||||
|
||||
for _t in range(0, self.timeout, 5):
|
||||
try:
|
||||
self._bus.get_name_owner(DBUS_CM_NAME)
|
||||
break
|
||||
except dbus.DBusException:
|
||||
pass
|
||||
time.sleep(5)
|
||||
raise RuntimeError('Failed to start certmonger')
|
||||
|
||||
super(_certmonger, self).__init__(self._bus, None, DBUS_CM_PATH,
|
||||
DBUS_CM_IF)
|
||||
|
||||
|
||||
def _get_requests(criteria=dict()):
|
||||
"""
|
||||
Get all requests that matches the provided criteria.
|
||||
"""
|
||||
if not isinstance(criteria, dict):
|
||||
raise TypeError('"criteria" must be dict.')
|
||||
|
||||
cm = _certmonger()
|
||||
requests = []
|
||||
requests_paths = []
|
||||
if 'nickname' in criteria:
|
||||
request_path = cm.obj_if.find_request_by_nickname(criteria['nickname'])
|
||||
if request_path:
|
||||
requests_paths = [request_path]
|
||||
else:
|
||||
requests_paths = cm.obj_if.get_requests()
|
||||
|
||||
for request_path in requests_paths:
|
||||
request = _cm_dbus_object(cm.bus, cm, request_path, DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
for criterion in criteria:
|
||||
if criterion == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
ca = _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
value = ca.obj_if.get_nickname()
|
||||
else:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if value != criteria[criterion]:
|
||||
break
|
||||
else:
|
||||
requests.append(request)
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
def _get_request(criteria):
|
||||
"""
|
||||
Find request that matches criteria.
|
||||
If 'nickname' is specified other criteria are ignored because 'nickname'
|
||||
uniquely identify single request.
|
||||
When multiple or none request matches specified criteria RuntimeError is
|
||||
raised.
|
||||
"""
|
||||
requests = _get_requests(criteria)
|
||||
if len(requests) == 0:
|
||||
return None
|
||||
elif len(requests) == 1:
|
||||
return requests[0]
|
||||
else:
|
||||
raise RuntimeError("Criteria expected to be met by 1 request, got %s."
|
||||
% len(requests))
|
||||
|
||||
|
||||
def get_request_value(request_id, directive):
|
||||
"""
|
||||
Get property of request.
|
||||
"""
|
||||
try:
|
||||
request = _get_request(dict(nickname=request_id))
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
if directive == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
ca = _cm_dbus_object(request.bus, request, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
return ca.obj_if.get_nickname()
|
||||
else:
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, directive)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_request_id(criteria):
|
||||
"""
|
||||
If you don't know the certmonger request_id then try to find it by looking
|
||||
through all the requests.
|
||||
|
||||
criteria is a tuple of key/value to search for. The more specific
|
||||
the better. An error is raised if multiple request_ids are returned for
|
||||
the same criteria.
|
||||
|
||||
None is returned if none of the criteria match.
|
||||
"""
|
||||
try:
|
||||
request = _get_request(criteria)
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_requests_for_dir(dir):
|
||||
"""
|
||||
Return a list containing the request ids for a given NSS database
|
||||
directory.
|
||||
"""
|
||||
reqid = []
|
||||
criteria = {'cert-storage': 'NSSDB', 'key-storage': 'NSSDB',
|
||||
'cert-database': dir, 'key-database': dir, }
|
||||
requests = _get_requests(criteria)
|
||||
for request in requests:
|
||||
reqid.append(request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname'))
|
||||
|
||||
return reqid
|
||||
|
||||
|
||||
def add_request_value(request_id, directive, value):
|
||||
"""
|
||||
Add a new directive to a certmonger request file.
|
||||
"""
|
||||
try:
|
||||
request = _get_request({'nickname': request_id})
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
request.obj_if.modify({directive: value})
|
||||
|
||||
|
||||
def add_principal(request_id, principal):
|
||||
"""
|
||||
In order for a certmonger request to be renewable it needs a principal.
|
||||
|
||||
When an existing certificate is added via start-tracking it won't have
|
||||
a principal.
|
||||
"""
|
||||
add_request_value(request_id, 'template-principal', [principal])
|
||||
|
||||
|
||||
def add_subject(request_id, subject):
|
||||
"""
|
||||
In order for a certmonger request to be renwable it needs the subject
|
||||
set in the request file.
|
||||
|
||||
When an existing certificate is added via start-tracking it won't have
|
||||
a subject_template set.
|
||||
"""
|
||||
add_request_value(request_id, 'template-subject', subject)
|
||||
|
||||
|
||||
def request_and_wait_for_cert(
|
||||
certpath, subject, principal, nickname=None, passwd_fname=None,
|
||||
dns=None, ca='IPA', profile=None,
|
||||
pre_command=None, post_command=None, storage='NSSDB', perms=None):
|
||||
"""
|
||||
Execute certmonger to request a server certificate.
|
||||
|
||||
The method also waits for the certificate to be available.
|
||||
"""
|
||||
reqId = request_cert(certpath, subject, principal, nickname,
|
||||
passwd_fname, dns, ca, profile,
|
||||
pre_command, post_command, storage, perms)
|
||||
state = wait_for_request(reqId, api.env.startup_timeout)
|
||||
ca_error = get_request_value(reqId, 'ca-error')
|
||||
if state != 'MONITORING' or ca_error:
|
||||
raise RuntimeError("Certificate issuance failed ({})".format(state))
|
||||
return reqId
|
||||
|
||||
|
||||
def request_cert(
|
||||
certpath, subject, principal, nickname=None, passwd_fname=None,
|
||||
dns=None, ca='IPA', profile=None,
|
||||
pre_command=None, post_command=None, storage='NSSDB', perms=None):
|
||||
"""
|
||||
Execute certmonger to request a server certificate.
|
||||
|
||||
``dns``
|
||||
A sequence of DNS names to appear in SAN request extension.
|
||||
``perms``
|
||||
A tuple of (cert, key) permissions in e.g., (0644,0660)
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
certfile, keyfile = certpath
|
||||
# This is a workaround for certmonger having different Subject
|
||||
# representation with NSS and OpenSSL
|
||||
# https://pagure.io/certmonger/issue/62
|
||||
subject = str(DN(*reversed(DN(subject))))
|
||||
else:
|
||||
certfile = certpath
|
||||
keyfile = certpath
|
||||
|
||||
cm = _certmonger()
|
||||
ca_path = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if not ca_path:
|
||||
raise RuntimeError('{} CA not found'.format(ca))
|
||||
request_parameters = dict(KEY_STORAGE=storage, CERT_STORAGE=storage,
|
||||
CERT_LOCATION=certfile, KEY_LOCATION=keyfile,
|
||||
SUBJECT=subject, CA=ca_path)
|
||||
if nickname:
|
||||
request_parameters["CERT_NICKNAME"] = nickname
|
||||
request_parameters["KEY_NICKNAME"] = nickname
|
||||
if principal:
|
||||
request_parameters['PRINCIPAL'] = [principal]
|
||||
if dns is not None and len(dns) > 0:
|
||||
request_parameters['DNS'] = dns
|
||||
if passwd_fname:
|
||||
request_parameters['KEY_PIN_FILE'] = passwd_fname
|
||||
if profile:
|
||||
request_parameters['ca-profile'] = profile
|
||||
|
||||
certmonger_cmd_template = paths.CERTMONGER_COMMAND_TEMPLATE
|
||||
if pre_command:
|
||||
if not os.path.isabs(pre_command):
|
||||
pre_command = certmonger_cmd_template % (pre_command)
|
||||
request_parameters['cert-presave-command'] = pre_command
|
||||
if post_command:
|
||||
if not os.path.isabs(post_command):
|
||||
post_command = certmonger_cmd_template % (post_command)
|
||||
request_parameters['cert-postsave-command'] = post_command
|
||||
|
||||
if perms:
|
||||
request_parameters['cert-perms'] = perms[0]
|
||||
request_parameters['key-perms'] = perms[1]
|
||||
|
||||
result = cm.obj_if.add_request(request_parameters)
|
||||
try:
|
||||
if result[0]:
|
||||
request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
else:
|
||||
raise RuntimeError('add_request() returned False')
|
||||
except Exception as e:
|
||||
logger.error('Failed to create a new request: %s', e)
|
||||
raise
|
||||
return request.obj_if.get_nickname()
|
||||
|
||||
|
||||
def start_tracking(
|
||||
certpath, ca='IPA', nickname=None, pin=None, pinfile=None,
|
||||
pre_command=None, post_command=None, profile=None, storage="NSSDB"):
|
||||
"""
|
||||
Tell certmonger to track the given certificate in either a file or an NSS
|
||||
database. The certificate access can be protected by a password_file.
|
||||
|
||||
This uses the generic certmonger command getcert so we can specify
|
||||
a different helper.
|
||||
|
||||
:param certpath:
|
||||
The path to an NSS database or a tuple (PEM certificate, private key).
|
||||
:param ca:
|
||||
Nickanme of the CA for which the given certificate should be tracked.
|
||||
:param nickname:
|
||||
Nickname of the NSS certificate in ``certpath`` to be tracked.
|
||||
:param pin:
|
||||
The passphrase for either NSS database containing ``nickname`` or
|
||||
for the encrypted key in the ``certpath`` tuple.
|
||||
:param pinfile:
|
||||
Similar to ``pin`` parameter except this is a path to a file containing
|
||||
the required passphrase.
|
||||
:param pre_command:
|
||||
Specifies a command for certmonger to run before it renews a
|
||||
certificate. This command must reside in /usr/lib/ipa/certmonger
|
||||
to work with SELinux.
|
||||
:param post_command:
|
||||
Specifies a command for certmonger to run after it has renewed a
|
||||
certificate. This command must reside in /usr/lib/ipa/certmonger
|
||||
to work with SELinux.
|
||||
:param storage:
|
||||
One of "NSSDB" or "FILE", describes whether certmonger should use
|
||||
NSS or OpenSSL backend to track the certificate in ``certpath``
|
||||
:param profile:
|
||||
Which certificate profile should be used.
|
||||
:returns: certificate tracking nickname.
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
certfile, keyfile = certpath
|
||||
else:
|
||||
certfile = certpath
|
||||
keyfile = certpath
|
||||
|
||||
cm = _certmonger()
|
||||
certmonger_cmd_template = paths.CERTMONGER_COMMAND_TEMPLATE
|
||||
|
||||
ca_path = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if not ca_path:
|
||||
raise RuntimeError('{} CA not found'.format(ca))
|
||||
|
||||
params = {
|
||||
'TRACK': True,
|
||||
'CERT_STORAGE': storage,
|
||||
'KEY_STORAGE': storage,
|
||||
'CERT_LOCATION': certfile,
|
||||
'KEY_LOCATION': keyfile,
|
||||
'CA': ca_path
|
||||
}
|
||||
if nickname:
|
||||
params['CERT_NICKNAME'] = nickname
|
||||
params['KEY_NICKNAME'] = nickname
|
||||
if pin:
|
||||
params['KEY_PIN'] = pin
|
||||
if pinfile:
|
||||
params['KEY_PIN_FILE'] = os.path.abspath(pinfile)
|
||||
if pre_command:
|
||||
if not os.path.isabs(pre_command):
|
||||
pre_command = certmonger_cmd_template % (pre_command)
|
||||
params['cert-presave-command'] = pre_command
|
||||
if post_command:
|
||||
if not os.path.isabs(post_command):
|
||||
post_command = certmonger_cmd_template % (post_command)
|
||||
params['cert-postsave-command'] = post_command
|
||||
if profile:
|
||||
params['ca-profile'] = profile
|
||||
|
||||
result = cm.obj_if.add_request(params)
|
||||
try:
|
||||
if result[0]:
|
||||
request = _cm_dbus_object(cm.bus, cm, result[1], DBUS_CM_REQUEST_IF,
|
||||
DBUS_CM_IF, True)
|
||||
else:
|
||||
raise RuntimeError('add_request() returned False')
|
||||
except Exception as e:
|
||||
logger.error('Failed to add new request: %s', e)
|
||||
raise
|
||||
return request.prop_if.Get(DBUS_CM_REQUEST_IF, 'nickname')
|
||||
|
||||
|
||||
def stop_tracking(secdir=None, request_id=None, nickname=None, certfile=None):
|
||||
"""
|
||||
Stop tracking the current request using either the request_id or nickname.
|
||||
|
||||
Returns True or False
|
||||
"""
|
||||
if request_id is None and nickname is None and certfile is None:
|
||||
raise RuntimeError('One of request_id, nickname and certfile is'
|
||||
' required.')
|
||||
if secdir is not None and certfile is not None:
|
||||
raise RuntimeError("Can't specify both secdir and certfile.")
|
||||
|
||||
criteria = dict()
|
||||
if secdir:
|
||||
criteria['cert-database'] = secdir
|
||||
if request_id:
|
||||
criteria['nickname'] = request_id
|
||||
if nickname:
|
||||
criteria['cert-nickname'] = nickname
|
||||
if certfile:
|
||||
criteria['cert-file'] = certfile
|
||||
try:
|
||||
request = _get_request(criteria)
|
||||
except RuntimeError as e:
|
||||
logger.error('Failed to get request: %s', e)
|
||||
raise
|
||||
if request:
|
||||
request.parent.obj_if.remove_request(request.path)
|
||||
|
||||
|
||||
def modify(request_id, ca=None, profile=None, template_v2=None):
|
||||
update = {}
|
||||
if ca is not None:
|
||||
cm = _certmonger()
|
||||
update['CA'] = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if profile is not None:
|
||||
update['template-profile'] = profile
|
||||
if template_v2 is not None:
|
||||
update['template-ms-certificate-template'] = template_v2
|
||||
|
||||
if len(update) > 0:
|
||||
request = _get_request({'nickname': request_id})
|
||||
request.obj_if.modify(update)
|
||||
|
||||
|
||||
def resubmit_request(
|
||||
request_id,
|
||||
ca=None,
|
||||
profile=None,
|
||||
template_v2=None,
|
||||
is_ca=False):
|
||||
"""
|
||||
:param request_id: the certmonger numeric request ID
|
||||
:param ca: the nickname for the certmonger CA, e.g. IPA or SelfSign
|
||||
:param profile: the profile to use, e.g. SubCA. For requests using the
|
||||
Dogtag CA, this is the profile to use. This also causes
|
||||
the Microsoft certificate tempalte name extension to the
|
||||
CSR (for telling AD CS what template to use).
|
||||
:param template_v2: Microsoft V2 template specifier extension value.
|
||||
Format: <oid>:<major-version>[:<minor-version>]
|
||||
:param is_ca: boolean that if True adds the CA basic constraint
|
||||
"""
|
||||
request = _get_request({'nickname': request_id})
|
||||
if request:
|
||||
update = {}
|
||||
if ca is not None:
|
||||
cm = _certmonger()
|
||||
update['CA'] = cm.obj_if.find_ca_by_nickname(ca)
|
||||
if profile is not None:
|
||||
update['template-profile'] = profile
|
||||
if template_v2 is not None:
|
||||
update['template-ms-certificate-template'] = template_v2
|
||||
if is_ca:
|
||||
update['template-is-ca'] = True
|
||||
update['template-ca-path-length'] = -1 # no path length
|
||||
|
||||
if len(update) > 0:
|
||||
request.obj_if.modify(update)
|
||||
request.obj_if.resubmit()
|
||||
|
||||
|
||||
def _find_IPA_ca():
|
||||
"""
|
||||
Look through all the certmonger CA files to find the one that
|
||||
has id=IPA
|
||||
|
||||
We can use find_request_value because the ca files have the
|
||||
same file format.
|
||||
"""
|
||||
cm = _certmonger()
|
||||
ca_path = cm.obj_if.find_ca_by_nickname('IPA')
|
||||
return _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF, DBUS_CM_IF, True)
|
||||
|
||||
|
||||
def add_principal_to_cas(principal):
|
||||
"""
|
||||
If the hostname we were passed to use in ipa-client-install doesn't
|
||||
match the value of gethostname() then we need to append
|
||||
-k host/HOSTNAME@REALM to the ca helper defined for
|
||||
/usr/libexec/certmonger/ipa-submit.
|
||||
|
||||
We also need to restore this on uninstall.
|
||||
"""
|
||||
ca = _find_IPA_ca()
|
||||
if ca:
|
||||
ext_helper = ca.prop_if.Get(DBUS_CM_CA_IF, 'external-helper')
|
||||
if ext_helper and '-k' not in shlex.split(ext_helper):
|
||||
ext_helper = '%s -k %s' % (ext_helper.strip(), principal)
|
||||
ca.prop_if.Set(DBUS_CM_CA_IF, 'external-helper', ext_helper)
|
||||
|
||||
|
||||
def remove_principal_from_cas():
|
||||
"""
|
||||
Remove any -k principal options from the ipa_submit helper.
|
||||
"""
|
||||
ca = _find_IPA_ca()
|
||||
if ca:
|
||||
ext_helper = ca.prop_if.Get(DBUS_CM_CA_IF, 'external-helper')
|
||||
if ext_helper and '-k' in shlex.split(ext_helper):
|
||||
ext_helper = shlex.split(ext_helper)[0]
|
||||
ca.prop_if.Set(DBUS_CM_CA_IF, 'external-helper', ext_helper)
|
||||
|
||||
|
||||
def modify_ca_helper(ca_name, helper):
|
||||
"""
|
||||
Modify certmonger CA helper.
|
||||
|
||||
Applies the new helper and return the previous configuration.
|
||||
"""
|
||||
bus = dbus.SystemBus()
|
||||
obj = bus.get_object('org.fedorahosted.certmonger',
|
||||
'/org/fedorahosted/certmonger')
|
||||
iface = dbus.Interface(obj, 'org.fedorahosted.certmonger')
|
||||
path = iface.find_ca_by_nickname(ca_name)
|
||||
if not path:
|
||||
raise RuntimeError("{} is not configured".format(ca_name))
|
||||
else:
|
||||
ca_obj = bus.get_object('org.fedorahosted.certmonger', path)
|
||||
ca_iface = dbus.Interface(ca_obj,
|
||||
'org.freedesktop.DBus.Properties')
|
||||
old_helper = ca_iface.Get('org.fedorahosted.certmonger.ca',
|
||||
'external-helper')
|
||||
ca_iface.Set('org.fedorahosted.certmonger.ca',
|
||||
'external-helper', helper,
|
||||
# Give dogtag extra time to generate cert
|
||||
timeout=CA_DBUS_TIMEOUT)
|
||||
return old_helper
|
||||
|
||||
|
||||
def get_pin(token):
|
||||
"""
|
||||
Dogtag stores its NSS pin in a file formatted as token:PIN.
|
||||
|
||||
The caller is expected to handle any exceptions raised.
|
||||
"""
|
||||
with open(paths.PKI_TOMCAT_PASSWORD_CONF, 'r') as f:
|
||||
for line in f:
|
||||
(tok, pin) = line.split('=', 1)
|
||||
if token == tok:
|
||||
return pin.strip()
|
||||
return None
|
||||
|
||||
|
||||
def check_state(dirs):
|
||||
"""
|
||||
Given a set of directories and nicknames verify that we are no longer
|
||||
tracking certificates.
|
||||
|
||||
dirs is a list of directories to test for. We will return a tuple
|
||||
of nicknames for any tracked certificates found.
|
||||
|
||||
This can only check for NSS-based certificates.
|
||||
"""
|
||||
reqids = []
|
||||
for dir in dirs:
|
||||
reqids.extend(get_requests_for_dir(dir))
|
||||
|
||||
return reqids
|
||||
|
||||
|
||||
def wait_for_request(request_id, timeout=120):
|
||||
for _i in range(0, timeout, 5):
|
||||
state = get_request_value(request_id, 'status')
|
||||
logger.debug("certmonger request is in state %r", state)
|
||||
if state in ('CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
|
||||
'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'):
|
||||
break
|
||||
time.sleep(5)
|
||||
else:
|
||||
raise RuntimeError("request timed out")
|
||||
|
||||
return state
|
||||
|
||||
if __name__ == '__main__':
|
||||
request_id = request_cert(paths.HTTPD_ALIAS_DIR,
|
||||
"cn=tiger.example.com,O=IPA",
|
||||
"HTTP/tiger.example.com@EXAMPLE.COM", "Test")
|
||||
csr = get_request_value(request_id, 'csr')
|
||||
print(csr)
|
||||
stop_tracking(request_id)
|
||||
@@ -1,406 +0,0 @@
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
"""
|
||||
LDAP shared certificate store.
|
||||
"""
|
||||
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
from ipapython.dn import DN
|
||||
from ipapython.certdb import get_ca_nickname, TrustFlags
|
||||
from ipalib import errors, x509
|
||||
from ipalib.constants import IPA_CA_CN
|
||||
|
||||
|
||||
def _parse_cert(cert):
|
||||
try:
|
||||
subject = DN(cert.subject)
|
||||
issuer = DN(cert.issuer)
|
||||
serial_number = cert.serial_number
|
||||
public_key_info = cert.public_key_info_bytes
|
||||
except (ValueError, PyAsn1Error) as e:
|
||||
raise ValueError("failed to decode certificate: %s" % e)
|
||||
|
||||
subject = str(subject).replace('\\;', '\\3b')
|
||||
issuer = str(issuer).replace('\\;', '\\3b')
|
||||
issuer_serial = '%s;%s' % (issuer, serial_number)
|
||||
|
||||
return subject, issuer_serial, public_key_info
|
||||
|
||||
|
||||
def init_ca_entry(entry, cert, nickname, trusted, ext_key_usage):
|
||||
"""
|
||||
Initialize certificate store entry for a CA certificate.
|
||||
"""
|
||||
subject, issuer_serial, public_key = _parse_cert(cert)
|
||||
|
||||
if ext_key_usage is not None:
|
||||
try:
|
||||
cert_eku = cert.extended_key_usage
|
||||
except ValueError as e:
|
||||
raise ValueError("failed to decode certificate: %s" % e)
|
||||
if cert_eku is not None:
|
||||
cert_eku -= {x509.EKU_SERVER_AUTH, x509.EKU_CLIENT_AUTH,
|
||||
x509.EKU_EMAIL_PROTECTION, x509.EKU_CODE_SIGNING,
|
||||
x509.EKU_ANY, x509.EKU_PLACEHOLDER}
|
||||
ext_key_usage = ext_key_usage | cert_eku
|
||||
|
||||
entry['objectClass'] = ['ipaCertificate', 'pkiCA', 'ipaKeyPolicy']
|
||||
entry['cn'] = [nickname]
|
||||
|
||||
entry['ipaCertSubject'] = [subject]
|
||||
entry['ipaCertIssuerSerial'] = [issuer_serial]
|
||||
entry['ipaPublicKey'] = [public_key]
|
||||
entry['cACertificate;binary'] = [cert]
|
||||
|
||||
if trusted is not None:
|
||||
entry['ipaKeyTrust'] = ['trusted' if trusted else 'distrusted']
|
||||
if ext_key_usage is not None:
|
||||
ext_key_usage = list(ext_key_usage)
|
||||
if not ext_key_usage:
|
||||
ext_key_usage.append(x509.EKU_PLACEHOLDER)
|
||||
entry['ipaKeyExtUsage'] = ext_key_usage
|
||||
|
||||
|
||||
def update_compat_ca(ldap, base_dn, cert):
|
||||
"""
|
||||
Update the CA certificate in cn=CAcert,cn=ipa,cn=etc,SUFFIX.
|
||||
"""
|
||||
dn = DN(('cn', 'CAcert'), ('cn', 'ipa'), ('cn', 'etc'), base_dn)
|
||||
try:
|
||||
entry = ldap.get_entry(dn, attrs_list=['cACertificate;binary'])
|
||||
entry.single_value['cACertificate;binary'] = cert
|
||||
ldap.update_entry(entry)
|
||||
except errors.NotFound:
|
||||
entry = ldap.make_entry(dn)
|
||||
entry['objectClass'] = ['nsContainer', 'pkiCA']
|
||||
entry.single_value['cn'] = 'CAcert'
|
||||
entry.single_value['cACertificate;binary'] = cert
|
||||
ldap.add_entry(entry)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def clean_old_config(ldap, base_dn, dn, config_ipa, config_compat):
|
||||
"""
|
||||
Remove ipaCA and compatCA flags from their previous carriers.
|
||||
"""
|
||||
if not config_ipa and not config_compat:
|
||||
return
|
||||
|
||||
try:
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn),
|
||||
filter='(|(ipaConfigString=ipaCA)(ipaConfigString=compatCA))',
|
||||
attrs_list=['ipaConfigString'])
|
||||
except errors.NotFound:
|
||||
return
|
||||
|
||||
for entry in result:
|
||||
if entry.dn == dn:
|
||||
continue
|
||||
for config in list(entry['ipaConfigString']):
|
||||
if config.lower() == 'ipaca' and config_ipa:
|
||||
entry['ipaConfigString'].remove(config)
|
||||
elif config.lower() == 'compatca' and config_compat:
|
||||
entry['ipaConfigString'].remove(config)
|
||||
try:
|
||||
ldap.update_entry(entry)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def add_ca_cert(ldap, base_dn, cert, nickname, trusted=None,
|
||||
ext_key_usage=None, config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add new entry for a CA certificate to the certificate store.
|
||||
"""
|
||||
container_dn = DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn)
|
||||
dn = DN(('cn', nickname), container_dn)
|
||||
entry = ldap.make_entry(dn)
|
||||
|
||||
init_ca_entry(entry, cert, nickname, trusted, ext_key_usage)
|
||||
|
||||
if config_ipa:
|
||||
entry.setdefault('ipaConfigString', []).append('ipaCA')
|
||||
if config_compat:
|
||||
entry.setdefault('ipaConfigString', []).append('compatCA')
|
||||
|
||||
if config_compat:
|
||||
update_compat_ca(ldap, base_dn, cert)
|
||||
|
||||
ldap.add_entry(entry)
|
||||
clean_old_config(ldap, base_dn, dn, config_ipa, config_compat)
|
||||
|
||||
|
||||
def update_ca_cert(ldap, base_dn, cert, trusted=None, ext_key_usage=None,
|
||||
config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Update existing entry for a CA certificate in the certificate store.
|
||||
"""
|
||||
subject, issuer_serial, public_key = _parse_cert(cert)
|
||||
|
||||
filter = ldap.make_filter({'ipaCertSubject': subject})
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=DN(('cn', 'certificates'), ('cn', 'ipa'), ('cn', 'etc'),
|
||||
base_dn),
|
||||
filter=filter,
|
||||
attrs_list=['cn', 'ipaCertSubject', 'ipaCertIssuerSerial',
|
||||
'ipaPublicKey', 'ipaKeyTrust', 'ipaKeyExtUsage',
|
||||
'ipaConfigString', 'cACertificate;binary'])
|
||||
entry = result[0]
|
||||
dn = entry.dn
|
||||
|
||||
for old_cert in entry['cACertificate;binary']:
|
||||
# Check if we are adding a new cert
|
||||
if old_cert == cert:
|
||||
break
|
||||
else:
|
||||
# We are adding a new cert, validate it
|
||||
if entry.single_value['ipaCertSubject'].lower() != subject.lower():
|
||||
raise ValueError("subject name mismatch")
|
||||
if entry.single_value['ipaPublicKey'] != public_key:
|
||||
raise ValueError("subject public key info mismatch")
|
||||
entry['ipaCertIssuerSerial'].append(issuer_serial)
|
||||
entry['cACertificate;binary'].append(cert)
|
||||
|
||||
# Update key trust
|
||||
if trusted is not None:
|
||||
old_trust = entry.single_value.get('ipaKeyTrust')
|
||||
new_trust = 'trusted' if trusted else 'distrusted'
|
||||
if old_trust is not None and old_trust.lower() != new_trust:
|
||||
raise ValueError("inconsistent trust")
|
||||
entry.single_value['ipaKeyTrust'] = new_trust
|
||||
|
||||
# Update extended key usage
|
||||
if trusted is not False:
|
||||
if ext_key_usage is not None:
|
||||
old_eku = set(entry.get('ipaKeyExtUsage', []))
|
||||
old_eku.discard(x509.EKU_PLACEHOLDER)
|
||||
new_eku = old_eku | ext_key_usage
|
||||
if not new_eku:
|
||||
new_eku.add(x509.EKU_PLACEHOLDER)
|
||||
entry['ipaKeyExtUsage'] = list(new_eku)
|
||||
else:
|
||||
entry.pop('ipaKeyExtUsage', None)
|
||||
|
||||
# Update configuration flags
|
||||
is_ipa = False
|
||||
is_compat = False
|
||||
for config in entry.get('ipaConfigString', []):
|
||||
if config.lower() == 'ipaca':
|
||||
is_ipa = True
|
||||
elif config.lower() == 'compatca':
|
||||
is_compat = True
|
||||
if config_ipa and not is_ipa:
|
||||
entry.setdefault('ipaConfigString', []).append('ipaCA')
|
||||
if config_compat and not is_compat:
|
||||
entry.setdefault('ipaConfigString', []).append('compatCA')
|
||||
|
||||
if is_compat or config_compat:
|
||||
update_compat_ca(ldap, base_dn, cert)
|
||||
|
||||
ldap.update_entry(entry)
|
||||
clean_old_config(ldap, base_dn, dn, config_ipa, config_compat)
|
||||
|
||||
|
||||
def put_ca_cert(ldap, base_dn, cert, nickname, trusted=None,
|
||||
ext_key_usage=None, config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add or update entry for a CA certificate in the certificate store.
|
||||
|
||||
:param cert: IPACertificate
|
||||
"""
|
||||
try:
|
||||
update_ca_cert(ldap, base_dn, cert, trusted, ext_key_usage,
|
||||
config_ipa=config_ipa, config_compat=config_compat)
|
||||
except errors.NotFound:
|
||||
add_ca_cert(ldap, base_dn, cert, nickname, trusted, ext_key_usage,
|
||||
config_ipa=config_ipa, config_compat=config_compat)
|
||||
except errors.EmptyModlist:
|
||||
pass
|
||||
|
||||
|
||||
def make_compat_ca_certs(certs, realm, ipa_ca_subject):
|
||||
"""
|
||||
Make CA certificates and associated key policy from DER certificates.
|
||||
"""
|
||||
result = []
|
||||
|
||||
for cert in certs:
|
||||
subject, _issuer_serial, _public_key_info = _parse_cert(cert)
|
||||
subject = DN(subject)
|
||||
|
||||
if ipa_ca_subject is not None and subject == DN(ipa_ca_subject):
|
||||
nickname = get_ca_nickname(realm)
|
||||
ext_key_usage = {x509.EKU_SERVER_AUTH,
|
||||
x509.EKU_CLIENT_AUTH,
|
||||
x509.EKU_EMAIL_PROTECTION,
|
||||
x509.EKU_CODE_SIGNING}
|
||||
else:
|
||||
nickname = str(subject)
|
||||
ext_key_usage = {x509.EKU_SERVER_AUTH}
|
||||
|
||||
result.append((cert, nickname, True, ext_key_usage))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_ca_certs(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=None):
|
||||
"""
|
||||
Get CA certificates and associated key policy from the certificate store.
|
||||
"""
|
||||
if filter_subject is not None:
|
||||
if not isinstance(filter_subject, list):
|
||||
filter_subject = [filter_subject]
|
||||
filter_subject = [str(subj).replace('\\;', '\\3b')
|
||||
for subj in filter_subject]
|
||||
|
||||
certs = []
|
||||
config_dn = DN(('cn', 'ipa'), ('cn', 'etc'), base_dn)
|
||||
container_dn = DN(('cn', 'certificates'), config_dn)
|
||||
try:
|
||||
# Search the certificate store for CA certificate entries
|
||||
filters = ['(objectClass=ipaCertificate)', '(objectClass=pkiCA)']
|
||||
if filter_subject:
|
||||
filter = ldap.make_filter({'ipaCertSubject': filter_subject})
|
||||
filters.append(filter)
|
||||
result, _truncated = ldap.find_entries(
|
||||
base_dn=container_dn,
|
||||
filter=ldap.combine_filters(filters, ldap.MATCH_ALL),
|
||||
attrs_list=['cn', 'ipaCertSubject', 'ipaCertIssuerSerial',
|
||||
'ipaPublicKey', 'ipaKeyTrust', 'ipaKeyExtUsage',
|
||||
'cACertificate;binary'])
|
||||
|
||||
for entry in result:
|
||||
nickname = entry.single_value['cn']
|
||||
trusted = entry.single_value.get('ipaKeyTrust', 'unknown').lower()
|
||||
if trusted == 'trusted':
|
||||
trusted = True
|
||||
elif trusted == 'distrusted':
|
||||
trusted = False
|
||||
else:
|
||||
trusted = None
|
||||
ext_key_usage = entry.get('ipaKeyExtUsage')
|
||||
if ext_key_usage is not None:
|
||||
ext_key_usage = set(str(p) for p in ext_key_usage)
|
||||
ext_key_usage.discard(x509.EKU_PLACEHOLDER)
|
||||
|
||||
for cert in entry.get('cACertificate;binary', []):
|
||||
try:
|
||||
_parse_cert(cert)
|
||||
except ValueError:
|
||||
certs = []
|
||||
break
|
||||
certs.append((cert, nickname, trusted, ext_key_usage))
|
||||
except errors.NotFound:
|
||||
try:
|
||||
ldap.get_entry(container_dn, [''])
|
||||
except errors.NotFound:
|
||||
# Fallback to cn=CAcert,cn=ipa,cn=etc,SUFFIX
|
||||
dn = DN(('cn', 'CAcert'), config_dn)
|
||||
entry = ldap.get_entry(dn, ['cACertificate;binary'])
|
||||
|
||||
cert = entry.single_value['cACertificate;binary']
|
||||
try:
|
||||
subject, _issuer_serial, _public_key_info = _parse_cert(cert)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if filter_subject is not None and subject not in filter_subject:
|
||||
raise errors.NotFound(reason="no matching entry found")
|
||||
|
||||
if compat_ipa_ca:
|
||||
ca_subject = subject
|
||||
else:
|
||||
ca_subject = None
|
||||
certs = make_compat_ca_certs([cert], compat_realm, ca_subject)
|
||||
|
||||
if certs:
|
||||
return certs
|
||||
else:
|
||||
raise errors.NotFound(reason="no such entry")
|
||||
|
||||
|
||||
def trust_flags_to_key_policy(trust_flags):
|
||||
"""
|
||||
Convert certutil trust flags to certificate store key policy.
|
||||
"""
|
||||
return trust_flags[1:]
|
||||
|
||||
|
||||
def key_policy_to_trust_flags(trusted, ca, ext_key_usage):
|
||||
"""
|
||||
Convert certificate store key policy to certutil trust flags.
|
||||
"""
|
||||
return TrustFlags(False, trusted, ca, ext_key_usage)
|
||||
|
||||
|
||||
def put_ca_cert_nss(ldap, base_dn, cert, nickname, trust_flags,
|
||||
config_ipa=False, config_compat=False):
|
||||
"""
|
||||
Add or update entry for a CA certificate in the certificate store.
|
||||
|
||||
:param cert: IPACertificate
|
||||
"""
|
||||
trusted, ca, ext_key_usage = trust_flags_to_key_policy(trust_flags)
|
||||
if ca is False:
|
||||
raise ValueError("must be CA certificate")
|
||||
|
||||
put_ca_cert(ldap, base_dn, cert, nickname, trusted, ext_key_usage,
|
||||
config_ipa, config_compat)
|
||||
|
||||
|
||||
def get_ca_certs_nss(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=None):
|
||||
"""
|
||||
Get CA certificates and associated trust flags from the certificate store.
|
||||
"""
|
||||
nss_certs = []
|
||||
|
||||
certs = get_ca_certs(ldap, base_dn, compat_realm, compat_ipa_ca,
|
||||
filter_subject=filter_subject)
|
||||
for cert, nickname, trusted, ext_key_usage in certs:
|
||||
trust_flags = key_policy_to_trust_flags(trusted, True, ext_key_usage)
|
||||
nss_certs.append((cert, nickname, trust_flags))
|
||||
|
||||
return nss_certs
|
||||
|
||||
|
||||
def get_ca_subject(ldap, container_ca, base_dn):
|
||||
"""
|
||||
Look for the IPA CA certificate subject.
|
||||
"""
|
||||
dn = DN(('cn', IPA_CA_CN), container_ca, base_dn)
|
||||
try:
|
||||
cacert_subject = ldap.get_entry(dn)['ipacasubjectdn'][0]
|
||||
except errors.NotFound:
|
||||
# if the entry doesn't exist, we are dealing with a pre-v4.4
|
||||
# installation, where the default CA subject was always based
|
||||
# on the subject_base.
|
||||
attrs = ldap.get_ipa_config()
|
||||
subject_base = attrs.get('ipacertificatesubjectbase')[0]
|
||||
cacert_subject = DN(('CN', 'Certificate Authority'), subject_base)
|
||||
|
||||
return cacert_subject
|
||||
@@ -1,59 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Host name installer module
|
||||
"""
|
||||
|
||||
from ipapython.install import typing
|
||||
from ipapython.install.core import knob
|
||||
from ipapython.ipautil import CheckedIPAddress
|
||||
|
||||
from . import service
|
||||
from .service import prepare_only
|
||||
|
||||
|
||||
class HostNameInstallInterface(service.ServiceInstallInterface):
|
||||
"""
|
||||
Interface common to all service installers which create DNS address
|
||||
records for `host_name`
|
||||
"""
|
||||
|
||||
ip_addresses = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[CheckedIPAddress], None,
|
||||
description="Specify IP address that should be added to DNS. This "
|
||||
"option can be used multiple times",
|
||||
cli_names='--ip-address',
|
||||
cli_metavar='IP_ADDRESS',
|
||||
)
|
||||
ip_addresses = prepare_only(ip_addresses)
|
||||
|
||||
@ip_addresses.validator
|
||||
def ip_addresses(self, values):
|
||||
for value in values:
|
||||
try:
|
||||
CheckedIPAddress(value)
|
||||
except Exception as e:
|
||||
raise ValueError("invalid IP address {0}: {1}".format(
|
||||
value, e))
|
||||
|
||||
all_ip_addresses = knob(
|
||||
None,
|
||||
description="All routable IP addresses configured on any interface "
|
||||
"will be added to DNS",
|
||||
)
|
||||
all_ip_addresses = prepare_only(all_ip_addresses)
|
||||
|
||||
no_host_dns = knob(
|
||||
None,
|
||||
description="Do not use DNS for hostname lookup during installation",
|
||||
)
|
||||
no_host_dns = prepare_only(no_host_dns)
|
||||
|
||||
no_wait_for_dns = knob(
|
||||
None,
|
||||
description="do not wait until the host is resolvable in DNS",
|
||||
)
|
||||
no_wait_for_dns = prepare_only(no_wait_for_dns)
|
||||
@@ -1,125 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import gssapi
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.ipautil import run
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Cannot contact any KDC for requested realm
|
||||
KRB5_KDC_UNREACH = 2529639068
|
||||
|
||||
# A service is not available that s required to process the request
|
||||
KRB5KDC_ERR_SVC_UNAVAILABLE = 2529638941
|
||||
|
||||
|
||||
def kinit_keytab(principal, keytab, ccache_name, config=None, attempts=1):
|
||||
"""
|
||||
Given a ccache_path, keytab file and a principal kinit as that user.
|
||||
|
||||
The optional parameter 'attempts' specifies how many times the credential
|
||||
initialization should be attempted in case of non-responsive KDC.
|
||||
"""
|
||||
errors_to_retry = {KRB5KDC_ERR_SVC_UNAVAILABLE,
|
||||
KRB5_KDC_UNREACH}
|
||||
logger.debug("Initializing principal %s using keytab %s",
|
||||
principal, keytab)
|
||||
logger.debug("using ccache %s", ccache_name)
|
||||
for attempt in range(1, attempts + 1):
|
||||
old_config = os.environ.get('KRB5_CONFIG')
|
||||
if config is not None:
|
||||
os.environ['KRB5_CONFIG'] = config
|
||||
else:
|
||||
os.environ.pop('KRB5_CONFIG', None)
|
||||
try:
|
||||
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
|
||||
store = {'ccache': ccache_name,
|
||||
'client_keytab': keytab}
|
||||
cred = gssapi.Credentials(name=name, store=store, usage='initiate')
|
||||
logger.debug("Attempt %d/%d: success", attempt, attempts)
|
||||
return cred
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
if e.min_code not in errors_to_retry: # pylint: disable=no-member
|
||||
raise
|
||||
logger.debug("Attempt %d/%d: failed: %s", attempt, attempts, e)
|
||||
if attempt == attempts:
|
||||
logger.debug("Maximum number of attempts (%d) reached",
|
||||
attempts)
|
||||
raise
|
||||
logger.debug("Waiting 5 seconds before next retry")
|
||||
time.sleep(5)
|
||||
finally:
|
||||
if old_config is not None:
|
||||
os.environ['KRB5_CONFIG'] = old_config
|
||||
else:
|
||||
os.environ.pop('KRB5_CONFIG', None)
|
||||
|
||||
def kinit_password(principal, password, ccache_name, config=None,
|
||||
armor_ccache_name=None, canonicalize=False,
|
||||
enterprise=False, lifetime=None):
|
||||
"""
|
||||
perform interactive kinit as principal using password. If using FAST for
|
||||
web-based authentication, use armor_ccache_path to specify http service
|
||||
ccache.
|
||||
"""
|
||||
logger.debug("Initializing principal %s using password", principal)
|
||||
args = [paths.KINIT, principal, '-c', ccache_name]
|
||||
if armor_ccache_name is not None:
|
||||
logger.debug("Using armor ccache %s for FAST webauth",
|
||||
armor_ccache_name)
|
||||
args.extend(['-T', armor_ccache_name])
|
||||
|
||||
if lifetime:
|
||||
args.extend(['-l', lifetime])
|
||||
|
||||
if canonicalize:
|
||||
logger.debug("Requesting principal canonicalization")
|
||||
args.append('-C')
|
||||
|
||||
if enterprise:
|
||||
logger.debug("Using enterprise principal")
|
||||
args.append('-E')
|
||||
|
||||
env = {'LC_ALL': 'C'}
|
||||
if config is not None:
|
||||
env['KRB5_CONFIG'] = config
|
||||
|
||||
# this workaround enables us to capture stderr and put it
|
||||
# into the raised exception in case of unsuccessful authentication
|
||||
result = run(args, stdin=password, env=env, raiseonerr=False,
|
||||
capture_error=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.error_output)
|
||||
|
||||
|
||||
def kinit_armor(ccache_name, pkinit_anchors=None):
|
||||
"""
|
||||
perform anonymous pkinit to obtain anonymous ticket to be used as armor
|
||||
for FAST.
|
||||
|
||||
:param ccache_name: location of the armor ccache
|
||||
:param pkinit_anchor: if not None, the location of PKINIT anchor file to
|
||||
use. Otherwise the value from Kerberos client library configuration is
|
||||
used
|
||||
|
||||
:raises: CalledProcessError if the anonymous PKINIT fails
|
||||
"""
|
||||
logger.debug("Initializing anonymous ccache")
|
||||
|
||||
env = {'LC_ALL': 'C'}
|
||||
args = [paths.KINIT, '-n', '-c', ccache_name]
|
||||
|
||||
if pkinit_anchors is not None:
|
||||
for pkinit_anchor in pkinit_anchors:
|
||||
args.extend(['-X', 'X509_anchors=FILE:{}'.format(pkinit_anchor)])
|
||||
|
||||
# this workaround enables us to capture stderr and put it
|
||||
# into the raised exception in case of unsuccessful authentication
|
||||
run(args, env=env, raiseonerr=True, capture_error=True)
|
||||
@@ -1,178 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Base service installer module
|
||||
"""
|
||||
|
||||
from ipalib.util import validate_domain_name
|
||||
from ipapython.install import common, core, typing
|
||||
from ipapython.install.core import group, knob
|
||||
|
||||
|
||||
def prepare_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in the prepare
|
||||
phase of the install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'enroll'}
|
||||
return obj
|
||||
|
||||
|
||||
def enroll_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in the enroll
|
||||
phase of the install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'prepare'}
|
||||
return obj
|
||||
|
||||
|
||||
def master_install_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in master install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'replica_install'}
|
||||
return obj
|
||||
|
||||
|
||||
def replica_install_only(obj):
|
||||
"""
|
||||
Decorator which makes an installer attribute appear only in replica install
|
||||
"""
|
||||
obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'master_install'}
|
||||
return obj
|
||||
|
||||
|
||||
def _does(cls, arg):
|
||||
def remove(name):
|
||||
def removed(self):
|
||||
raise AttributeError(name)
|
||||
|
||||
return property(removed)
|
||||
|
||||
return type(
|
||||
cls.__name__,
|
||||
(cls,),
|
||||
{
|
||||
n: remove(n) for n in dir(cls)
|
||||
if arg in getattr(getattr(cls, n), '__exclude__', set())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def prepares(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to the prepare
|
||||
phase of the install
|
||||
"""
|
||||
return _does(cls, 'prepare')
|
||||
|
||||
|
||||
def enrolls(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to the enroll
|
||||
phase of the install
|
||||
"""
|
||||
return _does(cls, 'enroll')
|
||||
|
||||
|
||||
def installs_master(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to master
|
||||
install
|
||||
"""
|
||||
return _does(cls, 'master_install')
|
||||
|
||||
|
||||
def installs_replica(cls):
|
||||
"""
|
||||
Returns installer class stripped of attributes not related to replica
|
||||
install
|
||||
"""
|
||||
return _does(cls, 'replica_install')
|
||||
|
||||
|
||||
@group
|
||||
class ServiceInstallInterface(common.Installable,
|
||||
common.Interactive,
|
||||
core.Composite):
|
||||
"""
|
||||
Interface common to all service installers
|
||||
"""
|
||||
description = "Basic"
|
||||
|
||||
domain_name = knob(
|
||||
str, None,
|
||||
description="primary DNS domain of the IPA deployment "
|
||||
"(not necessarily related to the current hostname)",
|
||||
cli_names='--domain',
|
||||
)
|
||||
|
||||
@domain_name.validator
|
||||
def domain_name(self, value):
|
||||
validate_domain_name(value)
|
||||
|
||||
servers = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[str], None,
|
||||
description="FQDN of IPA server",
|
||||
cli_names='--server',
|
||||
cli_metavar='SERVER',
|
||||
)
|
||||
|
||||
realm_name = knob(
|
||||
str, None,
|
||||
description="Kerberos realm name of the IPA deployment (typically "
|
||||
"an upper-cased name of the primary DNS domain)",
|
||||
cli_names='--realm',
|
||||
)
|
||||
|
||||
host_name = knob(
|
||||
str, None,
|
||||
description="The hostname of this machine (FQDN). If specified, the "
|
||||
"hostname will be set and the system configuration will "
|
||||
"be updated to persist over reboot. By default the result "
|
||||
"of getfqdn() call from Python's socket module is used.",
|
||||
cli_names='--hostname',
|
||||
)
|
||||
|
||||
ca_cert_files = knob(
|
||||
# pylint: disable=invalid-sequence-index
|
||||
typing.List[str], None,
|
||||
description="load the CA certificate from this file",
|
||||
cli_names='--ca-cert-file',
|
||||
cli_metavar='FILE',
|
||||
)
|
||||
|
||||
replica_file = knob(
|
||||
str, None,
|
||||
description="a file generated by ipa-replica-prepare",
|
||||
)
|
||||
replica_file = replica_install_only(replica_file)
|
||||
|
||||
dm_password = knob(
|
||||
str, None,
|
||||
sensitive=True,
|
||||
description="Directory Manager password (for the existing master)",
|
||||
)
|
||||
|
||||
|
||||
class ServiceAdminInstallInterface(ServiceInstallInterface):
|
||||
"""
|
||||
Interface common to all service installers which require admin user
|
||||
authentication
|
||||
"""
|
||||
|
||||
principal = knob(
|
||||
str, None,
|
||||
)
|
||||
principal = enroll_only(principal)
|
||||
principal = replica_install_only(principal)
|
||||
|
||||
admin_password = knob(
|
||||
str, None,
|
||||
sensitive=True,
|
||||
)
|
||||
admin_password = enroll_only(admin_password)
|
||||
@@ -1,453 +0,0 @@
|
||||
# Authors: Mark McLoughlin <markmc@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/>.
|
||||
#
|
||||
|
||||
#
|
||||
# This module provides a very simple API which allows
|
||||
# ipa-xxx-install --uninstall to restore certain
|
||||
# parts of the system configuration to the way it was
|
||||
# before ipa-server-install was first run
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
|
||||
import six
|
||||
# pylint: disable=import-error
|
||||
if six.PY3:
|
||||
# The SafeConfigParser class has been renamed to ConfigParser in Py3
|
||||
from configparser import ConfigParser as SafeConfigParser
|
||||
else:
|
||||
from ConfigParser import SafeConfigParser
|
||||
# pylint: enable=import-error
|
||||
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSRESTORE_PATH = paths.TMP
|
||||
SYSRESTORE_INDEXFILE = "sysrestore.index"
|
||||
SYSRESTORE_STATEFILE = "sysrestore.state"
|
||||
|
||||
|
||||
class FileStore(object):
|
||||
"""Class for handling backup and restore of files"""
|
||||
|
||||
def __init__(self, path = SYSRESTORE_PATH, index_file = SYSRESTORE_INDEXFILE):
|
||||
"""Create a _StoreFiles object, that uses @path as the
|
||||
base directory.
|
||||
|
||||
The file @path/sysrestore.index is used to store information
|
||||
about the original location of the saved files.
|
||||
"""
|
||||
self._path = path
|
||||
self._index = os.path.join(self._path, index_file)
|
||||
|
||||
self.random = random.Random()
|
||||
|
||||
self.files = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the file list from the index file. @files will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
|
||||
logger.debug("Loading Index file from '%s'", self._index)
|
||||
|
||||
self.files = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._index)
|
||||
|
||||
for section in p.sections():
|
||||
if section == "files":
|
||||
for (key, value) in p.items(section):
|
||||
self.files[key] = value
|
||||
|
||||
|
||||
def save(self):
|
||||
"""Save the file list to @_index. If @files is an empty
|
||||
dict, then @_index should be removed.
|
||||
"""
|
||||
logger.debug("Saving Index File to '%s'", self._index)
|
||||
|
||||
if len(self.files) == 0:
|
||||
logger.debug(" -> no files, removing file")
|
||||
if os.path.exists(self._index):
|
||||
os.remove(self._index)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
p.add_section('files')
|
||||
for (key, value) in self.files.items():
|
||||
p.set('files', key, str(value))
|
||||
|
||||
with open(self._index, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_file(self, path):
|
||||
"""Create a copy of the file at @path - so long as a copy
|
||||
does not already exist - which will be restored to its
|
||||
original location by restore_files().
|
||||
"""
|
||||
logger.debug("Backing up system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
if not os.path.isfile(path):
|
||||
logger.debug(" -> Not backing up - '%s' doesn't exist", path)
|
||||
return
|
||||
|
||||
_reldir, backupfile = os.path.split(path)
|
||||
|
||||
filename = ""
|
||||
for _i in range(8):
|
||||
h = "%02x" % self.random.randint(0,255)
|
||||
filename += h
|
||||
filename += "-"+backupfile
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if os.path.exists(backup_path):
|
||||
logger.debug(" -> Not backing up - already have a copy of '%s'",
|
||||
path)
|
||||
return
|
||||
|
||||
shutil.copy2(path, backup_path)
|
||||
|
||||
stat = os.stat(path)
|
||||
|
||||
template = '{stat.st_mode},{stat.st_uid},{stat.st_gid},{path}'
|
||||
self.files[filename] = template.format(stat=stat, path=path)
|
||||
self.save()
|
||||
|
||||
def has_file(self, path):
|
||||
"""Checks whether file at @path was added to the file store
|
||||
|
||||
Returns #True if the file exists in the file store, #False otherwise
|
||||
"""
|
||||
result = False
|
||||
for _key, value in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
result = True
|
||||
break
|
||||
return result
|
||||
|
||||
def restore_file(self, path, new_path = None):
|
||||
"""Restore the copy of a file at @path to its original
|
||||
location and delete the copy.
|
||||
|
||||
Takes optional parameter @new_path which specifies the
|
||||
location where the file is to be restored.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if new_path is None:
|
||||
logger.debug("Restoring system configuration file '%s'",
|
||||
path)
|
||||
else:
|
||||
logger.debug("Restoring system configuration file '%s' to '%s'",
|
||||
path, new_path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
if new_path is not None and not os.path.isabs(new_path):
|
||||
raise ValueError("Absolute new path required")
|
||||
|
||||
mode = None
|
||||
uid = None
|
||||
gid = None
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
(mode,uid,gid,filepath) = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
if new_path is not None:
|
||||
path = new_path
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def restore_all_files(self):
|
||||
"""Restore the files in the inbdex to their original
|
||||
location and delete the copy.
|
||||
|
||||
Returns #True if the file was restored, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
if len(self.files) == 0:
|
||||
return False
|
||||
|
||||
for (filename, value) in self.files.items():
|
||||
|
||||
(mode,uid,gid,path) = value.split(',', 3)
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
continue
|
||||
|
||||
shutil.copy(backup_path, path) # SELinux needs copy
|
||||
os.remove(backup_path)
|
||||
|
||||
os.chown(path, int(uid), int(gid))
|
||||
os.chmod(path, int(mode))
|
||||
|
||||
tasks.restore_context(path)
|
||||
|
||||
# force file to be deleted
|
||||
self.files = {}
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
def has_files(self):
|
||||
"""Return True or False if there are any files in the index
|
||||
|
||||
Can be used to determine if a program is configured.
|
||||
"""
|
||||
|
||||
return len(self.files) > 0
|
||||
|
||||
def untrack_file(self, path):
|
||||
"""Remove file at path @path from list of backed up files.
|
||||
|
||||
Does not remove any files from the filesystem.
|
||||
|
||||
Returns #True if the file was untracked, #False if there
|
||||
was no backup file to restore
|
||||
"""
|
||||
|
||||
logger.debug("Untracking system configuration file '%s'", path)
|
||||
|
||||
if not os.path.isabs(path):
|
||||
raise ValueError("Absolute path required")
|
||||
|
||||
filename = None
|
||||
|
||||
for (key, value) in self.files.items():
|
||||
_mode, _uid, _gid, filepath = value.split(',', 3)
|
||||
if (filepath == path):
|
||||
filename = key
|
||||
break
|
||||
|
||||
if not filename:
|
||||
raise ValueError("No such file name in the index")
|
||||
|
||||
backup_path = os.path.join(self._path, filename)
|
||||
if not os.path.exists(backup_path):
|
||||
logger.debug(" -> Not restoring - '%s' doesn't exist",
|
||||
backup_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
os.unlink(backup_path)
|
||||
except Exception as e:
|
||||
logger.error('Error removing %s: %s', backup_path, str(e))
|
||||
|
||||
del self.files[filename]
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class StateFile(object):
|
||||
"""A metadata file for recording system state which can
|
||||
be backed up and later restored.
|
||||
StateFile gets reloaded every time to prevent loss of information
|
||||
recorded by child processes. But we do not solve concurrency
|
||||
because there is no need for it right now.
|
||||
The format is something like:
|
||||
|
||||
[httpd]
|
||||
running=True
|
||||
enabled=False
|
||||
"""
|
||||
|
||||
def __init__(self, path = SYSRESTORE_PATH, state_file = SYSRESTORE_STATEFILE):
|
||||
"""Create a StateFile object, loading from @path.
|
||||
|
||||
The dictionary @modules, a member of the returned object,
|
||||
is where the state can be modified. @modules is indexed
|
||||
using a module name to return another dictionary containing
|
||||
key/value pairs with the saved state of that module.
|
||||
|
||||
The keys in these latter dictionaries are arbitrary strings
|
||||
and the values may either be strings or booleans.
|
||||
"""
|
||||
self._path = os.path.join(path, state_file)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load the modules from the file @_path. @modules will
|
||||
be an empty dictionary if the file doesn't exist.
|
||||
"""
|
||||
logger.debug("Loading StateFile from '%s'", self._path)
|
||||
|
||||
self.modules = {}
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
p.read(self._path)
|
||||
|
||||
for module in p.sections():
|
||||
self.modules[module] = {}
|
||||
for (key, value) in p.items(module):
|
||||
if value == str(True):
|
||||
value = True
|
||||
elif value == str(False):
|
||||
value = False
|
||||
self.modules[module][key] = value
|
||||
|
||||
def save(self):
|
||||
"""Save the modules to @_path. If @modules is an empty
|
||||
dict, then @_path should be removed.
|
||||
"""
|
||||
logger.debug("Saving StateFile to '%s'", self._path)
|
||||
|
||||
for module in list(self.modules):
|
||||
if len(self.modules[module]) == 0:
|
||||
del self.modules[module]
|
||||
|
||||
if len(self.modules) == 0:
|
||||
logger.debug(" -> no modules, removing file")
|
||||
if os.path.exists(self._path):
|
||||
os.remove(self._path)
|
||||
return
|
||||
|
||||
p = SafeConfigParser()
|
||||
p.optionxform = str
|
||||
|
||||
for module in self.modules:
|
||||
p.add_section(module)
|
||||
for (key, value) in self.modules[module].items():
|
||||
p.set(module, key, str(value))
|
||||
|
||||
with open(self._path, "w") as f:
|
||||
p.write(f)
|
||||
|
||||
def backup_state(self, module, key, value):
|
||||
"""Backup an item of system state from @module, identified
|
||||
by the string @key and with the value @value. @value may be
|
||||
a string or boolean.
|
||||
"""
|
||||
if not isinstance(value, (str, bool, unicode)):
|
||||
raise ValueError("Only strings, booleans or unicode strings are supported")
|
||||
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
self.modules[module] = {}
|
||||
|
||||
if key not in self.modules:
|
||||
self.modules[module][key] = value
|
||||
|
||||
self.save()
|
||||
|
||||
def get_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
if module not in self.modules:
|
||||
return None
|
||||
|
||||
return self.modules[module].get(key, None)
|
||||
|
||||
def delete_state(self, module, key):
|
||||
"""Delete system state from @module, identified by the string
|
||||
@key.
|
||||
|
||||
If the item doesn't exist, no change is done.
|
||||
"""
|
||||
self._load()
|
||||
|
||||
try:
|
||||
del self.modules[module][key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.save()
|
||||
|
||||
def restore_state(self, module, key):
|
||||
"""Return the value of an item of system state from @module,
|
||||
identified by the string @key, and remove it from the backed
|
||||
up system state.
|
||||
|
||||
If the item doesn't exist, #None will be returned, otherwise
|
||||
the original string or boolean value is returned.
|
||||
"""
|
||||
|
||||
value = self.get_state(module, key)
|
||||
|
||||
if value is not None:
|
||||
self.delete_state(module, key)
|
||||
|
||||
return value
|
||||
|
||||
def has_state(self, module):
|
||||
"""Return True or False if there is any state stored for @module.
|
||||
|
||||
Can be used to determine if a service is configured.
|
||||
"""
|
||||
|
||||
return module in self.modules
|
||||
Reference in New Issue
Block a user