Imported Debian patch 4.8.10-2
This commit is contained in:
committed by
Mario Fetka
parent
8bc559c5a1
commit
358acdd85f
@@ -46,8 +46,39 @@ DBUS_CM_REQUEST_IF = 'org.fedorahosted.certmonger.request'
|
||||
DBUS_CM_CA_IF = 'org.fedorahosted.certmonger.ca'
|
||||
DBUS_PROPERTY_IF = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
# These properties, if encountered in search criteria, result in a
|
||||
# subset test instead of equality test.
|
||||
ARRAY_PROPERTIES = ['template-hostname']
|
||||
|
||||
class _cm_dbus_object(object):
|
||||
|
||||
"""
|
||||
Certmonger helper routines.
|
||||
|
||||
Search criteria
|
||||
---------------
|
||||
|
||||
Functions that look up requests take a ``dict`` of search criteria.
|
||||
In general, the key is a name of a property in the request property
|
||||
interface. But there are some special cases with different
|
||||
behaviour:
|
||||
|
||||
``nickname``
|
||||
a.k.a. "request ID". If given, only the specified request is
|
||||
retrieved (if it exists), and it is still tested against other
|
||||
criteria.
|
||||
|
||||
``ca-name``
|
||||
Test equality against the nickname of the CA (a.k.a. request
|
||||
helper) object for the request.
|
||||
|
||||
``template-hostname``
|
||||
Must be an iterable of DNS names. Tests that the given values
|
||||
are a subset of the values defined on the Certmonger request.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class _cm_dbus_object:
|
||||
"""
|
||||
Auxiliary class for convenient DBus object handling.
|
||||
"""
|
||||
@@ -156,9 +187,12 @@ class _certmonger(_cm_dbus_object):
|
||||
DBUS_CM_IF)
|
||||
|
||||
|
||||
def _get_requests(criteria=dict()):
|
||||
def _get_requests(criteria):
|
||||
"""
|
||||
Get all requests that matches the provided criteria.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
|
||||
"""
|
||||
if not isinstance(criteria, dict):
|
||||
raise TypeError('"criteria" must be dict.')
|
||||
@@ -179,13 +213,23 @@ def _get_requests(criteria=dict()):
|
||||
for criterion in criteria:
|
||||
if criterion == 'ca-name':
|
||||
ca_path = request.obj_if.get_ca()
|
||||
if ca_path is None:
|
||||
raise RuntimeError("certmonger CA '%s' is not defined" %
|
||||
criteria.get('ca-name'))
|
||||
ca = _cm_dbus_object(cm.bus, cm, ca_path, DBUS_CM_CA_IF,
|
||||
DBUS_CM_IF)
|
||||
value = ca.obj_if.get_nickname()
|
||||
if criteria[criterion] != ca.obj_if.get_nickname():
|
||||
break
|
||||
elif criterion in ARRAY_PROPERTIES:
|
||||
# perform subset test
|
||||
expect = set(criteria[criterion])
|
||||
got = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if not expect.issubset(got):
|
||||
break
|
||||
else:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, criterion)
|
||||
if value != criteria[criterion]:
|
||||
break
|
||||
if criteria[criterion] != value:
|
||||
break
|
||||
else:
|
||||
requests.append(request)
|
||||
|
||||
@@ -194,11 +238,11 @@ def _get_requests(criteria=dict()):
|
||||
|
||||
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.
|
||||
Find request that matches criteria. Return ``None`` if no match.
|
||||
Raise ``RuntimeError`` if there is more than one matching request.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
|
||||
"""
|
||||
requests = _get_requests(criteria)
|
||||
if len(requests) == 0:
|
||||
@@ -236,11 +280,11 @@ 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.
|
||||
Return ``None`` if no match. Raise ``RuntimeError`` if there is
|
||||
more than one matching request.
|
||||
|
||||
:param criteria: dict of criteria; see module doc for details
|
||||
|
||||
None is returned if none of the criteria match.
|
||||
"""
|
||||
try:
|
||||
request = _get_request(criteria)
|
||||
@@ -306,7 +350,7 @@ 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,
|
||||
resubmit_timeout=0):
|
||||
resubmit_timeout=0, stop_tracking_on_error=False):
|
||||
"""Request certificate, wait and possibly resubmit failing requests
|
||||
|
||||
Submit a cert request to certmonger and wait until the request has
|
||||
@@ -326,7 +370,11 @@ def request_and_wait_for_cert(
|
||||
|
||||
deadline = time.time() + resubmit_timeout
|
||||
while True: # until success, timeout, or error
|
||||
state = wait_for_request(req_id, api.env.replication_wait_timeout)
|
||||
try:
|
||||
state = wait_for_request(req_id, api.env.http_timeout)
|
||||
except RuntimeError as e:
|
||||
logger.debug("wait_for_request raised %s", e)
|
||||
state = 'TIMEOUT'
|
||||
ca_error = get_request_value(req_id, 'ca-error')
|
||||
if state == 'MONITORING' and ca_error is None:
|
||||
# we got a winner, exiting
|
||||
@@ -336,22 +384,28 @@ def request_and_wait_for_cert(
|
||||
logger.debug(
|
||||
"Cert request %s failed: %s (%s)", req_id, state, ca_error
|
||||
)
|
||||
if state not in {'CA_REJECTED', 'CA_UNREACHABLE'}:
|
||||
if state in {'CA_REJECTED', 'CA_UNREACHABLE'}:
|
||||
# probably unrecoverable error
|
||||
logger.debug("Giving up on cert request %s", req_id)
|
||||
break
|
||||
elif not resubmit_timeout:
|
||||
if not resubmit_timeout:
|
||||
# no resubmit
|
||||
break
|
||||
elif time.time() > deadline:
|
||||
logger.debug("Request %s reached resubmit dead line", req_id)
|
||||
if time.time() > deadline:
|
||||
logger.debug("Request %s reached resubmit deadline", req_id)
|
||||
break
|
||||
if state == 'TIMEOUT':
|
||||
logger.debug("%s not in final state, continue waiting", req_id)
|
||||
time.sleep(10)
|
||||
else:
|
||||
# sleep and resubmit
|
||||
logger.debug("Sleep and resubmit cert request %s", req_id)
|
||||
time.sleep(10)
|
||||
resubmit_request(req_id)
|
||||
|
||||
if stop_tracking_on_error:
|
||||
stop_tracking(request_id=req_id)
|
||||
|
||||
raise RuntimeError(
|
||||
"Certificate issuance failed ({}: {})".format(state, ca_error)
|
||||
)
|
||||
@@ -427,7 +481,8 @@ def request_cert(
|
||||
|
||||
def start_tracking(
|
||||
certpath, ca='IPA', nickname=None, pin=None, pinfile=None,
|
||||
pre_command=None, post_command=None, profile=None, storage="NSSDB"):
|
||||
pre_command=None, post_command=None, profile=None, storage="NSSDB",
|
||||
token_name=None, dns=None):
|
||||
"""
|
||||
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.
|
||||
@@ -460,6 +515,10 @@ def start_tracking(
|
||||
NSS or OpenSSL backend to track the certificate in ``certpath``
|
||||
:param profile:
|
||||
Which certificate profile should be used.
|
||||
:param token_name:
|
||||
Hardware token name for HSM support
|
||||
:param dns:
|
||||
List of DNS names
|
||||
:returns: certificate tracking nickname.
|
||||
"""
|
||||
if storage == 'FILE':
|
||||
@@ -500,6 +559,12 @@ def start_tracking(
|
||||
params['cert-postsave-command'] = post_command
|
||||
if profile:
|
||||
params['ca-profile'] = profile
|
||||
if token_name not in {None, "internal"}:
|
||||
# only pass token names for external tokens (e.g. HSM)
|
||||
params['key-token'] = token_name
|
||||
params['cert-token'] = token_name
|
||||
if dns is not None and len(dns) > 0:
|
||||
params['DNS'] = dns
|
||||
|
||||
result = cm.obj_if.add_request(params)
|
||||
try:
|
||||
@@ -590,6 +655,18 @@ def resubmit_request(
|
||||
update['template-is-ca'] = True
|
||||
update['template-ca-path-length'] = -1 # no path length
|
||||
|
||||
# TODO: certmonger assumes some hard-coded defaults like RSA 2048.
|
||||
# Try to fetch values from current cert rather.
|
||||
for key, convert in [('key-size', int), ('key-type', str)]:
|
||||
try:
|
||||
value = request.prop_if.Get(DBUS_CM_REQUEST_IF, key)
|
||||
except dbus.DBusException:
|
||||
continue
|
||||
else:
|
||||
if value:
|
||||
# convert dbus.Int64() to int, dbus.String() to str
|
||||
update[key] = convert(value)
|
||||
|
||||
if len(update) > 0:
|
||||
request.obj_if.modify(update)
|
||||
request.obj_if.resubmit()
|
||||
@@ -663,7 +740,7 @@ def modify_ca_helper(ca_name, helper):
|
||||
return old_helper
|
||||
|
||||
|
||||
def get_pin(token):
|
||||
def get_pin(token="internal"):
|
||||
"""
|
||||
Dogtag stores its NSS pin in a file formatted as token:PIN.
|
||||
|
||||
|
||||
155
ipalib/install/dnsforwarders.py
Normal file
155
ipalib/install/dnsforwarders.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#
|
||||
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
"""DNS forwarder and systemd-resolve1 helpers
|
||||
"""
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
import dbus
|
||||
|
||||
from ipaplatform.paths import paths
|
||||
from ipapython.dnsutil import get_ipa_resolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_SYSTEMD_RESOLV_CONF = {
|
||||
"/run/systemd/resolve/stub-resolv.conf",
|
||||
"/run/systemd/resolve/resolv.conf",
|
||||
"/lib/systemd/resolv.conf",
|
||||
"/usr/lib/systemd/resolv.conf",
|
||||
}
|
||||
|
||||
_DBUS_RESOLVE1_NAME = "org.freedesktop.resolve1"
|
||||
_DBUS_RESOLVE1_PATH = "/org/freedesktop/resolve1"
|
||||
_DBUS_RESOLVE1_MANAGER_IF = "org.freedesktop.resolve1.Manager"
|
||||
_DBUS_PROPERTY_IF = "org.freedesktop.DBus.Properties"
|
||||
|
||||
# netlink interface index for resolve1 global settings and loopback
|
||||
IFINDEX_GLOBAL = 0
|
||||
IFINDEX_LOOPBACK = 1
|
||||
|
||||
|
||||
def detect_resolve1_resolv_conf():
|
||||
"""Detect if /etc/resolv.conf is managed by systemd-resolved
|
||||
|
||||
See man(5) NetworkManager.conf
|
||||
"""
|
||||
try:
|
||||
dest = os.readlink(paths.RESOLV_CONF)
|
||||
except OSError:
|
||||
# not a link
|
||||
return False
|
||||
# convert path relative to /etc/resolv.conf to abs path
|
||||
dest = os.path.normpath(
|
||||
os.path.join(os.path.dirname(paths.RESOLV_CONF), dest)
|
||||
)
|
||||
return dest in _SYSTEMD_RESOLV_CONF
|
||||
|
||||
|
||||
def get_resolve1_nameservers(*, with_ifindex=False):
|
||||
"""Get list of DNS nameservers from systemd-resolved
|
||||
|
||||
:return: list of tuples (ifindex, ipaddress_obj)
|
||||
"""
|
||||
bus = dbus.SystemBus()
|
||||
try:
|
||||
resolve1 = bus.get_object(_DBUS_RESOLVE1_NAME, _DBUS_RESOLVE1_PATH)
|
||||
prop_if = dbus.Interface(resolve1, _DBUS_PROPERTY_IF)
|
||||
dns_prop = prop_if.Get(_DBUS_RESOLVE1_MANAGER_IF, "DNSEx")
|
||||
finally:
|
||||
bus.close()
|
||||
|
||||
results = []
|
||||
for ifindex, af, dns_arr, port, sniname in dns_prop:
|
||||
if port not in {0, 53} or sniname:
|
||||
# non-default port, non-standard port, or SNI name configuration
|
||||
# for DNS over TLS, e.g. 1.2.3.4:9953#example.com
|
||||
continue
|
||||
# convert packed format to IPAddress object (like inet_ntop)
|
||||
if af == socket.AF_INET:
|
||||
dnsip = ipaddress.IPv4Address(bytes(dns_arr))
|
||||
elif af == socket.AF_INET6:
|
||||
dnsip = ipaddress.IPv6Address(bytes(dns_arr))
|
||||
else:
|
||||
# neither IPv4 nor IPv6
|
||||
continue
|
||||
if with_ifindex:
|
||||
# netlink interface index, see socket.if_nameindex()
|
||||
ifindex = int(ifindex)
|
||||
results.append((ifindex, dnsip))
|
||||
else:
|
||||
results.append(dnsip)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_dnspython_nameservers(*, with_ifindex=False):
|
||||
"""Get list of DNS nameservers from dnspython
|
||||
|
||||
On Linux dnspython parses /etc/resolv.conf for us
|
||||
|
||||
:return: list of tuples (ifindex, ipaddress_obj)
|
||||
"""
|
||||
results = []
|
||||
for nameserver in get_ipa_resolver().nameservers:
|
||||
nameserver = ipaddress.ip_address(nameserver)
|
||||
if with_ifindex:
|
||||
results.append((IFINDEX_GLOBAL, nameserver))
|
||||
else:
|
||||
results.append(nameserver)
|
||||
return results
|
||||
|
||||
|
||||
def get_nameservers():
|
||||
"""Get list of unique, non-loopback DNS nameservers
|
||||
|
||||
:return: list of strings
|
||||
"""
|
||||
if detect_resolve1_resolv_conf():
|
||||
logger.debug(
|
||||
"systemd-resolved detected, fetching nameservers from D-Bus"
|
||||
)
|
||||
nameservers = get_resolve1_nameservers(with_ifindex=True)
|
||||
else:
|
||||
logger.debug(
|
||||
"systemd-resolved not detected, parsing %s", paths.RESOLV_CONF
|
||||
)
|
||||
nameservers = get_dnspython_nameservers(with_ifindex=True)
|
||||
|
||||
logger.debug("Detected nameservers: %r", nameservers)
|
||||
|
||||
result = []
|
||||
seen = set()
|
||||
for ifindex, ip in nameservers:
|
||||
# unique entries
|
||||
if ip in seen:
|
||||
continue
|
||||
seen.add(ip)
|
||||
# skip loopback
|
||||
if ifindex == IFINDEX_LOOPBACK or ip.is_loopback:
|
||||
continue
|
||||
result.append(str(ip))
|
||||
|
||||
logger.debug("Use nameservers %r", result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pprint import pprint
|
||||
|
||||
print("systemd-resolved detected:", detect_resolve1_resolv_conf())
|
||||
print("Interfaces:", socket.if_nameindex())
|
||||
print("dnspython nameservers:")
|
||||
pprint(get_dnspython_nameservers(with_ifindex=True))
|
||||
print("resolve1 nameservers:")
|
||||
try:
|
||||
pprint(get_resolve1_nameservers(with_ifindex=True))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("nameservers:", get_nameservers())
|
||||
@@ -1,457 +1,21 @@
|
||||
# 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
|
||||
|
||||
from hashlib import sha256
|
||||
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)
|
||||
|
||||
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 - 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(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
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user