From d7a4756dac5140d4416312ed92270eb3249c83e3 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Wed, 8 Jul 2020 10:16:17 -0400 Subject: [PATCH] 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 Reviewed-By: Alexander Bokovoy Reviewed-By: Francois Cami --- freeipa.spec.in | 4 +- .../com.redhat.idm.trust-fetch-domains.in | 3 +- install/tools/ipa-custodia-check.in | 2 +- ipaclient/install/client.py | 28 +- ipaclient/install/ipa_epn.py | 4 +- ipalib/facts.py | 43 ++ ipalib/install/sysrestore.py | 464 +----------------- ipalib/sysrestore.py | 457 +++++++++++++++++ ipaserver/install/installutils.py | 10 +- ipaserver/install/ipa_cert_fix.py | 2 +- ipaserver/install/ipactl.py | 3 +- ipaserver/install/server/install.py | 5 +- ipaserver/install/server/replicainstall.py | 3 +- ipaserver/install/server/upgrade.py | 6 +- 14 files changed, 549 insertions(+), 485 deletions(-) create mode 100644 ipalib/facts.py create mode 100644 ipalib/sysrestore.py diff --git a/freeipa.spec.in b/freeipa.spec.in index 4cea29afd..c831cc13c 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -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 || : diff --git a/install/oddjob/com.redhat.idm.trust-fetch-domains.in b/install/oddjob/com.redhat.idm.trust-fetch-domains.in index c002b1bb3..616c8bf9e 100644 --- a/install/oddjob/com.redhat.idm.trust-fetch-domains.in +++ b/install/oddjob/com.redhat.idm.trust-fetch-domains.in @@ -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 diff --git a/install/tools/ipa-custodia-check.in b/install/tools/ipa-custodia-check.in index 7fdfbff52..5143dc498 100644 --- a/install/tools/ipa-custodia-check.in +++ b/install/tools/ipa-custodia-check.in @@ -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 diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 2aa8b5f7d..da671361b 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -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: diff --git a/ipaclient/install/ipa_epn.py b/ipaclient/install/ipa_epn.py index 666c703e5..65f9f3d47 100644 --- a/ipaclient/install/ipa_epn.py +++ b/ipaclient/install/ipa_epn.py @@ -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() diff --git a/ipalib/facts.py b/ipalib/facts.py new file mode 100644 index 000000000..a1e3e46bc --- /dev/null +++ b/ipalib/facts.py @@ -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 diff --git a/ipalib/install/sysrestore.py b/ipalib/install/sysrestore.py index 5fd52b8ed..930414ec1 100644 --- a/ipalib/install/sysrestore.py +++ b/ipalib/install/sysrestore.py @@ -1,457 +1,19 @@ -# Authors: Mark McLoughlin # -# 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 . +# 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) diff --git a/ipalib/sysrestore.py b/ipalib/sysrestore.py new file mode 100644 index 000000000..5fd52b8ed --- /dev/null +++ b/ipalib/sysrestore.py @@ -0,0 +1,457 @@ +# Authors: Mark McLoughlin +# +# 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 . +# + +# +# 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 diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index f19f64fbe..132122a6d 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -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, diff --git a/ipaserver/install/ipa_cert_fix.py b/ipaserver/install/ipa_cert_fix.py index 6b952d34f..1b2f5430e 100644 --- a/ipaserver/install/ipa_cert_fix.py +++ b/ipaserver/install/ipa_cert_fix.py @@ -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 = """ diff --git a/ipaserver/install/ipactl.py b/ipaserver/install/ipactl.py index f5113548a..f813d0d72 100644 --- a/ipaserver/install/ipactl.py +++ b/ipaserver/install/ipactl.py @@ -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 diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py index 137eee2b0..96311731f 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -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 diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index b8e896ac7..fb47781e4 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -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 diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index af403c6da..22d024b16 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -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