Address startup issues, attempt to identify misconfiguration issues with proper errors

This commit is contained in:
James Kasten 2015-01-19 03:15:31 -08:00
parent 66884e05cf
commit 1dc7bfccc4
6 changed files with 471 additions and 363 deletions

View file

@ -95,12 +95,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"work": CONFIG.WORK_DIR}
super(ApacheConfigurator, self).__init__(direc)
# See if any temporary changes need to be recovered
# This needs to occur before VirtualHost objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.recovery_routine()
self.direc = direc
# Verify that all directories and files exist with proper permissions
if os.geteuid() == 0:
@ -184,7 +179,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.aug.set(path["cert_chain"][0], cert_chain)
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
(vhost.filep, vhost.addrs))
(vhost.filep,
", ".join(str(addr) for addr in vhost.addrs)))
self.save_notes += "\tSSLCertificateFile %s\n" % cert
self.save_notes += "\tSSLCertificateKeyFile %s\n" % key
if cert_chain:
@ -450,7 +446,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# First register the creation so that it is properly removed if
# configuration is rolled back
self.register_file_creation(False, ssl_fp)
self.reverter.register_file_creation(False, ssl_fp)
try:
orig_file = open(avail_fp, 'r')
@ -704,7 +700,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Register the new file that will be created
# Note: always register the creation before writing to ensure file will
# be removed in case of unexpected program exit
self.register_file_creation(False, redirect_filepath)
self.reverter.register_file_creation(False, redirect_filepath)
# Write out file
with open(redirect_filepath, 'w') as redirect_fd:
@ -872,7 +868,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if "/sites-available/" in vhost.filep:
enabled_path = ("%ssites-enabled/%s" %
(self.parser.root, os.path.basename(vhost.filep)))
self.register_file_creation(False, enabled_path)
self.reverter.register_file_creation(False, enabled_path)
os.symlink(vhost.filep, enabled_path)
vhost.enabled = True
logging.info("Enabling available site: %s", vhost.filep)
@ -1033,9 +1029,7 @@ def enable_mod(mod_name):
stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w'))
# Hopefully this waits for output
subprocess.check_call(["sudo", CONFIG.APACHE2, "restart"],
stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w'))
apache_restart()
except (OSError, subprocess.CalledProcessError) as err:
logging.error("Error enabling mod_%s", mod_name)
logging.error("Exception: %s", err)
@ -1055,15 +1049,22 @@ def mod_loaded(module):
proc = subprocess.Popen(
[CONFIG.APACHE_CTL, '-M'],
stdout=subprocess.PIPE,
stderr=open("/dev/null", 'w')).communicate()[0]
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logging.error(
"Error accessing %s for loaded modules!", CONFIG.APACHE_CTL)
logging.error("This may be caused by an Apache Configuration Error")
return False
raise errors.LetsEncryptConfiguratorError(
"Error accessing loaded modules")
# Small errors that do not impede
if proc.returncode != 0:
logging.warn("Error in checking loaded module list: %s", stderr)
raise errors.LetsEncryptMisconfigurationError(
"Apache is unable to check whether or not the module is "
"loaded because Apache is misconfigured.")
if module in proc:
if module in stdout:
return True
return False
@ -1079,13 +1080,13 @@ def apache_restart():
proc = subprocess.Popen([CONFIG.APACHE2, 'restart'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
text = proc.communicate()
stdout, stderr = proc.communicate()
if proc.returncode != 0:
# Enter recovery routine...
logging.error("Configtest failed")
logging.error(text[0])
logging.error(text[1])
logging.error(stdout)
logging.error(stderr)
return False
except (OSError, ValueError):

View file

@ -88,7 +88,7 @@ class ApacheDvsni(object):
# Create all of the challenge certs
for chall in self.dvsni_chall:
cert_path = self.get_cert_file(chall.nonce)
self.config.register_file_creation(cert_path)
self.config.reverter.register_file_creation(cert_path)
s_b64 = challenge_util.dvsni_gen_cert(
cert_path, chall.domain, chall.r_b64, chall.nonce, chall.key)
@ -120,7 +120,7 @@ class ApacheDvsni(object):
config_text += "</IfModule>\n"
self._conf_include_check(self.config.parser.loc["default"])
self.config.register_file_creation(True, self.challenge_conf)
self.config.reverter.register_file_creation(True, self.challenge_conf)
with open(self.challenge_conf, 'w') as new_conf:
new_conf.write(config_text)

View file

@ -8,7 +8,7 @@ import time
import augeas
from letsencrypt.client import CONFIG
from letsencrypt.client import le_util
from letsencrypt.client import reverter
class AugeasConfigurator(object):
@ -19,6 +19,8 @@ class AugeasConfigurator(object):
:ivar str save_notes: Human-readable configuration change notes
:ivar dict direc: dictionary containing save directory paths
:ivar reverter: saves and reverts checkpoints
:type reverter: :class:`letsencrypt.client.reverter.Reverter`
"""
@ -35,13 +37,19 @@ class AugeasConfigurator(object):
"temp": CONFIG.TEMP_CHECKPOINT_DIR,
"progress": CONFIG.IN_PROGRESS_DIR}
self.direc = direc
# TODO: this instantiation can be optimized to only load
# relevant files - I believe -> NO_MODL_AUTOLOAD
# Set Augeas flags to save backup
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
self.save_notes = ""
# See if any temporary changes need to be recovered
# This needs to occur before VirtualHost objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.reverter = reverter.Reverter(direc)
self.reverter.recovery_routine()
def check_parsing_errors(self, lens):
"""Verify Augeas can parse all of the lens files.
@ -84,16 +92,7 @@ class AugeasConfigurator(object):
# This is a noop save
self.aug.save()
except (RuntimeError, IOError):
# Check for the root of save problems
new_errs = self.aug.match("/augeas//error")
# logging.error("During Save - %s", mod_conf)
# Only print new errors caused by recent save
for err in new_errs:
if err not in ex_errs:
logging.error(
"Unable to save file - %s", err[13:len(err) - 6])
logging.error("Attempted Save Notes")
logging.error(self.save_notes)
self._log_save_errors()
# Erase Save Notes
self.save_notes = ""
return False
@ -110,27 +109,15 @@ class AugeasConfigurator(object):
for path in save_paths:
save_files.add(self.aug.get(path)[6:])
valid, message = self.check_tempfile_saves(save_files)
if not valid:
logging.fatal(message)
# What is the protocol in this situation?
# This shouldn't happen if the challenge codebase is correct
return False
# Create Checkpoint
if temporary:
self.add_to_checkpoint(self.direc["temp"], save_files)
self.reverter.add_to_temp_checkpoint(
save_files, self.save_notes)
else:
self.add_to_checkpoint(self.direc["progress"], save_files)
self.reverter.add_to_checkpoint(save_files, self.save_notes)
if title and not temporary and os.path.isdir(self.direc["progress"]):
success = self._finalize_checkpoint(self.direc["progress"], title)
if not success:
# This should never happen
# This will be hopefully be cleaned up on the recovery
# routine startup
sys.exit(9)
if title and not temporary:
success = self.reverter.finalize_checkpoint(title)
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
@ -138,308 +125,44 @@ class AugeasConfigurator(object):
return True
def revert_challenge_config(self):
"""Reload users original configuration files after a challenge.
This function should reload the users original configuration files
for all saves with temporary=True
"""
if os.path.isdir(self.direc["temp"]):
result = self._recover_checkpoint(self.direc["temp"])
if result != 0:
# We have a partial or incomplete recovery
logging.fatal("Incomplete or failed recovery for %s",
self.direc["temp"])
sys.exit(67)
# Remember to reload Augeas
self.aug.load()
def rollback_checkpoints(self, rollback=1):
"""Revert 'rollback' number of configuration checkpoints.
:param int rollback: Number of checkpoints to reverse
"""
try:
rollback = int(rollback)
except ValueError:
logging.error("Rollback argument must be a positive integer")
# Sanity check input
if rollback < 1:
logging.error("Rollback argument must be a positive integer")
return
backups = os.listdir(self.direc["backup"])
backups.sort()
if len(backups) < rollback:
logging.error("Unable to rollback %d checkpoints, only %d exist",
rollback, len(backups))
while rollback > 0 and backups:
cp_dir = self.direc["backup"] + backups.pop()
result = self._recover_checkpoint(cp_dir)
if result != 0:
logging.fatal("Failed to load checkpoint during rollback")
sys.exit(39)
rollback -= 1
self.aug.load()
def show_config_changes(self):
"""Displays all saved checkpoints.
All checkpoints are printed to the console.
Note: Any 'IN_PROGRESS' checkpoints will be removed by the cleanup
script found in the constructor, before this function would ever be
called.
"""
backups = os.listdir(self.direc["backup"])
backups.sort(reverse=True)
if not backups:
print ("Letsencrypt has not saved any backups of your "
"apache configuration")
# Make sure there isn't anything unexpected in the backup folder
# There should only be timestamped (float) directories
try:
for bkup in backups:
float(bkup)
except ValueError:
assert False, "Invalid files in %s" % self.direc["backup"]
for bkup in backups:
print time.ctime(float(bkup))
cur_dir = self.direc["backup"] + bkup
with open(os.path.join(cur_dir, "CHANGES_SINCE")) as changes_fd:
print changes_fd.read()
print "Affected files:"
with open(os.path.join(cur_dir, "FILEPATHS")) as paths_fd:
filepaths = paths_fd.read().splitlines()
for path in filepaths:
print " %s" % path
try:
with open(os.path.join(cur_dir, "NEW_FILES")) as new_fd:
print "New Configuration Files:"
filepaths = new_fd.read().splitlines()
for path in filepaths:
print " %s" % path
except (IOError, OSError) as exc:
print exc
print ""
def add_to_checkpoint(self, cp_dir, save_files):
"""Add save files to checkpoint directory.
:param str cp_dir: Checkpoint directory filepath
:param set save_files: set of files to save
"""
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
existing_filepaths = []
filepaths_path = os.path.join(cp_dir, "FILEPATHS")
# Open up FILEPATHS differently depending on if it already exists
if os.path.isfile(filepaths_path):
op_fd = open(filepaths_path, 'r+')
existing_filepaths = op_fd.read().splitlines()
else:
op_fd = open(filepaths_path, 'w')
idx = len(existing_filepaths)
for filename in save_files:
if filename not in existing_filepaths:
# Tag files with index so multiple files can
# have the same filename
logging.debug("Creating backup of %s", filename)
shutil.copy2(filename, os.path.join(
cp_dir, os.path.basename(filename) + "_" + str(idx)))
op_fd.write(filename + '\n')
idx += 1
op_fd.close()
with open(os.path.join(cp_dir, "CHANGES_SINCE"), 'a') as notes_fd:
notes_fd.write(self.save_notes)
def _recover_checkpoint(self, cp_dir):
"""Recover a specific checkpoint.
Recover a specific checkpoint provided by cp_dir
Note: this function does not reload augeas.
:param str cp_dir: checkpoint directory file path
:returns: 0 success, 1 Unable to revert, -1 Unable to delete
:rtype: int
"""
if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")):
try:
with open(os.path.join(cp_dir, "FILEPATHS")) as paths_fd:
filepaths = paths_fd.read().splitlines()
for idx, path in enumerate(filepaths):
shutil.copy2(os.path.join(
cp_dir,
os.path.basename(path) + '_' + str(idx)), path)
except (IOError, OSError):
# This file is required in all checkpoints.
logging.error("Unable to recover files from %s", cp_dir)
return 1
# Remove any newly added files if they exist
self._remove_contained_files(os.path.join(cp_dir, "NEW_FILES"))
try:
shutil.rmtree(cp_dir)
except OSError:
logging.error("Unable to remove directory: %s", cp_dir)
return -1
return 0
def check_tempfile_saves(self, save_files): # pylint: disable=no-self-use
"""Verify save isn't overwriting any temporary files.
:param set save_files: Set of files about to be saved.
:returns: Success, error message
:rtype: bool, str
"""
temp_path = "%sFILEPATHS" % self.direc["temp"]
if os.path.isfile(temp_path):
with open(temp_path, 'r') as protected_fd:
protected_files = protected_fd.read().splitlines()
for filename in protected_files:
if filename in save_files:
return False, ("Attempting to overwrite challenge "
"file - %s" % filename)
return True, ""
# pylint: disable=no-self-use, anomalous-backslash-in-string
def register_file_creation(self, temporary, *files):
"""Register the creation of all files during letsencrypt execution.
Call this method before writing to the file to make sure that the
file will be cleaned up if the program exits unexpectedly.
(Before a save occurs)
:param bool temporary: If the file creation registry is for
a temp or permanent save.
:param \*files: file paths (str) to be registered
"""
if temporary:
cp_dir = self.direc["temp"]
else:
cp_dir = self.direc["progress"]
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
try:
with open(os.path.join(cp_dir, "NEW_FILES"), 'a') as new_fd:
for file_path in files:
new_fd.write("%s\n" % file_path)
except (IOError, OSError):
logging.error("ERROR: Unable to register file creation")
def _log_save_errors(self):
"""Log errors due to bad Augeas save."""
# Check for the root of save problems
new_errs = self.aug.match("/augeas//error")
# logging.error("During Save - %s", mod_conf)
# Only print new errors caused by recent save
for err in new_errs:
if err not in ex_errs:
logging.error(
"Unable to save file - %s", err[13:len(err) - 6])
logging.error("Attempted Save Notes")
logging.error(self.save_notes)
# Wrapper functions for Reverter class
def recovery_routine(self):
"""Revert all previously modified files.
First, any changes found in self.direc["temp"] are removed,
then IN_PROGRESS changes are removed The order is important.
IN_PROGRESS is unable to add files that are already added by a TEMP
change. Thus TEMP must be rolled back first because that will be the
'latest' occurrence of the file.
Reverts all modified files that have not been saved as a checkpoint
"""
self.revert_challenge_config()
if os.path.isdir(self.direc["progress"]):
result = self._recover_checkpoint(self.direc["progress"])
if result != 0:
# We have a partial or incomplete recovery
# Not as egregious
# TODO: Additional tests? recovery
logging.fatal("Incomplete or failed recovery for %s",
self.direc["progress"])
sys.exit(68)
self.reverter.recovery_routine()
# Need to reload configuration after these changes take effect
self.aug.load()
# Need to reload configuration after these changes take effect
self.aug.load()
def revert_challenge_config(self):
"""Used to cleanup challenge configurations."""
self.reverter.revert_temporary_config()
self.aug.load()
# pylint: disable=no-self-use
def _remove_contained_files(self, file_list):
"""Erase all files contained within file_list.
def rollback_checkpoints(self, rollback=1):
"""Rollback saved checkpoints.
:param str file_list: file containing list of file paths to be deleted
:returns: Success
:rtype: bool
:param int rollback: Number of checkpoints to revert
"""
# Check to see that file exists to differentiate can't find file_list
# and can't remove filepaths within file_list errors.
if not os.path.isfile(file_list):
return False
try:
with open(file_list, 'r') as list_fd:
filepaths = list_fd.read().splitlines()
for path in filepaths:
# Files are registered before they are added... so
# check to see if file exists first
if os.path.lexists(path):
os.remove(path)
else:
logging.warn(
"File: %s - Could not be found to be deleted\n"
"LE probably shut down unexpectedly", path)
except (IOError, OSError):
logging.fatal(
"Unable to remove filepaths contained within %s", file_list)
sys.exit(41)
self.reverter.rollback_checkpoints(rollback)
self.aug.load()
return True
# pylint: disable=no-self-use
def _finalize_checkpoint(self, cp_dir, title):
"""Move IN_PROGRESS checkpoint to timestamped checkpoint.
Adds title to cp_dir CHANGES_SINCE
Move cp_dir to Backups directory and rename with timestamp
:param cp_dir: "IN PROGRESS" directory
:type cp_dir: str
:returns: Success
:rtype: bool
"""
final_dir = os.path.join(self.direc["backup"], str(time.time()))
changes_since_path = os.path.join(cp_dir, "CHANGES_SINCE")
changes_since_tmp_path = os.path.join(cp_dir, "CHANGES_SINCE.tmp")
try:
with open(changes_since_tmp_path, 'w') as changes_tmp:
changes_tmp.write("-- %s --\n" % title)
with open(changes_since_path, 'r') as changes_orig:
changes_tmp.write(changes_orig.read())
shutil.move(changes_since_tmp_path, changes_since_path)
except (IOError, OSError):
logging.error("Unable to finalize checkpoint - adding title")
return False
try:
os.rename(cp_dir, final_dir)
except OSError:
logging.error(
"Unable to finalize checkpoint, %s -> %s", cp_dir, final_dir)
return False
return True
def view_config_changes(self):
"""Show all of the configuration changes that have taken place."""
self.reverter.show_config_changes()

View file

@ -5,6 +5,10 @@ class LetsEncryptClientError(Exception):
"""Generic Let's Encrypt client error."""
class LetsEncryptReverterError(LetsEncryptClientError):
"""Let's Encrypt Reverter error."""
class LetsEncryptAuthHandlerError(LetsEncryptClientError):
"""Let's Encrypt Auth Handler error."""
@ -17,5 +21,9 @@ class LetsEncryptConfiguratorError(LetsEncryptClientError):
"""Let's Encrypt Configurator error."""
class LetsEncryptMisconfigurationError(LetsEncryptClientError):
"""Let's Encrypt Misconfiguration Error."""
class LetsEncryptDvsniError(LetsEncryptConfiguratorError):
"""Let's Encrypt DVSNI error."""

View file

@ -0,0 +1,343 @@
"""Reverter class saves configuration checkpoints and allows for recovery."""
import logging
import os
import shutil
import sys
import time
from letsencrypt.client import CONFIG
from letsencrypt.client import errors
from letsencrypt.client import le_util
class Reverter(object):
"""Reverter Class - save and revert configuration checkpoints"""
def __init__(self, direc=None):
if not direc:
direc = {"backup": CONFIG.BACKUP_DIR,
"temp": CONFIG.TEMP_CHECKPOINT_DIR,
"progress": CONFIG.IN_PROGRESS_DIR}
self.direc = direc
def revert_temporary_config(self):
"""Reload users original configuration files after a temporary save.
This function should reinstall the users original configuration files
for all saves with temporary=True
:raises :class:`errors.LetsEncryptReverterError`:
Unable to revert config
"""
if os.path.isdir(self.direc["temp"]):
result = self._recover_checkpoint(self.direc["temp"])
if result != 0:
# We have a partial or incomplete recovery
logging.fatal("Incomplete or failed recovery for %s",
self.direc["temp"])
raise errors.LetsEncryptReverterError(
"Unable to revert temporary config")
def rollback_checkpoints(self, rollback=1):
"""Revert 'rollback' number of configuration checkpoints.
:param int rollback: Number of checkpoints to reverse
"""
try:
rollback = int(rollback)
except ValueError:
logging.error("Rollback argument must be a positive integer")
# Sanity check input
if rollback < 1:
logging.error("Rollback argument must be a positive integer")
return
backups = os.listdir(self.direc["backup"])
backups.sort()
if len(backups) < rollback:
logging.error("Unable to rollback %d checkpoints, only %d exist",
rollback, len(backups))
while rollback > 0 and backups:
cp_dir = self.direc["backup"] + backups.pop()
result = self._recover_checkpoint(cp_dir)
if result != 0:
logging.fatal("Failed to load checkpoint during rollback")
sys.exit(39)
rollback -= 1
def view_config_changes(self):
"""Displays all saved checkpoints.
All checkpoints are printed to the console.
Note: Any 'IN_PROGRESS' checkpoints will be removed by the cleanup
script found in the constructor, before this function would ever be
called.
"""
backups = os.listdir(self.direc["backup"])
backups.sort(reverse=True)
if not backups:
print ("Letsencrypt has not saved any backups of your "
"configuration")
# Make sure there isn't anything unexpected in the backup folder
# There should only be timestamped (float) directories
try:
for bkup in backups:
float(bkup)
except ValueError:
assert False, "Invalid files in %s" % self.direc['backup']
for bkup in backups:
print time.ctime(float(bkup))
cur_dir = self.direc['backup'] + bkup
with open(os.path.join(cur_dir, 'CHANGES_SINCE')) as changes_fd:
print changes_fd.read()
print "Affected files:"
with open(os.path.join(cur_dir, 'FILEPATHS')) as paths_fd:
filepaths = paths_fd.read().splitlines()
for path in filepaths:
print " %s" % path
try:
if os.path.isfile(os.path.join(cur_dir, 'NEW_FILES')):
with open(os.path.join(cur_dir, 'NEW_FILES')) as new_fd:
print "New Configuration Files:"
filepaths = new_fd.read().splitlines()
for path in filepaths:
print " %s" % path
except (IOError, OSError) as err:
logging.warn(str(err))
print ""
def add_to_temp_checkpoint(self, save_files, save_notes):
"""Add files to temporary checkpoint
param set save_files: set of filepaths to save
param str save_notes: notes about changes during the save
"""
self._add_to_checkpoint_dir(self.direc['temp'], save_files, save_notes)
def add_to_checkpoint(self, save_files, save_notes):
"""Add files to a permanent checkpoint
:param set save_files: set of filepaths to save
:param str save_notes: notes about changes during the save
"""
self._add_to_checkpoint_dir(
self.direc['progress'], save_files, save_notes)
def _add_to_checkpoint_dir(self, cp_dir, save_files, save_notes):
"""Add save files to checkpoint directory.
:param str cp_dir: Checkpoint directory filepath
:param set save_files: set of files to save
:param str save_notes: notes about changes made during the save
"""
self._check_tempfile_saves(save_files)
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
existing_filepaths = []
filepaths_path = os.path.join(cp_dir, "FILEPATHS")
# Open up FILEPATHS differently depending on if it already exists
if os.path.isfile(filepaths_path):
op_fd = open(filepaths_path, 'r+')
existing_filepaths = op_fd.read().splitlines()
else:
op_fd = open(filepaths_path, 'w')
idx = len(existing_filepaths)
for filename in save_files:
if filename not in existing_filepaths:
# Tag files with index so multiple files can
# have the same filename
logging.debug("Creating backup of %s", filename)
shutil.copy2(filename, os.path.join(
cp_dir, os.path.basename(filename) + "_" + str(idx)))
op_fd.write(filename + '\n')
idx += 1
op_fd.close()
with open(os.path.join(cp_dir, "CHANGES_SINCE"), 'a') as notes_fd:
notes_fd.write(save_notes)
def _recover_checkpoint(self, cp_dir):
"""Recover a specific checkpoint.
Recover a specific checkpoint provided by cp_dir
Note: this function does not reload augeas.
:param str cp_dir: checkpoint directory file path
:returns: 0 success, 1 Unable to revert, -1 Unable to delete
:rtype: int
"""
if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")):
try:
with open(os.path.join(cp_dir, "FILEPATHS")) as paths_fd:
filepaths = paths_fd.read().splitlines()
for idx, path in enumerate(filepaths):
shutil.copy2(os.path.join(
cp_dir,
os.path.basename(path) + '_' + str(idx)), path)
except (IOError, OSError):
# This file is required in all checkpoints.
logging.error("Unable to recover files from %s", cp_dir)
return 1
# Remove any newly added files if they exist
self._remove_contained_files(os.path.join(cp_dir, "NEW_FILES"))
try:
shutil.rmtree(cp_dir)
except OSError:
logging.error("Unable to remove directory: %s", cp_dir)
return -1
return 0
def _check_tempfile_saves(self, save_files): # pylint: disable=no-self-use
"""Verify save isn't overwriting any temporary files.
:param set save_files: Set of files about to be saved.
:raises :class:`letsencrypt.client.errors.LetsEncryptReverterError`:
when save is attempting to overwrite a temporary file.
"""
temp_path = "%sFILEPATHS" % self.direc["temp"]
if os.path.isfile(temp_path):
with open(temp_path, 'r') as protected_fd:
protected_files = protected_fd.read().splitlines()
for filename in protected_files:
if filename in save_files:
raise errors.LetsEncryptReverterError(
"Attempting to overwrite challenge "
"file - %s" % filename)
# pylint: disable=no-self-use, anomalous-backslash-in-string
def register_file_creation(self, temporary, *files):
"""Register the creation of all files during letsencrypt execution.
Call this method before writing to the file to make sure that the
file will be cleaned up if the program exits unexpectedly.
(Before a save occurs)
:param bool temporary: If the file creation registry is for
a temp or permanent save.
:param \*files: file paths (str) to be registered
"""
if temporary:
cp_dir = self.direc["temp"]
else:
cp_dir = self.direc["progress"]
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
try:
with open(os.path.join(cp_dir, "NEW_FILES"), 'a') as new_fd:
for file_path in files:
new_fd.write("%s\n" % file_path)
except (IOError, OSError):
logging.error("ERROR: Unable to register file creation")
def recovery_routine(self):
"""Revert all previously modified files.
First, any changes found in self.direc["temp"] are removed,
then IN_PROGRESS changes are removed The order is important.
IN_PROGRESS is unable to add files that are already added by a TEMP
change. Thus TEMP must be rolled back first because that will be the
'latest' occurrence of the file.
"""
self.revert_temporary_config()
if os.path.isdir(self.direc["progress"]):
result = self._recover_checkpoint(self.direc["progress"])
if result != 0:
# We have a partial or incomplete recovery
# Not as egregious
# TODO: Additional tests? recovery
logging.fatal("Incomplete or failed recovery for %s",
self.direc["progress"])
sys.exit(68)
# pylint: disable=no-self-use
def _remove_contained_files(self, file_list):
"""Erase all files contained within file_list.
:param str file_list: file containing list of file paths to be deleted
:returns: Success
:rtype: bool
"""
# Check to see that file exists to differentiate can't find file_list
# and can't remove filepaths within file_list errors.
if not os.path.isfile(file_list):
return False
try:
with open(file_list, 'r') as list_fd:
filepaths = list_fd.read().splitlines()
for path in filepaths:
# Files are registered before they are added... so
# check to see if file exists first
if os.path.lexists(path):
os.remove(path)
else:
logging.warn(
"File: %s - Could not be found to be deleted\n"
"LE probably shut down unexpectedly", path)
except (IOError, OSError):
logging.fatal(
"Unable to remove filepaths contained within %s", file_list)
sys.exit(41)
return True
# pylint: disable=no-self-use
def finalize_checkpoint(self, title):
"""Move IN_PROGRESS checkpoint to timestamped checkpoint.
Adds title to self.direc['progress'] CHANGES_SINCE
Move self.direc['progress'] to Backups directory and rename with timestamp
"""
# Check to make sure an "in progress" directory exists
if not os.path.isdir(self.direc['progress']):
return
final_dir = os.path.join(self.direc['backup'], str(time.time()))
changes_since_path = os.path.join(
self.direc['progress'], 'CHANGES_SINCE')
changes_since_tmp_path = os.path.join(
self.direc['progress'], 'CHANGES_SINCE.tmp')
try:
with open(changes_since_tmp_path, 'w') as changes_tmp:
changes_tmp.write("-- %s --\n" % title)
with open(changes_since_path, 'r') as changes_orig:
changes_tmp.write(changes_orig.read())
shutil.move(changes_since_tmp_path, changes_since_path)
except (IOError, OSError):
logging.error("Unable to finalize checkpoint - adding title")
raise errors.LetsEncryptReverterError("Unable to add title")
try:
os.rename(self.direc['progress'], final_dir)
except OSError:
logging.error(
"Unable to finalize checkpoint, %s -> %s", cp_dir, final_dir)
raise errors.LetsEncryptReverterError(
"Unable to finalize checkpoint renaming")

View file

@ -13,6 +13,7 @@ from letsencrypt.client import display
from letsencrypt.client import interfaces
from letsencrypt.client import errors
from letsencrypt.client import log
from letsencrypt.client import reverter
from letsencrypt.client import revoker
from letsencrypt.client.apache import configurator
@ -71,25 +72,32 @@ def main():
displayer = display.FileDisplay(sys.stdout)
zope.component.provideUtility(displayer)
installer = determine_installer()
server = CONFIG.ACME_SERVER if args.server is None else args.server
if args.view_config_changes:
view_config_changes()
sys.exit()
if args.revoke:
revoc = revoker.Revoker(server, installer)
revoc.list_certs_keys()
revoke(server)
sys.exit()
if args.rollback > 0:
rollback(installer, args.rollback)
sys.exit()
if args.view_config_changes:
view_config_changes(installer)
rollback(args.rollback)
sys.exit()
if not args.eula:
display_eula()
# Make sure we actually get an installer that is functioning properly
# before we begin to try to use it.
try:
installer = determine_installer()
except errors.LetsEncryptMisconfigurationError as err:
logging.fatal("Please fix your configuration before proceeding. "
"The Installer exited with the following message: "
"%s", str(err))
# Use the same object if possible
if interfaces.IAuthenticator.providedBy(installer):
auth = installer
@ -198,27 +206,52 @@ def read_file(filename):
raise argparse.ArgumentTypeError(exc.strerror)
def rollback(installer, checkpoints):
def rollback(checkpoints):
"""Revert configuration the specified number of checkpoints.
:param installer: Installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
:param int checkpoints: Number of checkpoints to revert.
"""
installer.rollback_checkpoints(checkpoints)
installer.restart()
# Misconfigurations are only a slight problems... allow the user to rollback
try:
installer = determine_installer()
installer.rollback_checkpoints(checkpoints)
installer.restart()
except errors.LetsEncryptMisconfigurationError:
logging.warn("Installer is misconfigured before rollback.")
logging.info("Rolling back using Reverter module")
# recovery routine has already been run by installer __init__ attempt
reverter.Reverter().rollback_checkpoints(checkpoints)
try:
installer = determine_installer()
installer.restart()
logging.info("Rollback solved misconfiguration!")
except errors.LetsEncryptMisconfigurationError:
logging.warn("Rollback was unable to solve misconfiguration issues")
def view_config_changes(installer):
"""View checkpoints and associated configuration changes.
def revoke(server):
"""Revoke certificates.
:param installer: Installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
:param str server: ACME server client wishes to revoke certificates from
"""
installer.view_config_changes()
# Misconfigurations don't really matter. Determine installer better choose
# correctly though.
try:
installer = determine_installer()
except errors.LetsEncryptMisconfigurationError:
logging.warn("Installer is currently misconfigured.")
revoc = revoker.Revoker(server, installer)
revoc.list_certs_keys()
def view_config_changes():
"""View checkpoints and associated configuration changes."""
rev = reverter.Reverter()
rev.recovery_routine()
rev.view_config_changes()
if __name__ == "__main__":
main()