Merge pull request #126 from kuba/logging

Refactor logging, "pythonic" DialogHandler
This commit is contained in:
James Kasten
2014-12-10 18:30:51 -08:00
11 changed files with 237 additions and 333 deletions
+44 -46
View File
@@ -1,5 +1,6 @@
"""Apache Configuration based off of Augeas Configurator.""" """Apache Configuration based off of Augeas Configurator."""
import hashlib import hashlib
import logging
import os import os
import pkg_resources import pkg_resources
import re import re
@@ -15,7 +16,6 @@ from letsencrypt.client import CONFIG
from letsencrypt.client import crypto_util from letsencrypt.client import crypto_util
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client import logger
# Configurator should be turned into a Singleton # Configurator should be turned into a Singleton
@@ -229,13 +229,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0: if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
# Throw some "can't find all of the directives error" # Throw some "can't find all of the directives error"
logger.warn(("Cannot find a cert or key directive in %s" logging.warn(
% vhost.path)) "Cannot find a cert or key directive in %s", vhost.path)
logger.warn("VirtualHost was not modified") logging.warn("VirtualHost was not modified")
# Presumably break here so that the virtualhost is not modified # Presumably break here so that the virtualhost is not modified
return False return False
logger.info("Deploying Certificate to VirtualHost %s" % vhost.filep) logging.info("Deploying Certificate to VirtualHost %s", vhost.filep)
self.aug.set(path["cert_file"][0], cert) self.aug.set(path["cert_file"][0], cert)
self.aug.set(path["cert_key"][0], key) self.aug.set(path["cert_key"][0], key)
@@ -524,15 +524,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
if not check_ssl_loaded(): if not check_ssl_loaded():
logger.info("Loading mod_ssl into Apache Server") logging.info("Loading mod_ssl into Apache Server")
enable_mod("ssl") enable_mod("ssl")
# Check for Listen 443 # Check for Listen 443
# Note: This could be made to also look for ip:443 combo # Note: This could be made to also look for ip:443 combo
# TODO: Need to search only open directives and IfMod mod_ssl.c # TODO: Need to search only open directives and IfMod mod_ssl.c
if len(self.find_directive(case_i("Listen"), "443")) == 0: if len(self.find_directive(case_i("Listen"), "443")) == 0:
logger.debug("No Listen 443 directive found") logging.debug("No Listen 443 directive found")
logger.debug("Setting the Apache Server to Listen on port 443") logging.debug("Setting the Apache Server to Listen on port 443")
path = self._add_dir_to_ifmodssl( path = self._add_dir_to_ifmodssl(
get_aug_path(self.location["listen"]), "Listen", "443") get_aug_path(self.location["listen"]), "Listen", "443")
self.save_notes += "Added Listen 443 directive to %s\n" % path self.save_notes += "Added Listen 443 directive to %s\n" % path
@@ -554,15 +554,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
tup = addr.partition(":") tup = addr.partition(":")
if tup[0] == "_default_": if tup[0] == "_default_":
if not self.is_name_vhost(default_addr): if not self.is_name_vhost(default_addr):
logger.debug(("Setting all VirtualHosts on " logging.debug("Setting all VirtualHosts on %s to be "
"%s to be name based vhosts" % default_addr)) "name based vhosts", default_addr)
self.add_name_vhost(default_addr) self.add_name_vhost(default_addr)
# No default addresses... so set each one individually # No default addresses... so set each one individually
for addr in vhost.addrs: for addr in vhost.addrs:
if not self.is_name_vhost(addr): if not self.is_name_vhost(addr):
logger.debug(("Setting VirtualHost at %s " logging.debug("Setting VirtualHost at %s to be a name "
"to be a name based virtual host" % addr)) "based virtual host", addr)
self.add_name_vhost(addr) self.add_name_vhost(addr)
def _get_ifmod(self, aug_conf_path, mod): def _get_ifmod(self, aug_conf_path, mod):
@@ -703,7 +703,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg) # matchObj = validChars.match(arg)
# if matchObj.group() != arg: # if matchObj.group() != arg:
# logger.error("Error: Invalid regexp characters in %s" % arg) # logging.error("Error: Invalid regexp characters in %s", arg)
# return [] # return []
# Standardize the include argument based on server root # Standardize the include argument based on server root
@@ -769,7 +769,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
new_file.write(line) new_file.write(line)
new_file.write("</IfModule>\n") new_file.write("</IfModule>\n")
except IOError: except IOError:
logger.fatal("Error writing/reading to file in make_vhost_ssl") logging.fatal("Error writing/reading to file in make_vhost_ssl")
sys.exit(49) sys.exit(49)
finally: finally:
orig_file.close() orig_file.close()
@@ -803,7 +803,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
vh_p = self.aug.match(("/files%s//* [label()=~regexp('%s')]" % vh_p = self.aug.match(("/files%s//* [label()=~regexp('%s')]" %
(ssl_fp, case_i('VirtualHost')))) (ssl_fp, case_i('VirtualHost'))))
if len(vh_p) != 1: if len(vh_p) != 1:
logger.error("Error: should only be one vhost in %s" % avail_fp) logging.error("Error: should only be one vhost in %s", avail_fp)
sys.exit(1) sys.exit(1)
self.add_dir(vh_p[0], "SSLCertificateFile", self.add_dir(vh_p[0], "SSLCertificateFile",
@@ -813,7 +813,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.add_dir(vh_p[0], "Include", self.location["ssl_options"]) self.add_dir(vh_p[0], "Include", self.location["ssl_options"])
# Log actions and create save notes # Log actions and create save notes
logger.info("Created an SSL vhost at %s" % ssl_fp) logging.info("Created an SSL vhost at %s", ssl_fp)
self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp
self.save() self.save()
@@ -832,7 +832,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if (self.is_name_vhost(nonssl_vhost.addrs[i]) and if (self.is_name_vhost(nonssl_vhost.addrs[i]) and
not self.is_name_vhost(ssl_addrs[i])): not self.is_name_vhost(ssl_addrs[i])):
self.add_name_vhost(ssl_addrs[i]) self.add_name_vhost(ssl_addrs[i])
logger.info("Enabling NameVirtualHosts on " + ssl_addrs[i]) logging.info("Enabling NameVirtualHosts on %s", ssl_addrs[i])
need_to_save = True need_to_save = True
if need_to_save: if need_to_save:
@@ -862,7 +862,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
general_v = self._general_vhost(ssl_vhost) general_v = self._general_vhost(ssl_vhost)
if general_v is None: if general_v is None:
# Add virtual_server with redirect # Add virtual_server with redirect
logger.debug( logging.debug(
"Did not find http version of ssl virtual host... creating") "Did not find http version of ssl virtual host... creating")
return self.create_redirect_vhost(ssl_vhost) return self.create_redirect_vhost(ssl_vhost)
else: else:
@@ -870,10 +870,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
exists, code = self.existing_redirect(general_v) exists, code = self.existing_redirect(general_v)
if exists: if exists:
if code == 0: if code == 0:
logger.debug("Redirect already added") logging.debug("Redirect already added")
return True, general_v return True, general_v
else: else:
logger.debug("Unknown redirect exists for this vhost") logging.debug("Unknown redirect exists for this vhost")
return False, general_v return False, general_v
# Add directives to server # Add directives to server
self.add_dir(general_v.path, "RewriteEngine", "On") self.add_dir(general_v.path, "RewriteEngine", "On")
@@ -986,7 +986,7 @@ LogLevel warn \n\
# Write out file # Write out file
with open(redirect_filepath, 'w') as redirect_fd: with open(redirect_filepath, 'w') as redirect_fd:
redirect_fd.write(redirect_file) redirect_fd.write(redirect_file)
logger.info("Created redirect file: " + redirect_filename) logging.info("Created redirect file: %s", redirect_filename)
self.aug.load() self.aug.load()
# Make a new vhost data structure and add it to the lists # Make a new vhost data structure and add it to the lists
@@ -1115,8 +1115,8 @@ LogLevel warn \n\
# Can be removed once find directive can return ordered results # Can be removed once find directive can return ordered results
if len(cert_path) != 1 or len(key_path) != 1: if len(cert_path) != 1 or len(key_path) != 1:
logger.error(("Too many cert or key directives in vhost " logging.error("Too many cert or key directives in vhost %s",
"%s" % vhost.filep)) vhost.filep)
sys.exit(40) sys.exit(40)
cert = os.path.abspath(self.aug.get(cert_path[0])) cert = os.path.abspath(self.aug.get(cert_path[0]))
@@ -1166,7 +1166,7 @@ LogLevel warn \n\
self.register_file_creation(False, enabled_path) self.register_file_creation(False, enabled_path)
os.symlink(vhost.filep, enabled_path) os.symlink(vhost.filep, enabled_path)
vhost.enabled = True vhost.enabled = True
logger.info("Enabling available site: %s" % vhost.filep) logging.info("Enabling available site: %s", vhost.filep)
self.save_notes += 'Enabled site %s\n' % vhost.filep self.save_notes += 'Enabled site %s\n' % vhost.filep
return True return True
return False return False
@@ -1283,14 +1283,14 @@ LogLevel warn \n\
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
text = proc.communicate() text = proc.communicate()
except (OSError, ValueError): except (OSError, ValueError):
logger.fatal("Unable to run /usr/sbin/apache2ctl configtest") logging.fatal("Unable to run /usr/sbin/apache2ctl configtest")
sys.exit(1) sys.exit(1)
if proc.returncode != 0: if proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logger.error("Configtest failed") logging.error("Configtest failed")
logger.error(text[0]) logging.error(text[0])
logger.error(text[1]) logging.error(text[1])
return False return False
return True return True
@@ -1378,8 +1378,8 @@ LogLevel warn \n\
# Do weak validation that challenge is of expected type # Do weak validation that challenge is of expected type
if not ("list_sni_tuple" in chall_dict and "dvsni_key" in chall_dict): if not ("list_sni_tuple" in chall_dict and "dvsni_key" in chall_dict):
logger.fatal("Incorrect parameter given to Apache DVSNI challenge") logging.fatal("Incorrect parameter given to Apache DVSNI challenge")
logger.fatal("Chall dict: " + str(chall_dict)) logging.fatal("Chall dict: %s", chall_dict)
sys.exit(1) sys.exit(1)
addresses = [] addresses = []
@@ -1387,10 +1387,10 @@ LogLevel warn \n\
for tup in chall_dict["list_sni_tuple"]: for tup in chall_dict["list_sni_tuple"]:
vhost = self.choose_virtual_host(tup[0]) vhost = self.choose_virtual_host(tup[0])
if vhost is None: if vhost is None:
logger.error(("No vhost exists with servername " logging.error(
"or alias of: %s" % tup[0])) "No vhost exists with servername or alias of: %s", tup[0])
logger.error("No _default_:443 vhost exists") logging.error("No _default_:443 vhost exists")
logger.error("Please specify servernames in the Apache config") logging.error("Please specify servernames in the Apache config")
return None return None
# TODO - @jdkasten review this code to make sure it makes sense # TODO - @jdkasten review this code to make sure it makes sense
@@ -1559,8 +1559,8 @@ def enable_mod(mod_name):
stdout=open("/dev/null", 'w'), stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w')) stderr=open("/dev/null", 'w'))
except (OSError, subprocess.CalledProcessError) as err: except (OSError, subprocess.CalledProcessError) as err:
logger.error("Error enabling mod_" + mod_name) logging.error("Error enabling mod_%s", mod_name)
logger.error("Exception: %s" % str(err)) logging.error("Exception: %s", err)
sys.exit(1) sys.exit(1)
@@ -1583,9 +1583,9 @@ def check_ssl_loaded():
stderr=open( stderr=open(
"/dev/null", 'w')).communicate()[0] "/dev/null", 'w')).communicate()[0]
except (OSError, ValueError): except (OSError, ValueError):
logger.error( logging.error(
"Error accessing %s for loaded modules!" % CONFIG.APACHE_CTL) "Error accessing %s for loaded modules!", CONFIG.APACHE_CTL)
logger.error("This may be caused by an Apache Configuration Error") logging.error("This may be caused by an Apache Configuration Error")
return False return False
if "ssl_module" in proc: if "ssl_module" in proc:
@@ -1608,14 +1608,14 @@ def apache_restart():
if proc.returncode != 0: if proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logger.error("Configtest failed") logging.error("Configtest failed")
logger.error(text[0]) logging.error(text[0])
logger.error(text[1]) logging.error(text[1])
return False return False
except (OSError, ValueError): except (OSError, ValueError):
logger.fatal(("Apache Restart Failed - " logging.fatal(
"Please Check the Configuration")) "Apache Restart Failed - Please Check the Configuration")
sys.exit(1) sys.exit(1)
return True return True
@@ -1714,8 +1714,6 @@ def main():
"""Main function used for quick testing purposes""" """Main function used for quick testing purposes"""
config = ApacheConfigurator() config = ApacheConfigurator()
logger.setLogger(logger.FileLogger(sys.stdout))
logger.setLogLevel(logger.DEBUG)
# for v in config.vhosts: # for v in config.vhosts:
# print v.filep # print v.filep
+30 -31
View File
@@ -1,4 +1,5 @@
"""Class of Augeas Configurators.""" """Class of Augeas Configurators."""
import logging
import os import os
import sys import sys
import shutil import shutil
@@ -9,7 +10,6 @@ import augeas
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import configurator from letsencrypt.client import configurator
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client import logger
class AugeasConfigurator(configurator.Configurator): class AugeasConfigurator(configurator.Configurator):
@@ -61,9 +61,9 @@ class AugeasConfigurator(configurator.Configurator):
# As aug.get may return null # As aug.get may return null
if lens_path and lens in lens_path: if lens_path and lens in lens_path:
# Strip off /augeas/files and /error # Strip off /augeas/files and /error
logger.error('There has been an error in parsing the file: ' logging.error('There has been an error in parsing the file: %s',
'%s' % path[13:len(path) - 6]) path[13:len(path) - 6])
logger.error(self.aug.get(path + '/message')) logging.error(self.aug.get(path + '/message'))
def save(self, title=None, temporary=False): def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files. """Saves all changes to the configuration files.
@@ -90,14 +90,14 @@ class AugeasConfigurator(configurator.Configurator):
except (RuntimeError, IOError): except (RuntimeError, IOError):
# Check for the root of save problems # Check for the root of save problems
new_errs = self.aug.match("/augeas//error") new_errs = self.aug.match("/augeas//error")
# logger.error("During Save - " + mod_conf) # logging.error("During Save - %s", mod_conf)
# Only print new errors caused by recent save # Only print new errors caused by recent save
for err in new_errs: for err in new_errs:
if err not in ex_errs: if err not in ex_errs:
logger.error("Unable to save file - " logging.error(
"%s" % err[13:len(err)-6]) "Unable to save file - %s", err[13:len(err) - 6])
logger.error("Attempted Save Notes") logging.error("Attempted Save Notes")
logger.error(self.save_notes) logging.error(self.save_notes)
# Erase Save Notes # Erase Save Notes
self.save_notes = "" self.save_notes = ""
return False return False
@@ -117,7 +117,7 @@ class AugeasConfigurator(configurator.Configurator):
valid, message = self.check_tempfile_saves(save_files) valid, message = self.check_tempfile_saves(save_files)
if not valid: if not valid:
logger.fatal(message) logging.fatal(message)
# What is the protocol in this situation? # What is the protocol in this situation?
# This shouldn't happen if the challenge codebase is correct # This shouldn't happen if the challenge codebase is correct
return False return False
@@ -153,8 +153,8 @@ class AugeasConfigurator(configurator.Configurator):
result = self._recover_checkpoint(self.direc["temp"]) result = self._recover_checkpoint(self.direc["temp"])
if result != 0: if result != 0:
# We have a partial or incomplete recovery # We have a partial or incomplete recovery
logger.fatal("Incomplete or failed recovery for " logging.fatal("Incomplete or failed recovery for %s",
"%s" % self.direc["temp"]) self.direc["temp"])
sys.exit(67) sys.exit(67)
# Remember to reload Augeas # Remember to reload Augeas
self.aug.load() self.aug.load()
@@ -168,24 +168,24 @@ class AugeasConfigurator(configurator.Configurator):
try: try:
rollback = int(rollback) rollback = int(rollback)
except ValueError: except ValueError:
logger.error("Rollback argument must be a positive integer") logging.error("Rollback argument must be a positive integer")
# Sanity check input # Sanity check input
if rollback < 1: if rollback < 1:
logger.error("Rollback argument must be a positive integer") logging.error("Rollback argument must be a positive integer")
return return
backups = os.listdir(self.direc["backup"]) backups = os.listdir(self.direc["backup"])
backups.sort() backups.sort()
if len(backups) < rollback: if len(backups) < rollback:
logger.error(("Unable to rollback %d checkpoints, only " logging.error("Unable to rollback %d checkpoints, only %d exist",
"%d exist") % (rollback, len(backups))) rollback, len(backups))
while rollback > 0 and backups: while rollback > 0 and backups:
cp_dir = self.direc["backup"] + backups.pop() cp_dir = self.direc["backup"] + backups.pop()
result = self._recover_checkpoint(cp_dir) result = self._recover_checkpoint(cp_dir)
if result != 0: if result != 0:
logger.fatal("Failed to load checkpoint during rollback") logging.fatal("Failed to load checkpoint during rollback")
sys.exit(39) sys.exit(39)
rollback -= 1 rollback -= 1
@@ -262,7 +262,7 @@ class AugeasConfigurator(configurator.Configurator):
if filename not in existing_filepaths: if filename not in existing_filepaths:
# Tag files with index so multiple files can # Tag files with index so multiple files can
# have the same filename # have the same filename
logger.debug("Creating backup of %s" % filename) logging.debug("Creating backup of %s", filename)
shutil.copy2(filename, os.path.join( shutil.copy2(filename, os.path.join(
cp_dir, os.path.basename(filename) + "_" + str(idx))) cp_dir, os.path.basename(filename) + "_" + str(idx)))
op_fd.write(filename + '\n') op_fd.write(filename + '\n')
@@ -295,7 +295,7 @@ class AugeasConfigurator(configurator.Configurator):
path) path)
except (IOError, OSError): except (IOError, OSError):
# This file is required in all checkpoints. # This file is required in all checkpoints.
logger.error("Unable to recover files from %s" % cp_dir) logging.error("Unable to recover files from %s", cp_dir)
return 1 return 1
# Remove any newly added files if they exist # Remove any newly added files if they exist
@@ -304,7 +304,7 @@ class AugeasConfigurator(configurator.Configurator):
try: try:
shutil.rmtree(cp_dir) shutil.rmtree(cp_dir)
except OSError: except OSError:
logger.error("Unable to remove directory: %s" % cp_dir) logging.error("Unable to remove directory: %s", cp_dir)
return -1 return -1
return 0 return 0
@@ -354,7 +354,7 @@ class AugeasConfigurator(configurator.Configurator):
for file_path in files: for file_path in files:
new_fd.write("%s\n" % file_path) new_fd.write("%s\n" % file_path)
except (IOError, OSError): except (IOError, OSError):
logger.error("ERROR: Unable to register file creation") logging.error("ERROR: Unable to register file creation")
def recovery_routine(self): def recovery_routine(self):
"""Revert all previously modified files. """Revert all previously modified files.
@@ -373,8 +373,8 @@ class AugeasConfigurator(configurator.Configurator):
# We have a partial or incomplete recovery # We have a partial or incomplete recovery
# Not as egregious # Not as egregious
# TODO: Additional tests? recovery # TODO: Additional tests? recovery
logger.fatal("Incomplete or failed recovery for %s" % logging.fatal("Incomplete or failed recovery for %s",
self.direc["progress"]) self.direc["progress"])
sys.exit(68) sys.exit(68)
# Need to reload configuration after these changes take effect # Need to reload configuration after these changes take effect
@@ -403,13 +403,12 @@ class AugeasConfigurator(configurator.Configurator):
if os.path.lexists(path): if os.path.lexists(path):
os.remove(path) os.remove(path)
else: else:
logger.warn(( logging.warn(
"File: %s - Could not be found to be deleted\n" "File: %s - Could not be found to be deleted\n"
"Program was probably shut down unexpectedly, " "Program was probably shut down unexpectedly, ")
"in which case this is not a problem") % path)
except (IOError, OSError): except (IOError, OSError):
logger.fatal( logging.fatal(
"Unable to remove filepaths contained within %s" % file_list) "Unable to remove filepaths contained within %s", file_list)
sys.exit(41) sys.exit(41)
return True return True
@@ -441,12 +440,12 @@ class AugeasConfigurator(configurator.Configurator):
shutil.move(changes_since_tmp_path, changes_since_path) shutil.move(changes_since_tmp_path, changes_since_path)
except (IOError, OSError): except (IOError, OSError):
logger.error("Unable to finalize checkpoint - adding title") logging.error("Unable to finalize checkpoint - adding title")
return False return False
try: try:
os.rename(cp_dir, final_dir) os.rename(cp_dir, final_dir)
except OSError: except OSError:
logger.error("Unable to finalize checkpoint, %s -> %s" % logging.error(
(cp_dir, final_dir)) "Unable to finalize checkpoint, %s -> %s", cp_dir, final_dir)
return False return False
return True return True
+3 -3
View File
@@ -1,8 +1,8 @@
"""ACME challenge.""" """ACME challenge."""
import logging
import sys import sys
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import logger
class Challenge(object): class Challenge(object):
@@ -93,8 +93,8 @@ def _find_smart_path(challenges, combos):
combo_total = 0 combo_total = 0
if not best_combo: if not best_combo:
logger.fatal("Client does not support any combination of " logging.fatal("Client does not support any combination of "
"challenges to satisfy ACME server") "challenges to satisfy ACME server")
sys.exit(22) sys.exit(22)
return best_combo return best_combo
+39 -51
View File
@@ -2,6 +2,7 @@
import collections import collections
import csv import csv
import json import json
import logging
import os import os
import shutil import shutil
import socket import socket
@@ -21,7 +22,6 @@ from letsencrypt.client import crypto_util
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client import logger
# it's weird to point to chocolate servers via raw IPv6 addresses, and # it's weird to point to chocolate servers via raw IPv6 addresses, and
@@ -65,9 +65,6 @@ class Client(object):
self.privkey = privkey self.privkey = privkey
self._validate_csr_key_cli() # TODO: catch exceptions self._validate_csr_key_cli() # TODO: catch exceptions
# Logger needs to be initialized before Configurator
self.init_logger()
# TODO: Can probably figure out which configurator to use # TODO: Can probably figure out which configurator to use
# without special packaging based on system info Command # without special packaging based on system info Command
# line arg or client function to discover # line arg or client function to discover
@@ -178,8 +175,8 @@ class Client(object):
try: try:
return self.is_expected_msg(auth_dict, "authorization") return self.is_expected_msg(auth_dict, "authorization")
except: except:
logger.fatal("Failed Authorization procedure - " logging.fatal(
"cleaning up challenges") "Failed Authorization procedure - cleaning up challenges")
sys.exit(1) sys.exit(1)
finally: finally:
self.cleanup_challenges(chal_objs) self.cleanup_challenges(chal_objs)
@@ -193,7 +190,7 @@ class Client(object):
:rtype: dict :rtype: dict
""" """
logger.info("Preparing and sending CSR..") logging.info("Preparing and sending CSR...")
return self.send_and_receive_expected( return self.send_and_receive_expected(
acme.certificate_request(csr_der, self.privkey.pem), "certificate") acme.certificate_request(csr_der, self.privkey.pem), "certificate")
@@ -303,24 +300,23 @@ class Client(object):
return response return response
elif response["type"] == "error": elif response["type"] == "error":
logger.error("%s: %s - More Info: %s" % logging.error(
(response["error"], "%s: %s - More Info: %s", response["error"],
response.get("message", ""), response.get("message", ""), response.get("moreInfo", ""))
response.get("moreInfo", "")))
raise errors.LetsEncryptClientError(response["error"]) raise errors.LetsEncryptClientError(response["error"])
elif response["type"] == "defer": elif response["type"] == "defer":
logger.info("Waiting for %d seconds..." % delay) logging.info("Waiting for %d seconds...", delay)
time.sleep(delay) time.sleep(delay)
response = self.send(acme.status_request(response["token"])) response = self.send(acme.status_request(response["token"]))
else: else:
logger.fatal("Received unexpected message") logging.fatal("Received unexpected message")
logger.fatal("Expected: %s" % expected) logging.fatal("Expected: %s" % expected)
logger.fatal("Received: " + response) logging.fatal("Received: " + response)
sys.exit(33) sys.exit(33)
logger.error("Server has deferred past the max of %d seconds" % logging.error(
(rounds * delay)) "Server has deferred past the max of %d seconds", rounds * delay)
def list_certs_keys(self): def list_certs_keys(self):
"""List trusted Let's Encrypt certificates.""" """List trusted Let's Encrypt certificates."""
@@ -328,7 +324,7 @@ class Client(object):
certs = [] certs = []
if not os.path.isfile(list_file): if not os.path.isfile(list_file):
logger.info( logging.info(
"You don't have any certificates saved from letsencrypt") "You don't have any certificates saved from letsencrypt")
return return
@@ -399,8 +395,8 @@ class Client(object):
cert_fd.write( cert_fd.write(
crypto_util.b64_cert_to_pem(certificate_dict["certificate"])) crypto_util.b64_cert_to_pem(certificate_dict["certificate"]))
cert_fd.close() cert_fd.close()
logger.info("Server issued certificate; certificate written to %s" % logging.info(
cert_file) "Server issued certificate; certificate written to %s", cert_file)
if certificate_dict.get("chain", None): if certificate_dict.get("chain", None):
chain_fd, chain_fn = le_util.unique_file(CONFIG.CHAIN_PATH, 0o644) chain_fd, chain_fn = le_util.unique_file(CONFIG.CHAIN_PATH, 0o644)
@@ -408,7 +404,7 @@ class Client(object):
chain_fd.write(crypto_util.b64_cert_to_pem(cert)) chain_fd.write(crypto_util.b64_cert_to_pem(cert))
chain_fd.close() chain_fd.close()
logger.info("Cert chain written to %s" % chain_fn) logging.info("Cert chain written to %s", chain_fn)
# This expects a valid chain file # This expects a valid chain file
cert_chain_abspath = os.path.abspath(chain_fn) cert_chain_abspath = os.path.abspath(chain_fn)
@@ -420,7 +416,7 @@ class Client(object):
cert_chain_abspath) cert_chain_abspath)
# Enable any vhost that was issued to, but not enabled # Enable any vhost that was issued to, but not enabled
if not host.enabled: if not host.enabled:
logger.info("Enabling Site " + host.filep) logging.info("Enabling Site %s", host.filep)
self.config.enable_site(host) self.config.enable_site(host)
# sites may have been enabled / final cleanup # sites may have been enabled / final cleanup
@@ -464,7 +460,7 @@ class Client(object):
:param dict challenges: challenges from a challenge message :param dict challenges: challenges from a challenge message
""" """
logger.info("Cleaning up challenges...") logging.info("Cleaning up challenges...")
for chall in challenges: for chall in challenges:
if chall["type"] in CONFIG.CONFIG_CHALLENGES: if chall["type"] in CONFIG.CONFIG_CHALLENGES:
self.config.cleanup() self.config.cleanup()
@@ -484,7 +480,7 @@ class Client(object):
path = challenge.gen_challenge_path( path = challenge.gen_challenge_path(
challenge_msg["challenges"], challenge_msg.get("combinations", [])) challenge_msg["challenges"], challenge_msg.get("combinations", []))
logger.info("Performing the following challenges:") logging.info("Performing the following challenges:")
# Every indices element is a list of integers referring to which # Every indices element is a list of integers referring to which
# challenges in the master list the challenge object satisfies # challenges in the master list the challenge object satisfies
@@ -507,8 +503,8 @@ class Client(object):
for index in indices[i]: for index in indices[i]:
responses[index] = response responses[index] = response
logger.info("Configured Apache for challenges; " + logging.info(
"waiting for verification...") "Configured Apache for challenges; waiting for verification...")
return responses, challenge_objs return responses, challenge_objs
@@ -528,9 +524,10 @@ class Client(object):
idx = 0 idx = 0
if encrypt: if encrypt:
logger.error("Unfortunately securely storing the certificates/" logging.error(
"keys is not yet available. Stay tuned for the " "Unfortunately securely storing the certificates/"
"next update!") "keys is not yet available. Stay tuned for the "
"next update!")
return False return False
if os.path.isfile(list_file): if os.path.isfile(list_file):
@@ -566,9 +563,8 @@ class Client(object):
""" """
for ssl_vh in vhost: for ssl_vh in vhost:
success, redirect_vhost = self.config.enable_redirect(ssl_vh) success, redirect_vhost = self.config.enable_redirect(ssl_vh)
# pylint: disable=maybe-no-member logging.info(
logger.info("\nRedirect vhost: " + redirect_vhost.filep + "\nRedirect vhost: %s - %s ", redirect_vhost.filep, success)
" - " + str(success))
# If successful, make sure redirect site is enabled # If successful, make sure redirect site is enabled
if success: if success:
self.config.enable_site(redirect_vhost) self.config.enable_site(redirect_vhost)
@@ -615,20 +611,20 @@ class Client(object):
chall = challenges[index] chall = challenges[index]
if chall["type"] == "dvsni": if chall["type"] == "dvsni":
logger.info(" DVSNI challenge for name %s." % name) logging.info(" DVSNI challenge for name %s.", name)
sni_satisfies.append(index) sni_satisfies.append(index)
sni_todo.append((str(name), str(chall["r"]), sni_todo.append((str(name), str(chall["r"]),
str(chall["nonce"]))) str(chall["nonce"])))
elif chall["type"] == "recoveryToken": elif chall["type"] == "recoveryToken":
logger.info("\tRecovery Token Challenge for name: %s." % name) logging.info("\tRecovery Token Challenge for name: %s.", name)
challenge_obj_indices.append(index) challenge_obj_indices.append(index)
challenge_objs.append({ challenge_objs.append({
type: "recoveryToken", type: "recoveryToken",
}) })
else: else:
logger.fatal("Challenge not currently supported") logging.fatal("Challenge not currently supported")
sys.exit(82) sys.exit(82)
if sni_todo: if sni_todo:
@@ -640,7 +636,7 @@ class Client(object):
"dvsni_key": self.privkey, "dvsni_key": self.privkey,
}) })
challenge_obj_indices.append(sni_satisfies) challenge_obj_indices.append(sni_satisfies)
logger.debug(sni_todo) logging.debug(sni_todo)
return challenge_objs, challenge_obj_indices return challenge_objs, challenge_obj_indices
@@ -663,7 +659,7 @@ class Client(object):
key_f.write(key_pem) key_f.write(key_pem)
key_f.close() key_f.close()
logger.info("Generating key: %s" % key_filename) logging.info("Generating key: %s", key_filename)
self.privkey = Client.Key(key_filename, key_pem) self.privkey = Client.Key(key_filename, key_pem)
@@ -678,7 +674,7 @@ class Client(object):
csr_f.write(csr_pem) csr_f.write(csr_pem)
csr_f.close() csr_f.close()
logger.info("Creating CSR: %s" % csr_filename) logging.info("Creating CSR: %s", csr_filename)
self.csr = Client.CSR(csr_filename, csr_der, "der") self.csr = Client.CSR(csr_filename, csr_der, "der")
elif self.csr.type != "der": elif self.csr.type != "der":
@@ -726,22 +722,14 @@ class Client(object):
sanity_check_names(names) sanity_check_names(names)
if not names: if not names:
logger.fatal("No domain names were found in your apache config") logging.fatal("No domain names were found in your apache config")
logger.fatal("Either specify which names you would like " logging.fatal("Either specify which names you would like "
"letsencrypt to validate or add server names " "letsencrypt to validate or add server names "
"to your virtual hosts") "to your virtual hosts")
sys.exit(1) sys.exit(1)
return names return names
def init_logger(self):
if self.use_curses:
logger.setLogger(logger.NcursesLogger())
logger.setLogLevel(logger.INFO)
else:
logger.setLogger(logger.FileLogger(sys.stdout))
logger.setLogLevel(logger.INFO)
def remove_cert_key(cert): def remove_cert_key(cert):
"""Remove certificate key. """Remove certificate key.
@@ -778,7 +766,7 @@ def sanity_check_names(names):
""" """
for name in names: for name in names:
if not is_hostname_sane(name): if not is_hostname_sane(name):
logger.fatal(repr(name) + " is an impossible hostname") logging.fatal("%r is an impossible hostname", name)
sys.exit(81) sys.exit(81)
+2 -2
View File
@@ -1,6 +1,7 @@
"""Let's Encrypt client crypto utility functions""" """Let's Encrypt client crypto utility functions"""
import binascii import binascii
import hashlib import hashlib
import logging
import time import time
from Crypto import Random from Crypto import Random
@@ -12,7 +13,6 @@ import M2Crypto
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client import logger
# TODO: All of these functions need unit tests # TODO: All of these functions need unit tests
@@ -53,7 +53,7 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
hashed = Crypto.Hash.SHA256.new(msg_with_nonce) hashed = Crypto.Hash.SHA256.new(msg_with_nonce)
signature = Crypto.Signature.PKCS1_v1_5.new(key).sign(hashed) signature = Crypto.Signature.PKCS1_v1_5.new(key).sign(hashed)
logger.debug('%s signed as %s' % (msg_with_nonce, signature)) logging.debug('%s signed as %s', msg_with_nonce, signature)
n_bytes = binascii.unhexlify(leading_zeros(hex(key.n)[2:].rstrip("L"))) n_bytes = binascii.unhexlify(leading_zeros(hex(key.n)[2:].rstrip("L")))
e_bytes = binascii.unhexlify(leading_zeros(hex(key.e)[2:].rstrip("L"))) e_bytes = binascii.unhexlify(leading_zeros(hex(key.e)[2:].rstrip("L")))
+64
View File
@@ -0,0 +1,64 @@
"""Logging utilities."""
import logging
import dialog
from letsencrypt.client import display
class DialogHandler(logging.Handler):
"""Logging handler using dialog info box.
:ivar int height: Height of the info box (without padding).
:ivar int width: Width of the info box (without padding).
:ivar list lines: Lines to be displayed in the info box.
:ivar d: Instance of :class:`dialog.Dialog`.
"""
PADDING_HEIGHT = 2
PADDING_WIDTH = 4
def __init__(self, level=logging.NOTSET, height=display.HEIGHT,
width=display.WIDTH - 4, d=None):
# Handler not new-style -> no super
logging.Handler.__init__(self, level)
self.height = height
self.width = width
# "dialog" collides with module name... pylint: disable=invalid-name
self.d = dialog.Dialog() if d is None else d
self.lines = []
def emit(self, record):
"""Emit message to a dialog info box.
Only show the last (self.height) lines; note that lines can wrap
at self.width, so a single line could actually be multiple
lines.
"""
for line in (record.msg % record.args).splitlines():
# check for lines that would wrap
cur_out = line
while len(cur_out) > self.width:
# find first space before self.width chars into cur_out
last_space_pos = cur_out.rfind(' ', 0, self.width)
if last_space_pos == -1:
# no spaces, just cut them off at whatever
self.lines.append(cur_out[0:self.width])
cur_out = cur_out[self.width:]
else:
# cut off at last space
self.lines.append(cur_out[0:last_space_pos])
cur_out = cur_out[last_space_pos + 1:]
if cur_out != '':
self.lines.append(cur_out)
# show last 16 lines
content = '\n'.join(self.lines[-self.height:])
# add the padding around the box
self.d.infobox(
content, self.height + self.PADDING_HEIGHT,
self.width + self.PADDING_WIDTH)
-189
View File
@@ -1,189 +0,0 @@
"""Logger."""
import logger
import textwrap
import time
import dialog
from letsencrypt.client import display
from letsencrypt.client import errors
class Singleton(object):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(
Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
# log levels
TRACE = 5
DEBUG = 4
INFO = 3
WARN = 2
ERROR = 1
FATAL = 0
NONE = -1
class Logger(Singleton):
debugLevelStr = {TRACE: 'TRACE', DEBUG: 'DEBUG', INFO: 'INFO',
WARN: 'WARN', ERROR: 'ERROR', FATAL: 'FATAL'}
def __init__(self):
self.level = INFO
def debugLevel(self, level=DEBUG):
return self.debugLevelStr[level]
def log(self, level, data):
raise errors.LetsEncryptClientError("No Logger defined")
def timefmt(self, t=None):
if t is None:
t = time.time()
return time.strftime("%b %d %Y %H:%M:%S",
time.localtime(t)) + ('%.03f' % (t - int(t)))[1:]
class FileLogger(Logger):
def __init__(self, outfile):
self.outfile = outfile
def log(self, level, data):
msg = "%s [%s] %s" % (self.timefmt(), self.debugLevel(level), data)
wm = textwrap.fill(msg, 80)
self.outfile.write("%s\n" % wm)
class NcursesLogger(Logger):
def __init__(self,
firstmessage="",
height=display.HEIGHT,
width=display.WIDTH-4):
self.lines = []
self.all_content = ""
self.d = dialog.Dialog()
self.height = height
self.width = width
self.add(firstmessage)
'''
Only show the last (self.height) lines;
note that lines can wrap at self.width, so
a single line could actually be multiple lines
'''
def add(self, s):
self.all_content += s
for line in s.splitlines():
# check for lines that would wrap
cur_out = line
while len(cur_out) > self.width:
# find first space before self.width chars into cur_out
last_space_pos = cur_out.rfind(' ', 0, self.width)
if (last_space_pos == -1):
# no spaces, just cut them off at whatever
self.lines.append(cur_out[0:self.width])
cur_out = cur_out[self.width:]
else:
# cut off at last space
self.lines.append(cur_out[0:last_space_pos])
cur_out = cur_out[last_space_pos+1:]
if cur_out != '':
self.lines.append(cur_out)
# show last 16 lines
self.content = '\n'.join(self.lines[-self.height:])
self.show()
def show(self):
# add the padding around the box
self.d.infobox(self.content, self.height+2, self.width+4)
def log(self, level, data):
self.add(str(data) + "\n")
log_instance = None
def setLogger(log_inst):
global log_instance
log_instance = log_inst
def setLogLevel(log_level):
global log_instance
log_instance.level = log_level
def log(level, data):
global log_instance
if level <= log_instance.level:
log_instance.log(level, data)
def trace(data):
log(TRACE, data)
def debug(data):
log(DEBUG, data)
def info(data):
log(INFO, data)
def warn(data):
log(WARN, data)
def error(data):
log(ERROR, data)
def fatal(data):
log(FATAL, data)
def none(data):
# Uh...what?
pass
if __name__ == "__main__":
# Unit test/example usage:
# Set the logging type you want to use (stdout logging):
# logger.setLogger(FileLogger(sys.stdout))
setLogger(NcursesLogger())
# Set the most verbose you want to log
# (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, NONE)
setLogLevel(logger.TRACE)
# Log a message:
# logger.log(logger.INFO, "logger!")
time.sleep(0.01)
info(("This is a long line, it's pretty long, butitalso hasbig wordsthat "
"areprobably hardtobreak oninan easywayforthe ncurseslib, "
"sowhatdoes itdo then?"))
info("aa " + "a"*70 + "B")
for i in range(20):
info("iteration #%d/20" % i)
time.sleep(0.3)
# Alternatively, use
error("errrrr")
trace("some trace data: %d - %f - %s" % (5, 8.3, 'cows'))
+1 -1
View File
@@ -66,7 +66,7 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator):
# # validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# # matchObj = validChars.match(arg) # # matchObj = validChars.match(arg)
# # if matchObj.group() != arg: # # if matchObj.group() != arg:
# # logger.error("Error: Invalid regexp characters in %s" % arg) # # logging.error("Error: Invalid regexp characters in %s", arg)
# # return [] # # return []
# # Standardize the include argument based on server root # # Standardize the include argument based on server root
@@ -13,7 +13,6 @@ from letsencrypt.client import apache_configurator
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import logger
UBUNTU_CONFIGS = pkg_resources.resource_filename( UBUNTU_CONFIGS = pkg_resources.resource_filename(
@@ -24,8 +23,6 @@ class TwoVhost80Test(unittest.TestCase):
"""Test two standard well configured HTTP vhosts.""" """Test two standard well configured HTTP vhosts."""
def setUp(self): def setUp(self):
logger.setLogger(logger.FileLogger(sys.stdout))
logger.setLogLevel(logger.INFO)
display.set_display(display.NcursesDisplay()) display.set_display(display.NcursesDisplay())
self.temp_dir = os.path.join( self.temp_dir = os.path.join(
+43
View File
@@ -0,0 +1,43 @@
"""Tests for letsencrypt.client.log."""
import unittest
import mock
class DialogHandlerTest(unittest.TestCase):
def setUp(self):
self.d = mock.MagicMock() # pylint: disable=invalid-name
from letsencrypt.client.log import DialogHandler
self.handler = DialogHandler(height=2, width=6, d=self.d)
self.handler.PADDING_HEIGHT = 2
self.handler.PADDING_WIDTH = 4
def test_adds_padding(self):
self.handler.emit(mock.MagicMock())
self.d.infobox.assert_called_once_with(mock.ANY, 4, 10)
def test_args_in_msg_get_replaced(self):
assert len('123456') <= self.handler.width
self.handler.emit(mock.MagicMock(msg='123%s', args=(456,)))
self.d.infobox.assert_called_once_with('123456', mock.ANY, mock.ANY)
def test_wraps_nospace_is_greedy(self):
assert len('1234567') > self.handler.width
self.handler.emit(mock.MagicMock(msg='1234567'))
self.d.infobox.assert_called_once_with('123456\n7', mock.ANY, mock.ANY)
def test_wraps_at_whitespace(self):
assert len('123 567') > self.handler.width
self.handler.emit(mock.MagicMock(msg='123 567'))
self.d.infobox.assert_called_once_with('123\n567', mock.ANY, mock.ANY)
def test_only_last_lines_are_printed(self):
assert len('a\nb\nc'.split()) > self.handler.height
self.handler.emit(mock.MagicMock(msg='a\n\nb\nc'))
self.d.infobox.assert_called_once_with('b\nc', mock.ANY, mock.ANY)
if __name__ == '__main__':
unittest.main()
+11 -7
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Parse command line and call the appropriate functions.""" """Parse command line and call the appropriate functions."""
import argparse import argparse
import logging
import os import os
import sys import sys
@@ -8,10 +9,7 @@ from letsencrypt.client import apache_configurator
from letsencrypt.client import CONFIG from letsencrypt.client import CONFIG
from letsencrypt.client import client from letsencrypt.client import client
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import logger from letsencrypt.client import log
logger.setLogger(logger.FileLogger(sys.stdout))
logger.setLogLevel(logger.INFO)
def main(): def main():
@@ -55,20 +53,26 @@ def main():
"HTTP and HTTPS.") "HTTP and HTTPS.")
parser.add_argument("-e", "--agree-eula", dest="eula", action="store_true", parser.add_argument("-e", "--agree-eula", dest="eula", action="store_true",
help="Skip the end user license agreement screen.") help="Skip the end user license agreement screen.")
parser.add_argument("-t", "--text", dest="curses", action="store_false", parser.add_argument("-t", "--text", dest="use_curses", action="store_false",
help="Use the text output instead of the curses UI.") help="Use the text output instead of the curses UI.")
parser.add_argument("--test", dest="test", action="store_true", parser.add_argument("--test", dest="test", action="store_true",
help="Run in test mode.") help="Run in test mode.")
args = parser.parse_args() args = parser.parse_args()
# Set up logging
logger = logging.getLogger()
logger.setLevel(logging.INFO) # TODO: --log
if args.use_curses:
logger.addHandler(log.DialogHandler())
# Enforce '--privkey' is set along with '--csr'. # Enforce '--privkey' is set along with '--csr'.
if args.csr and not args.privkey: if args.csr and not args.privkey:
parser.error("private key file (--privkey) must be specified along{0} " parser.error("private key file (--privkey) must be specified along{0} "
"with the certificate signing request file (--csr)" "with the certificate signing request file (--csr)"
.format(os.linesep)) .format(os.linesep))
if args.curses: if args.use_curses:
display.set_display(display.NcursesDisplay()) display.set_display(display.NcursesDisplay())
else: else:
display.set_display(display.FileDisplay(sys.stdout)) display.set_display(display.FileDisplay(sys.stdout))
@@ -93,7 +97,7 @@ def main():
else: else:
csr = client.Client.CSR(args.csr[0], args.csr[1], "pem") csr = client.Client.CSR(args.csr[0], args.csr[1], "pem")
acme = client.Client(server, csr, privkey, args.curses) acme = client.Client(server, csr, privkey, args.use_curses)
if args.revoke: if args.revoke:
acme.list_certs_keys() acme.list_certs_keys()
else: else: