mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Create a common place to retrieve facts about an IPA installation
This is common to both client and server. Start with whether the client or server is configured. https://pagure.io/freeipa/issue/8384 Signed-off-by: Rob Crittenden <rcritten@redhat.com> Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Francois Cami <fcami@redhat.com>
This commit is contained in:
committed by
Florence Blanc-Renaud
parent
4758db121e
commit
d7a4756dac
@@ -945,7 +945,7 @@ fi
|
||||
|
||||
%posttrans server
|
||||
# don't execute upgrade and restart of IPA when server is not installed
|
||||
%{__python3} -c "import sys; from ipaserver.install import installutils; sys.exit(0 if installutils.is_ipa_configured() else 1);" > /dev/null 2>&1
|
||||
%{__python3} -c "import sys; from ipalib import facts; sys.exit(0 if facts.is_ipa_configured() else 1);" > /dev/null 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# This is necessary for Fedora system upgrades which by default
|
||||
@@ -1024,7 +1024,7 @@ fi
|
||||
|
||||
|
||||
%posttrans server-trust-ad
|
||||
%{__python3} -c "import sys; from ipaserver.install import installutils; sys.exit(0 if installutils.is_ipa_configured() else 1);" > /dev/null 2>&1
|
||||
%{__python3} -c "import sys; from ipalib import facts; sys.exit(0 if facts.is_ipa_configured() else 1);" > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
# NOTE: systemd specific section
|
||||
/bin/systemctl try-restart httpd.service >/dev/null 2>&1 || :
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from ipaserver import dcerpc
|
||||
from ipaserver.install.installutils import is_ipa_configured, ScriptError
|
||||
from ipaserver.install.installutils import ScriptError
|
||||
from ipapython import config, ipautil
|
||||
from ipalib import api
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipapython.dn import DN
|
||||
from ipapython.dnsutil import DNSName
|
||||
from ipaplatform.constants import constants
|
||||
|
||||
@@ -18,9 +18,9 @@ from jwcrypto.common import json_decode
|
||||
from jwcrypto.jwk import JWK
|
||||
|
||||
from ipalib import api
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipaplatform.paths import paths
|
||||
import ipapython.version
|
||||
from ipaserver.install.installutils import is_ipa_configured
|
||||
|
||||
try:
|
||||
# FreeIPA >= 4.5
|
||||
|
||||
@@ -35,8 +35,10 @@ from configparser import RawConfigParser
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from ipalib import api, errors, x509
|
||||
from ipalib import sysrestore
|
||||
from ipalib.constants import IPAAPI_USER, MAXHOSTNAMELEN
|
||||
from ipalib.install import certmonger, certstore, service, sysrestore
|
||||
from ipalib.facts import is_ipa_client_configured
|
||||
from ipalib.install import certmonger, certstore, service
|
||||
from ipalib.install import hostname as hostname_
|
||||
from ipalib.install.kinit import kinit_keytab, kinit_password
|
||||
from ipalib.install.service import enroll_only, prepare_only
|
||||
@@ -273,22 +275,12 @@ def is_ipa_client_installed(on_master=False):
|
||||
the existence of default.conf file is not taken into consideration,
|
||||
since it has been already created by ipa-server-install.
|
||||
"""
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
installed = statestore.get_state('installation', 'complete')
|
||||
if installed is not None:
|
||||
return installed
|
||||
|
||||
# Fall back to the old detection
|
||||
|
||||
installed = (
|
||||
fstore.has_files() or (
|
||||
not on_master and os.path.exists(paths.IPA_DEFAULT_CONF)
|
||||
)
|
||||
warnings.warn(
|
||||
"Use 'ipalib.facts.is_ipa_client_configured'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
|
||||
return installed
|
||||
return is_ipa_client_configured(on_master)
|
||||
|
||||
|
||||
def configure_nsswitch_database(fstore, database, services, preserve=True,
|
||||
@@ -2094,7 +2086,7 @@ def install_check(options):
|
||||
|
||||
tasks.check_selinux_status()
|
||||
|
||||
if is_ipa_client_installed(on_master=options.on_master):
|
||||
if is_ipa_client_configured(on_master=options.on_master):
|
||||
logger.error("IPA client is already configured on this system.")
|
||||
logger.info(
|
||||
"If you want to reinstall the IPA client, uninstall it first "
|
||||
@@ -3202,7 +3194,7 @@ def _install(options):
|
||||
def uninstall_check(options):
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
if not is_ipa_client_installed():
|
||||
if not is_ipa_client_configured():
|
||||
if options.on_master:
|
||||
rval = SUCCESS
|
||||
else:
|
||||
|
||||
@@ -39,9 +39,9 @@ from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from email.utils import make_msgid
|
||||
|
||||
from ipaclient.install.client import is_ipa_client_installed
|
||||
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
|
||||
|
||||
@@ -254,7 +254,7 @@ class EPN(admintool.AdminTool):
|
||||
def run(self):
|
||||
super(EPN, self).run()
|
||||
|
||||
if not is_ipa_client_installed():
|
||||
if not is_ipa_client_configured():
|
||||
logger.error("IPA client is not configured on this system.")
|
||||
raise admintool.ScriptError()
|
||||
|
||||
|
||||
43
ipalib/facts.py
Normal file
43
ipalib/facts.py
Normal file
@@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""
|
||||
Facts about the installation
|
||||
"""
|
||||
|
||||
from . import sysrestore
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
|
||||
def is_ipa_configured():
|
||||
"""
|
||||
Use the state to determine if IPA has been configured.
|
||||
"""
|
||||
sstore = sysrestore.StateFile(paths.SYSRESTORE)
|
||||
return sstore.get_state('installation', 'complete')
|
||||
|
||||
|
||||
def is_ipa_client_configured(on_master=False):
|
||||
"""
|
||||
Consider IPA client not installed if nothing is backed up
|
||||
and default.conf file does not exist. If on_master is set to True,
|
||||
the existence of default.conf file is not taken into consideration,
|
||||
since it has been already created by ipa-server-install.
|
||||
"""
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
installed = statestore.get_state('installation', 'complete')
|
||||
if installed is not None:
|
||||
return installed
|
||||
|
||||
# Fall back to the old detection
|
||||
|
||||
installed = (
|
||||
fstore.has_files() or (
|
||||
not on_master and os.path.exists(paths.IPA_DEFAULT_CONF)
|
||||
)
|
||||
)
|
||||
|
||||
return installed
|
||||
@@ -1,457 +1,19 @@
|
||||
# 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/>.
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
#
|
||||
# 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
|
||||
"""
|
||||
Facade for ipalib.sysrestore for backwards compatibility
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from ipalib import sysrestore as real_sysrestore
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
class FileStore(real_sysrestore.FileStore):
|
||||
def __init__(self, path=real_sysrestore.SYSRESTORE_PATH,
|
||||
index_file=real_sysrestore.SYSRESTORE_INDEXFILE):
|
||||
super(FileStore, self).__init__(path, index_file)
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
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:
|
||||
"""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 - as long as an exact 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)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
cont_hash = sha256(f.read()).hexdigest()
|
||||
|
||||
filename = "{hexhash}-{bcppath}".format(
|
||||
hexhash=cont_hash, bcppath=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:
|
||||
"""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
|
||||
class StateFile(real_sysrestore.StateFile):
|
||||
def __init__(self, path=real_sysrestore.SYSRESTORE_PATH,
|
||||
state_file=real_sysrestore.SYSRESTORE_STATEFILE):
|
||||
super(StateFile, self).__init__(path, state_file)
|
||||
|
||||
457
ipalib/sysrestore.py
Normal file
457
ipalib/sysrestore.py
Normal file
@@ -0,0 +1,457 @@
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import random
|
||||
|
||||
from hashlib import sha256
|
||||
|
||||
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:
|
||||
"""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 - as long as an exact 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)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
cont_hash = sha256(f.read()).hexdigest()
|
||||
|
||||
filename = "{hexhash}-{bcppath}".format(
|
||||
hexhash=cont_hash, bcppath=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:
|
||||
"""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
|
||||
@@ -43,7 +43,7 @@ from dns.exception import DNSException
|
||||
import ldap
|
||||
import six
|
||||
|
||||
from ipalib.install import sysrestore
|
||||
from ipalib import facts, sysrestore
|
||||
from ipalib.install.kinit import kinit_password
|
||||
import ipaplatform
|
||||
from ipapython import ipautil, admintool, version, ipaldap
|
||||
@@ -702,8 +702,12 @@ def is_ipa_configured():
|
||||
"""
|
||||
Use the state to determine if IPA has been configured.
|
||||
"""
|
||||
sstore = sysrestore.StateFile(paths.SYSRESTORE)
|
||||
return sstore.get_state('installation', 'complete')
|
||||
warnings.warn(
|
||||
"Use 'ipalib.facts.is_ipa_configured'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return facts.is_ipa_configured()
|
||||
|
||||
|
||||
def run_script(main_function, operation_name, log_file_name=None,
|
||||
|
||||
@@ -32,13 +32,13 @@ import shutil
|
||||
|
||||
from ipalib import api
|
||||
from ipalib import x509
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.admintool import AdminTool
|
||||
from ipapython.certdb import NSSDatabase, EMPTY_TRUST_FLAGS
|
||||
from ipapython.dn import DN
|
||||
from ipapython.ipaldap import realm_to_serverid
|
||||
from ipaserver.install import ca, cainstance, dsinstance
|
||||
from ipaserver.install.installutils import is_ipa_configured
|
||||
from ipapython import ipautil
|
||||
|
||||
msg = """
|
||||
|
||||
@@ -27,9 +27,10 @@ import ldapurl
|
||||
|
||||
from ipaserver.install import service, installutils
|
||||
from ipaserver.install.dsinstance import config_dirname
|
||||
from ipaserver.install.installutils import is_ipa_configured, ScriptError
|
||||
from ipaserver.install.installutils import ScriptError
|
||||
from ipaserver.masters import ENABLED_SERVICE, HIDDEN_SERVICE
|
||||
from ipalib import api, errors
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipapython.ipaldap import LDAPClient, realm_to_serverid
|
||||
from ipapython.ipautil import wait_for_open_ports, wait_for_open_socket
|
||||
from ipapython.ipautil import run
|
||||
|
||||
@@ -32,6 +32,7 @@ from ipaplatform.paths import paths
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipalib import api, errors, x509
|
||||
from ipalib.constants import DOMAIN_LEVEL_0
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipalib.util import (
|
||||
validate_domain_name,
|
||||
no_matching_interface_for_ip_address_warning,
|
||||
@@ -43,8 +44,8 @@ from ipaserver.install import (
|
||||
sysupgrade, cainstance)
|
||||
from ipaserver.install.installutils import (
|
||||
IPA_MODULES, BadHostError, get_fqdn, get_server_ip_address,
|
||||
is_ipa_configured, load_pkcs12, read_password, verify_fqdn,
|
||||
update_hosts_file, validate_mask)
|
||||
load_pkcs12, read_password, verify_fqdn, update_hosts_file,
|
||||
validate_mask)
|
||||
|
||||
if six.PY3:
|
||||
unicode = str
|
||||
|
||||
@@ -35,13 +35,14 @@ from ipaplatform.tasks import tasks
|
||||
from ipaplatform.paths import paths
|
||||
from ipalib import api, constants, create_api, errors, rpc, x509
|
||||
from ipalib.config import Env
|
||||
from ipalib.facts import is_ipa_configured
|
||||
from ipalib.util import no_matching_interface_for_ip_address_warning
|
||||
from ipaclient.install.client import configure_krb5_conf, purge_host_keytab
|
||||
from ipaserver.install import (
|
||||
adtrust, bindinstance, ca, dns, dsinstance, httpinstance,
|
||||
installutils, kra, krbinstance, otpdinstance, custodiainstance, service)
|
||||
from ipaserver.install.installutils import (
|
||||
ReplicaConfig, load_pkcs12, is_ipa_configured, validate_mask)
|
||||
ReplicaConfig, load_pkcs12, validate_mask)
|
||||
from ipaserver.install.replication import (
|
||||
ReplicationManager, replica_conn_check)
|
||||
from ipaserver.masters import find_providing_servers, find_providing_server
|
||||
|
||||
@@ -22,7 +22,9 @@ from augeas import Augeas
|
||||
|
||||
from ipalib import api, x509
|
||||
from ipalib.constants import RENEWAL_CA_NAME, RA_AGENT_PROFILE, IPA_CA_RECORD
|
||||
from ipalib.install import certmonger, sysrestore
|
||||
from ipalib.install import certmonger
|
||||
from ipalib import sysrestore
|
||||
from ipalib.facts import is_ipa_configured
|
||||
import SSSDConfig
|
||||
import ipalib.util
|
||||
import ipalib.errors
|
||||
@@ -1484,7 +1486,7 @@ def upgrade_configuration():
|
||||
fstore = sysrestore.FileStore(paths.SYSRESTORE)
|
||||
sstore = sysrestore.StateFile(paths.SYSRESTORE)
|
||||
|
||||
if installutils.is_ipa_configured() is None:
|
||||
if is_ipa_configured() is None:
|
||||
sstore.backup_state('installation', 'complete', True)
|
||||
|
||||
fqdn = api.env.host
|
||||
|
||||
Reference in New Issue
Block a user