Refactor backup_and_replace_hostname() into a flexible config modification tool

backup_and_replace_hostname() was doing three things:
    1. Given config file in 'key=value' style, replace value for a
       specified key (HOSTNAME)
    2. Backup original file and install a replacement
    3. Restore original security context after editing

We have several more places where parts of the functionality are needed,
thus making two tools in ipapython.ipautil:

    1. config_replace_variables(filepath, replacevars=dict(),
                                appendvars=dict())
       Replaces or appends values to specified keys, adding new key=value
       pairs if key was absent

    2. backup_config_and_replace_variables(fstore, filepath,
                                           replacevars=dict(),
                                           appendvars=dict())
       Backups config file and calls config_replace_variables()

A caller must handle security context after using these two tools.

In addition, as before, there is
ipapython.services.backup_and_replace_hostname() that uses
these common tools and restores security context after editing.

The code will be used extensively for systemd integration for Fedora 16.

Fixes:
    https://fedorahosted.org/freeipa/ticket/1871
This commit is contained in:
Alexander Bokovoy 2011-10-12 16:42:09 +03:00 committed by Rob Crittenden
parent 5c10f66e4a
commit 8badce286f
2 changed files with 97 additions and 40 deletions

View File

@ -1185,3 +1185,93 @@ def get_ipa_basedn(conn):
return None
def config_replace_variables(filepath, replacevars=dict(), appendvars=dict()):
"""
Take a key=value based configuration file, and write new version
with certain values replaced or appended
All (key,value) pairs from replacevars and appendvars that were not found
in the configuration file, will be added there.
It is responsibility of a caller to ensure that replacevars and
appendvars do not overlap.
It is responsibility of a caller to back up file.
returns dictionary of affected keys and their previous values
One have to run restore_context(filepath) afterwards or
security context of the file will not be correct after modification
"""
pattern = re.compile('''
(^
\s*
(?P<option> [^\#;]+?)
(\s*=\s*)
(?P<value> .+?)?
(\s*((\#|;).*)?)?
$)''', re.VERBOSE)
orig_stat = os.stat(filepath)
old_values = dict()
temp_filename = None
with tempfile.NamedTemporaryFile(delete=False) as new_config:
temp_filename = new_config.name
with open(filepath, 'r') as f:
for line in f:
new_line = line
m = pattern.match(line)
if m:
option, value = m.group('option', 'value')
if option is not None:
if replacevars and option in replacevars:
# replace value completely
new_line = u"%s=%s\n" % (option, replacevars[option])
old_values[option] = value
if appendvars and option in appendvars:
# append new value unless it is already existing in the original one
if value.find(appendvars[option]) == -1:
new_line = u"%s=%s %s\n" % (option, value, appendvars[option])
old_values[option] = value
new_config.write(new_line)
# Now add all options from replacevars and appendvars that were not found in the file
new_vars = replacevars.copy()
new_vars.update(appendvars)
newvars_view = new_vars.viewkeys() - old_values.viewkeys()
append_view = (appendvars.viewkeys() - replacevars.viewkeys()) - old_values.viewkeys()
for item in newvars_view:
new_config.write("%s=%s\n" % (item,new_vars[item]))
for item in append_view:
new_config.write("%s=%s\n" % (item,appendvars[item]))
new_config.flush()
# Make sure the resulting file is readable by others before installing it
os.fchmod(new_config.fileno(), orig_stat.st_mode)
os.fchown(new_config.fileno(), orig_stat.st_uid, orig_stat.st_gid)
# At this point new_config is closed but not removed due to 'delete=False' above
# Now, install the temporary file as configuration and ensure old version is available as .orig
# While .orig file is not used during uninstall, it is left there for administrator.
install_file(temp_filename, filepath)
return old_values
def backup_config_and_replace_variables(fstore, filepath, replacevars=dict(), appendvars=dict()):
"""
Take a key=value based configuration file, back up it, and
write new version with certain values replaced or appended
All (key,value) pairs from replacevars and appendvars that
were not found in the configuration file, will be added there.
It is responsibility of a caller to ensure that replacevars and
appendvars do not overlap.
returns dictionary of affected keys and their previous values
One have to run restore_context(filepath) afterwards or
security context of the file will not be correct after modification
"""
# Backup original filepath
fstore.backup_file(filepath)
old_values = config_replace_variables(filepath, replacevars, appendvars)
return old_values

View File

@ -133,48 +133,15 @@ def restore_context(filepath):
ipautil.run(["/sbin/restorecon", filepath], raiseonerr=False)
def backup_and_replace_hostname(fstore, statestore, hostname):
network_filename = "/etc/sysconfig/network"
# Backup original /etc/sysconfig/network
fstore.backup_file(network_filename)
hostname_pattern = re.compile('''
(^
\s*
(?P<option> [^\#;]+?)
(\s*=\s*)
(?P<value> .+?)?
(\s*((\#|;).*)?)?
$)''', re.VERBOSE)
temp_filename = None
with tempfile.NamedTemporaryFile(delete=False) as new_config:
temp_filename = new_config.name
with open(network_filename, 'r') as f:
for line in f:
new_line = line
m = hostname_pattern.match(line)
if m:
option, value = m.group('option', 'value')
if option is not None and option == 'HOSTNAME':
if value is not None and hostname != value:
new_line = u"HOSTNAME=%s\n" % (hostname)
statestore.backup_state('network', 'hostname', value)
new_config.write(new_line)
new_config.flush()
# Make sure the resulting file is readable by others before installing it
os.fchmod(new_config.fileno(), stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
os.fchown(new_config.fileno(), 0, 0)
# At this point new_config is closed but not removed due to 'delete=False' above
# Now, install the temporary file as configuration and ensure old version is available as .orig
# While .orig file is not used during uninstall, it is left there for administrator.
ipautil.install_file(temp_filename, network_filename)
try:
ipautil.run(['/bin/hostname', hostname])
except ipautil.CalledProcessError, e:
print >>sys.stderr, "Failed to set this machine hostname to %s (%s)." % (hostname, str(e))
# For SE Linux environments it is important to reset SE labels to the expected ones
try:
restore_context(network_filename)
except ipautil.CalledProcessError, e:
print >>sys.stderr, "Failed to set permissions for %s (%s)." % (network_filename, str(e))
replacevars = {'HOSTNAME':hostname}
old_values = ipautil.backup_config_and_replace_variables(fstore,
"/etc/sysconfig/network",
replacevars=replacevars)
restore_context("/etc/sysconfig/network")
if 'HOSTNAME' in old_values:
statestore.backup_state('network', 'hostname', old_values['HOSTNAME'])