diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index 0a2f155f5..72934fbe0 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -1,5 +1,6 @@ """Apache Configuration based off of Augeas Configurator.""" import hashlib +import logging import os import pkg_resources import re @@ -15,7 +16,6 @@ from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util from letsencrypt.client import errors from letsencrypt.client import le_util -from letsencrypt.client import logger # Configurator should be turned into a Singleton @@ -233,13 +233,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0: # Throw some "can't find all of the directives error" - logger.warn(("Cannot find a cert or key directive in %s" - % vhost.path)) - logger.warn("VirtualHost was not modified") + logging.warn( + "Cannot find a cert or key directive in %s", vhost.path) + logging.warn("VirtualHost was not modified") # Presumably break here so that the virtualhost is not modified 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_key"][0], key) @@ -530,15 +530,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): """ if not check_ssl_loaded(): - logger.info("Loading mod_ssl into Apache Server") + logging.info("Loading mod_ssl into Apache Server") enable_mod("ssl") # Check for Listen 443 # Note: This could be made to also look for ip:443 combo # TODO: Need to search only open directives and IfMod mod_ssl.c if len(self.find_directive(case_i("Listen"), "443")) == 0: - logger.debug("No Listen 443 directive found") - logger.debug("Setting the Apache Server to Listen on port 443") + logging.debug("No Listen 443 directive found") + logging.debug("Setting the Apache Server to Listen on port 443") path = self._add_dir_to_ifmodssl( get_aug_path(self.location["listen"]), "Listen", "443") self.save_notes += "Added Listen 443 directive to %s\n" % path @@ -560,15 +560,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): tup = addr.partition(":") if tup[0] == "_default_": if not self.is_name_vhost(default_addr): - logger.debug(("Setting all VirtualHosts on " - "%s to be name based vhosts" % default_addr)) + logging.debug("Setting all VirtualHosts on %s to be " + "name based vhosts", default_addr) self.add_name_vhost(default_addr) # No default addresses... so set each one individually for addr in vhost.addrs: if not self.is_name_vhost(addr): - logger.debug(("Setting VirtualHost at %s " - "to be a name based virtual host" % addr)) + logging.debug("Setting VirtualHost at %s to be a name " + "based virtual host", addr) self.add_name_vhost(addr) def _get_ifmod(self, aug_conf_path, mod): @@ -709,7 +709,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # matchObj = validChars.match(arg) # if matchObj.group() != arg: - # logger.error("Error: Invalid regexp characters in %s" % arg) + # logging.error("Error: Invalid regexp characters in %s", arg) # return [] # Standardize the include argument based on server root @@ -775,7 +775,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): new_file.write(line) new_file.write("\n") 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) finally: orig_file.close() @@ -809,7 +809,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): vh_p = self.aug.match(("/files%s//* [label()=~regexp('%s')]" % (ssl_fp, case_i('VirtualHost')))) 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) self.add_dir(vh_p[0], "SSLCertificateFile", @@ -819,7 +819,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): self.add_dir(vh_p[0], "Include", self.location["ssl_options"]) # 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() @@ -838,7 +838,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): if (self.is_name_vhost(nonssl_vhost.addrs[i]) and not self.is_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 if need_to_save: @@ -868,7 +868,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): general_v = self._general_vhost(ssl_vhost) if general_v is None: # Add virtual_server with redirect - logger.debug( + logging.debug( "Did not find http version of ssl virtual host... creating") return self.create_redirect_vhost(ssl_vhost) else: @@ -876,10 +876,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): exists, code = self.existing_redirect(general_v) if exists: if code == 0: - logger.debug("Redirect already added") + logging.debug("Redirect already added") return True, general_v else: - logger.debug("Unknown redirect exists for this vhost") + logging.debug("Unknown redirect exists for this vhost") return False, general_v # Add directives to server self.add_dir(general_v.path, "RewriteEngine", "On") @@ -992,7 +992,7 @@ LogLevel warn \n\ # Write out file with open(redirect_filepath, 'w') as redirect_fd: redirect_fd.write(redirect_file) - logger.info("Created redirect file: " + redirect_filename) + logging.info("Created redirect file: %s", redirect_filename) self.aug.load() # Make a new vhost data structure and add it to the lists @@ -1121,8 +1121,8 @@ LogLevel warn \n\ # Can be removed once find directive can return ordered results if len(cert_path) != 1 or len(key_path) != 1: - logger.error(("Too many cert or key directives in vhost " - "%s" % vhost.filep)) + logging.error("Too many cert or key directives in vhost %s", + vhost.filep) sys.exit(40) cert = os.path.abspath(self.aug.get(cert_path[0])) @@ -1172,7 +1172,7 @@ LogLevel warn \n\ self.register_file_creation(False, enabled_path) os.symlink(vhost.filep, enabled_path) 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 return True return False @@ -1289,14 +1289,14 @@ LogLevel warn \n\ stderr=subprocess.PIPE) text = proc.communicate() except (OSError, ValueError): - logger.fatal("Unable to run /usr/sbin/apache2ctl configtest") + logging.fatal("Unable to run /usr/sbin/apache2ctl configtest") sys.exit(1) if proc.returncode != 0: # Enter recovery routine... - logger.error("Configtest failed") - logger.error(text[0]) - logger.error(text[1]) + logging.error("Configtest failed") + logging.error(text[0]) + logging.error(text[1]) return False return True @@ -1384,8 +1384,8 @@ LogLevel warn \n\ # Do weak validation that challenge is of expected type if not ("list_sni_tuple" in chall_dict and "dvsni_key" in chall_dict): - logger.fatal("Incorrect parameter given to Apache DVSNI challenge") - logger.fatal("Chall dict: " + str(chall_dict)) + logging.fatal("Incorrect parameter given to Apache DVSNI challenge") + logging.fatal("Chall dict: %s", chall_dict) sys.exit(1) addresses = [] @@ -1393,10 +1393,10 @@ LogLevel warn \n\ for tup in chall_dict["list_sni_tuple"]: vhost = self.choose_virtual_host(tup[0]) if vhost is None: - logger.error(("No vhost exists with servername " - "or alias of: %s" % tup[0])) - logger.error("No _default_:443 vhost exists") - logger.error("Please specify servernames in the Apache config") + logging.error( + "No vhost exists with servername or alias of: %s", tup[0]) + logging.error("No _default_:443 vhost exists") + logging.error("Please specify servernames in the Apache config") return None # TODO - @jdkasten review this code to make sure it makes sense @@ -1565,8 +1565,8 @@ def enable_mod(mod_name): stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w')) except (OSError, subprocess.CalledProcessError) as err: - logger.error("Error enabling mod_" + mod_name) - logger.error("Exception: %s" % str(err)) + logging.error("Error enabling mod_%s", mod_name) + logging.error("Exception: %s", err) sys.exit(1) @@ -1589,9 +1589,9 @@ def check_ssl_loaded(): stderr=open( "/dev/null", 'w')).communicate()[0] except (OSError, ValueError): - logger.error( - "Error accessing %s for loaded modules!" % CONFIG.APACHE_CTL) - logger.error("This may be caused by an Apache Configuration Error") + logging.error( + "Error accessing %s for loaded modules!", CONFIG.APACHE_CTL) + logging.error("This may be caused by an Apache Configuration Error") return False if "ssl_module" in proc: @@ -1614,14 +1614,14 @@ def apache_restart(): if proc.returncode != 0: # Enter recovery routine... - logger.error("Configtest failed") - logger.error(text[0]) - logger.error(text[1]) + logging.error("Configtest failed") + logging.error(text[0]) + logging.error(text[1]) return False except (OSError, ValueError): - logger.fatal(("Apache Restart Failed - " - "Please Check the Configuration")) + logging.fatal( + "Apache Restart Failed - Please Check the Configuration") sys.exit(1) return True @@ -1720,8 +1720,6 @@ def main(): """Main function used for quick testing purposes""" config = ApacheConfigurator() - logger.setLogger(logger.FileLogger(sys.stdout)) - logger.setLogLevel(logger.DEBUG) # for v in config.vhosts: # print v.filep diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index 9aa4ac75b..0ad813c8a 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -1,4 +1,5 @@ """Class of Augeas Configurators.""" +import logging import os import sys import shutil @@ -9,7 +10,6 @@ import augeas from letsencrypt.client import CONFIG from letsencrypt.client import configurator from letsencrypt.client import le_util -from letsencrypt.client import logger class AugeasConfigurator(configurator.Configurator): @@ -61,9 +61,9 @@ class AugeasConfigurator(configurator.Configurator): # As aug.get may return null if lens_path and lens in lens_path: # Strip off /augeas/files and /error - logger.error('There has been an error in parsing the file: ' - '%s' % path[13:len(path) - 6]) - logger.error(self.aug.get(path + '/message')) + logging.error('There has been an error in parsing the file: %s', + path[13:len(path) - 6]) + logging.error(self.aug.get(path + '/message')) def save(self, title=None, temporary=False): """Saves all changes to the configuration files. @@ -90,14 +90,14 @@ class AugeasConfigurator(configurator.Configurator): except (RuntimeError, IOError): # Check for the root of save problems 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 for err in new_errs: if err not in ex_errs: - logger.error("Unable to save file - " - "%s" % err[13:len(err)-6]) - logger.error("Attempted Save Notes") - logger.error(self.save_notes) + logging.error( + "Unable to save file - %s", err[13:len(err) - 6]) + logging.error("Attempted Save Notes") + logging.error(self.save_notes) # Erase Save Notes self.save_notes = "" return False @@ -117,7 +117,7 @@ class AugeasConfigurator(configurator.Configurator): valid, message = self.check_tempfile_saves(save_files) if not valid: - logger.fatal(message) + logging.fatal(message) # What is the protocol in this situation? # This shouldn't happen if the challenge codebase is correct return False @@ -153,8 +153,8 @@ class AugeasConfigurator(configurator.Configurator): result = self._recover_checkpoint(self.direc["temp"]) if result != 0: # We have a partial or incomplete recovery - logger.fatal("Incomplete or failed recovery for " - "%s" % self.direc["temp"]) + logging.fatal("Incomplete or failed recovery for %s", + self.direc["temp"]) sys.exit(67) # Remember to reload Augeas self.aug.load() @@ -168,24 +168,24 @@ class AugeasConfigurator(configurator.Configurator): try: rollback = int(rollback) except ValueError: - logger.error("Rollback argument must be a positive integer") + logging.error("Rollback argument must be a positive integer") # Sanity check input if rollback < 1: - logger.error("Rollback argument must be a positive integer") + logging.error("Rollback argument must be a positive integer") return backups = os.listdir(self.direc["backup"]) backups.sort() if len(backups) < rollback: - logger.error(("Unable to rollback %d checkpoints, only " - "%d exist") % (rollback, len(backups))) + 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: - logger.fatal("Failed to load checkpoint during rollback") + logging.fatal("Failed to load checkpoint during rollback") sys.exit(39) rollback -= 1 @@ -262,7 +262,7 @@ class AugeasConfigurator(configurator.Configurator): if filename not in existing_filepaths: # Tag files with index so multiple files can # have the same filename - logger.debug("Creating backup of %s" % 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') @@ -295,7 +295,7 @@ class AugeasConfigurator(configurator.Configurator): path) except (IOError, OSError): # 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 # Remove any newly added files if they exist @@ -304,7 +304,7 @@ class AugeasConfigurator(configurator.Configurator): try: shutil.rmtree(cp_dir) except OSError: - logger.error("Unable to remove directory: %s" % cp_dir) + logging.error("Unable to remove directory: %s", cp_dir) return -1 return 0 @@ -354,7 +354,7 @@ class AugeasConfigurator(configurator.Configurator): for file_path in files: new_fd.write("%s\n" % file_path) except (IOError, OSError): - logger.error("ERROR: Unable to register file creation") + logging.error("ERROR: Unable to register file creation") def recovery_routine(self): """Revert all previously modified files. @@ -373,8 +373,8 @@ class AugeasConfigurator(configurator.Configurator): # We have a partial or incomplete recovery # Not as egregious # TODO: Additional tests? recovery - logger.fatal("Incomplete or failed recovery for %s" % - self.direc["progress"]) + logging.fatal("Incomplete or failed recovery for %s", + self.direc["progress"]) sys.exit(68) # Need to reload configuration after these changes take effect @@ -403,13 +403,12 @@ class AugeasConfigurator(configurator.Configurator): if os.path.lexists(path): os.remove(path) else: - logger.warn(( + logging.warn( "File: %s - Could not be found to be deleted\n" - "Program was probably shut down unexpectedly, " - "in which case this is not a problem") % path) + "Program was probably shut down unexpectedly, ") except (IOError, OSError): - logger.fatal( - "Unable to remove filepaths contained within %s" % file_list) + logging.fatal( + "Unable to remove filepaths contained within %s", file_list) sys.exit(41) return True @@ -441,12 +440,12 @@ class AugeasConfigurator(configurator.Configurator): shutil.move(changes_since_tmp_path, changes_since_path) except (IOError, OSError): - logger.error("Unable to finalize checkpoint - adding title") + logging.error("Unable to finalize checkpoint - adding title") return False try: os.rename(cp_dir, final_dir) except OSError: - logger.error("Unable to finalize checkpoint, %s -> %s" % - (cp_dir, final_dir)) + logging.error( + "Unable to finalize checkpoint, %s -> %s", cp_dir, final_dir) return False return True diff --git a/letsencrypt/client/challenge.py b/letsencrypt/client/challenge.py index 719b04945..44aabcda4 100644 --- a/letsencrypt/client/challenge.py +++ b/letsencrypt/client/challenge.py @@ -1,8 +1,8 @@ """ACME challenge.""" +import logging import sys from letsencrypt.client import CONFIG -from letsencrypt.client import logger class Challenge(object): @@ -93,8 +93,8 @@ def _find_smart_path(challenges, combos): combo_total = 0 if not best_combo: - logger.fatal("Client does not support any combination of " - "challenges to satisfy ACME server") + logging.fatal("Client does not support any combination of " + "challenges to satisfy ACME server") sys.exit(22) return best_combo diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index e6af6db15..b6f507688 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -2,6 +2,7 @@ import collections import csv import json +import logging import os import shutil import socket @@ -21,7 +22,6 @@ from letsencrypt.client import crypto_util from letsencrypt.client import display from letsencrypt.client import errors from letsencrypt.client import le_util -from letsencrypt.client import logger # it's weird to point to chocolate servers via raw IPv6 addresses, and @@ -65,9 +65,6 @@ class Client(object): self.privkey = privkey 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 # without special packaging based on system info Command # line arg or client function to discover @@ -178,8 +175,8 @@ class Client(object): try: return self.is_expected_msg(auth_dict, "authorization") except: - logger.fatal("Failed Authorization procedure - " - "cleaning up challenges") + logging.fatal( + "Failed Authorization procedure - cleaning up challenges") sys.exit(1) finally: self.cleanup_challenges(chal_objs) @@ -193,7 +190,7 @@ class Client(object): :rtype: dict """ - logger.info("Preparing and sending CSR..") + logging.info("Preparing and sending CSR...") return self.send_and_receive_expected( acme.certificate_request(csr_der, self.privkey.pem), "certificate") @@ -303,24 +300,23 @@ class Client(object): return response elif response["type"] == "error": - logger.error("%s: %s - More Info: %s" % - (response["error"], - response.get("message", ""), - response.get("moreInfo", ""))) + logging.error( + "%s: %s - More Info: %s", response["error"], + response.get("message", ""), response.get("moreInfo", "")) raise errors.LetsEncryptClientError(response["error"]) elif response["type"] == "defer": - logger.info("Waiting for %d seconds..." % delay) + logging.info("Waiting for %d seconds...", delay) time.sleep(delay) response = self.send(acme.status_request(response["token"])) else: - logger.fatal("Received unexpected message") - logger.fatal("Expected: %s" % expected) - logger.fatal("Received: " + response) + logging.fatal("Received unexpected message") + logging.fatal("Expected: %s" % expected) + logging.fatal("Received: " + response) sys.exit(33) - logger.error("Server has deferred past the max of %d seconds" % - (rounds * delay)) + logging.error( + "Server has deferred past the max of %d seconds", rounds * delay) def list_certs_keys(self): """List trusted Let's Encrypt certificates.""" @@ -328,7 +324,7 @@ class Client(object): certs = [] if not os.path.isfile(list_file): - logger.info( + logging.info( "You don't have any certificates saved from letsencrypt") return @@ -399,8 +395,8 @@ class Client(object): cert_fd.write( crypto_util.b64_cert_to_pem(certificate_dict["certificate"])) cert_fd.close() - logger.info("Server issued certificate; certificate written to %s" % - cert_file) + logging.info( + "Server issued certificate; certificate written to %s", cert_file) if certificate_dict.get("chain", None): 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.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 cert_chain_abspath = os.path.abspath(chain_fn) @@ -420,7 +416,7 @@ class Client(object): cert_chain_abspath) # Enable any vhost that was issued to, but not enabled if not host.enabled: - logger.info("Enabling Site " + host.filep) + logging.info("Enabling Site %s", host.filep) self.config.enable_site(host) # sites may have been enabled / final cleanup @@ -464,7 +460,7 @@ class Client(object): :param dict challenges: challenges from a challenge message """ - logger.info("Cleaning up challenges...") + logging.info("Cleaning up challenges...") for chall in challenges: if chall["type"] in CONFIG.CONFIG_CHALLENGES: self.config.cleanup() @@ -484,7 +480,7 @@ class Client(object): path = challenge.gen_challenge_path( 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 # challenges in the master list the challenge object satisfies @@ -507,8 +503,8 @@ class Client(object): for index in indices[i]: responses[index] = response - logger.info("Configured Apache for challenges; " + - "waiting for verification...") + logging.info( + "Configured Apache for challenges; waiting for verification...") return responses, challenge_objs @@ -528,9 +524,10 @@ class Client(object): idx = 0 if encrypt: - logger.error("Unfortunately securely storing the certificates/" - "keys is not yet available. Stay tuned for the " - "next update!") + logging.error( + "Unfortunately securely storing the certificates/" + "keys is not yet available. Stay tuned for the " + "next update!") return False if os.path.isfile(list_file): @@ -566,9 +563,8 @@ class Client(object): """ for ssl_vh in vhost: success, redirect_vhost = self.config.enable_redirect(ssl_vh) - # pylint: disable=maybe-no-member - logger.info("\nRedirect vhost: " + redirect_vhost.filep + - " - " + str(success)) + logging.info( + "\nRedirect vhost: %s - %s ", redirect_vhost.filep, success) # If successful, make sure redirect site is enabled if success: self.config.enable_site(redirect_vhost) @@ -615,20 +611,20 @@ class Client(object): chall = challenges[index] 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_todo.append((str(name), str(chall["r"]), str(chall["nonce"]))) 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_objs.append({ type: "recoveryToken", }) else: - logger.fatal("Challenge not currently supported") + logging.fatal("Challenge not currently supported") sys.exit(82) if sni_todo: @@ -640,7 +636,7 @@ class Client(object): "dvsni_key": self.privkey, }) challenge_obj_indices.append(sni_satisfies) - logger.debug(sni_todo) + logging.debug(sni_todo) return challenge_objs, challenge_obj_indices @@ -663,7 +659,7 @@ class Client(object): key_f.write(key_pem) 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) @@ -678,7 +674,7 @@ class Client(object): csr_f.write(csr_pem) 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") elif self.csr.type != "der": @@ -726,22 +722,14 @@ class Client(object): sanity_check_names(names) if not names: - logger.fatal("No domain names were found in your apache config") - logger.fatal("Either specify which names you would like " - "letsencrypt to validate or add server names " - "to your virtual hosts") + logging.fatal("No domain names were found in your apache config") + logging.fatal("Either specify which names you would like " + "letsencrypt to validate or add server names " + "to your virtual hosts") sys.exit(1) 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): """Remove certificate key. @@ -778,7 +766,7 @@ def sanity_check_names(names): """ for name in names: 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) diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 7419b674b..d19cbc0da 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -1,6 +1,7 @@ """Let's Encrypt client crypto utility functions""" import binascii import hashlib +import logging import time from Crypto import Random @@ -12,7 +13,6 @@ import M2Crypto from letsencrypt.client import CONFIG from letsencrypt.client import le_util -from letsencrypt.client import logger # 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) 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"))) e_bytes = binascii.unhexlify(leading_zeros(hex(key.e)[2:].rstrip("L"))) diff --git a/letsencrypt/client/log.py b/letsencrypt/client/log.py new file mode 100644 index 000000000..19d33c53a --- /dev/null +++ b/letsencrypt/client/log.py @@ -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) diff --git a/letsencrypt/client/logger.py b/letsencrypt/client/logger.py deleted file mode 100644 index 5444c3ce0..000000000 --- a/letsencrypt/client/logger.py +++ /dev/null @@ -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')) diff --git a/letsencrypt/client/nginx_configurator.py b/letsencrypt/client/nginx_configurator.py index 5ca0ab68c..24bf4529b 100644 --- a/letsencrypt/client/nginx_configurator.py +++ b/letsencrypt/client/nginx_configurator.py @@ -66,7 +66,7 @@ class NginxConfigurator(augeas_configurator.AugeasConfigurator): # # validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # # matchObj = validChars.match(arg) # # if matchObj.group() != arg: - # # logger.error("Error: Invalid regexp characters in %s" % arg) + # # logging.error("Error: Invalid regexp characters in %s", arg) # # return [] # # Standardize the include argument based on server root diff --git a/letsencrypt/client/tests/apache_configurator_test.py b/letsencrypt/client/tests/apache_configurator_test.py index e8a161745..596fd2122 100644 --- a/letsencrypt/client/tests/apache_configurator_test.py +++ b/letsencrypt/client/tests/apache_configurator_test.py @@ -13,7 +13,6 @@ from letsencrypt.client import apache_configurator from letsencrypt.client import CONFIG from letsencrypt.client import display from letsencrypt.client import errors -from letsencrypt.client import logger # pylint: disable=no-member UBUNTU_CONFIGS = pkg_resources.resource_filename( @@ -30,8 +29,6 @@ def setUpModule(): global TEMP_DIR, CONFIG_DIR, WORK_DIR - logger.setLogger(logger.FileLogger(sys.stdout)) - logger.setLogLevel(logger.INFO) display.set_display(display.NcursesDisplay()) TEMP_DIR = tempfile.mkdtemp("temp") diff --git a/letsencrypt/client/tests/log_test.py b/letsencrypt/client/tests/log_test.py new file mode 100644 index 000000000..a9ff23fc2 --- /dev/null +++ b/letsencrypt/client/tests/log_test.py @@ -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='1234%s', args=(567,))) + 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 %s', args=(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() diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index fc63e5b62..8cbda62dc 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python """Parse command line and call the appropriate functions.""" import argparse +import logging import os import sys @@ -8,10 +9,7 @@ from letsencrypt.client import apache_configurator from letsencrypt.client import CONFIG from letsencrypt.client import client from letsencrypt.client import display -from letsencrypt.client import logger - -logger.setLogger(logger.FileLogger(sys.stdout)) -logger.setLogLevel(logger.INFO) +from letsencrypt.client import log def main(): @@ -55,20 +53,26 @@ def main(): "HTTP and HTTPS.") parser.add_argument("-e", "--agree-eula", dest="eula", action="store_true", 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.") parser.add_argument("--test", dest="test", action="store_true", help="Run in test mode.") 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'. if args.csr and not args.privkey: parser.error("private key file (--privkey) must be specified along{0} " "with the certificate signing request file (--csr)" .format(os.linesep)) - if args.curses: + if args.use_curses: display.set_display(display.NcursesDisplay()) else: display.set_display(display.FileDisplay(sys.stdout)) @@ -93,7 +97,7 @@ def main(): else: 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: acme.list_certs_keys() else: