From 45203d25d162f0a54ecab9130b86f9866c0e3d1f Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 9 Aug 2013 08:29:57 -0400 Subject: [PATCH 1/6] Update tuf-spec.txt and implement "lazy bin walk" tuf-spec.txt was updated to include the latest metadata changes, such as version numbers, and the "lazy bin walk" scheme was implemented in updater.py. --- docs/tuf-spec.txt | 64 ++++++++++++++++++++-------------------- tuf/client/updater.py | 68 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/docs/tuf-spec.txt b/docs/tuf-spec.txt index 0c88c437..517d17e3 100644 --- a/docs/tuf-spec.txt +++ b/docs/tuf-spec.txt @@ -32,7 +32,10 @@ in all popular Linux package managers. More information and current versions of this document can be found at https://www.updateframework.com/ - The development of TUF is supported by GENI (http://www.geni.net/). + The Global Environment for Network Innovations (GENI) and the National + Science Foundation (NSF) have provided support for the development of TUF. + (http://www.geni.net/) + (http://www.nsf.gov/) TUF's Python implementation is based heavily on Thandy, the application updater for Tor (http://www.torproject.org/). Its design and this spec are @@ -409,26 +412,24 @@ 4.2. File formats: general principles All signed files are of the format: - { "signed" : X, + { "signed" : ROLE, "signatures" : [ - { "keyid" : K, - "method" : M, - "sig" : S } + { "keyid" : KEYID, + "method" : METHOD, + "sig" : SIGNATURE } , ... ] } - where: X is a list whose first element describes the signed object. - K is the identifier of a key signing the document - M is the method to be used to make the signature - S is a signature of the canonical encoding of X using the - identified key. + where: ROLE is a dictionary whose "_type" field describes the role type. + KEYID is the identifier of the key signing the ROLE dictionary. + METHOD is the key signing method used to generate the signature. + SIGNATURE is a signature of the canonical encoding of ROLE using the + signing key belonging to KEYID. We define one signing method at present: - sha256-pkcs1 : A base64 encoded signature of the SHA256 hash of the - canonical encoding of X, using PKCS-1 padding. + "evp" : An interface to OpenSSL's EVP functions. - All times are given as strings of the format "YYYY-MM-DD HH:MM:SS", - in UTC. + All times are given as strings of the format "YYYY-MM-DD HH:MM:SS UTC". All keys are of the format: { "keytype" : KEYTYPE, @@ -443,13 +444,12 @@ We define one keytype at present: 'rsa'. Its format is: { "keytype" : "rsa", - "keyval" : { "e" : E, - "n" : N } + "keyval" : { "public" : PUBLIC, + "private" : PRIVATE } } - where E and N are the binary representations of the exponent and - modulus, encoded as big-endian numbers in base64. All RSA keys must - be at least 2048 bits long. + where PUBLIC and PRIVATE are in PEM format and are strings. All RSA keys + must be at least 2048 bits long. 4.3. File formats: root.txt @@ -462,7 +462,7 @@ The format of root.txt is as follows: { "_type" : "Root", - "ts" : TIME, + "version" : VERSION, "expires" : EXPIRES, "keys" : { KEYID : KEY @@ -474,12 +474,11 @@ , ... } } - The "ts" line describes when this file was updated. Clients - MUST NOT replace a file with an older one, and SHOULD NOT accept a - file too far in the future. + VERSION is an integer that is greater than 0. Clients MUST NOT replace a + metadata file with a version number less than the one currently trusted. - The "expires" line states when the metadata should be considered expired - and no longer trusted by clients. Clients MUST NOT trust an expired file. + EXPIRES determines when metadata should be considered expired and no longer + trusted by clients. Clients MUST NOT trust an expired file. A ROLE is one of "root", "release", "targets", "timestamp", or "mirrors". A role for each of "root", "release", "timestamp", and "targets" MUST be @@ -505,7 +504,7 @@ The format of release.txt is as follows: { "_type" : "Release", - "ts" : TIME, + "version" : VERSION, "expires" : EXPIRES, "meta" : METAFILES } @@ -527,7 +526,7 @@ The format of targets.txt is as follows: { "_type" : "Targets", - "ts" : TIME, + "version" : VERSION, "expires" : EXPIRES, "targets" : TARGETS, ("delegations" : DELEGATIONS) @@ -572,10 +571,9 @@ The "paths" list describes paths that the role is trusted to provide. Clients MUST check that a target is in one of the trusted paths of all roles in a delegation chain, not just in a trusted path of the role that describes - the target file. The format of a PATHPATTERN may be either a path to a - single file or a path to a directory and end with "/**" to indicate all - files under that directory. The value of "/**" by itself therefore means - all files. + the target file. The format of a PATHPATTERN may be either a path to a single + file, or a path to a directory to indicate all files and/or subdirectories + under that directory. We are currently investigating a few "priority tag" schemes to resolve conflicts between delegated roles that share responsibility for overlapping @@ -610,7 +608,7 @@ The format of the timestamp file is as follows: { "_type" : "Timestamp", - "ts" : TIME, + "version" : VERSION, "expires" : EXPIRES, "meta" : METAFILES } @@ -628,7 +626,7 @@ The format of mirrors.txt is as follows: { "_type" : "Mirrorlist", - "ts" : TIME, + "version" : VERSION, "expires" : EXPIRES, "mirrors" : [ { "urlbase" : URLBASE, diff --git a/tuf/client/updater.py b/tuf/client/updater.py index dce742b5..acc6daf5 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1534,6 +1534,13 @@ def target(self, target_filepath): # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.RELPATH_SCHEMA.check_match(target_filepath) + # The algorithm used by the repository to generate the hashes of the + # target filepaths. The repository may optionally organize + # targets into hashed bins to ease target delegations and role metadata + # management. The use of consistent hashing allows for a uniform + # distribution of targets into bins. + HASH_PATH_ALGORITHM = 'sha256' + # Ensure the client has the most up-to-date version of 'targets.txt'. # Raise 'tuf.MetadataNotAvailableError' if the changed metadata # cannot be successfully downloaded and 'tuf.RepositoryError' if the @@ -1545,12 +1552,23 @@ def target(self, target_filepath): # The target is assumed to be missing until proven otherwise. target = None + # Calculate the hash of the filepath to determine which bin to find the + # target. The client currently assumes the repository uses + # 'HASH_PATH_ALGORITHM' to generate hashes. + # TODO: Should the TUF spec restrict the repository to one particular + # algorithm? Should we allow the repository to specify in the role + # dictionary the algorithm used for these generated hashed paths? + digest_object = tuf.hash.digest(HASH_PATH_ALGORITHM) + digest_object.update(target_filepath) + target_file_path_hash = digest_object.hexdigest() + try: current_metadata = self.metadata['current'] role_names = ['targets'] # Preorder depth-first traversal of the tree of target delegations. while len(role_names) > 0 and target is None: + # Pop the role name from the top of the stack. role_name = role_names.pop(-1) @@ -1575,20 +1593,48 @@ def target(self, target_filepath): break # Push children in reverse order of appearance onto the stack. + # NOTE: This may be a slow operation if there are many delegated roles + # or bins. for child_role in reversed(child_roles): child_role_name = child_role['name'] - child_role_paths = child_role['paths'] + child_role_paths = child_role.get('paths') + child_role_path_hash_prefix = child_role.get('path_hash_prefix') - # Ensure that we explore only delegated roles trusted with the target. - # We assume conservation of delegated paths in the complete tree of - # delegations. Note that the call to _ensure_all_targets_allowed in - # _update_metadata should already ensure that all targets metadata is - # valid; i.e. that the targets signed by a delegatee is a proper - # subset of the targets delegated to it by the delegator. - # Nevertheless, we check it again here for performance and safety - # reasons. - if target_filepath in child_role_paths: - role_names.append(child_role_name) + if child_role_path_hash_prefix is not None: + if target_file_path_hash.startswith(child_role_path_hash_prefix): + + # Found a matching path hash prefix. The metadata for + # 'child_role_name' will be retrieved on the next iteration + # of the while-loop. + role_names.append(child_role_name) + elif child_role_paths is not None: + + # Ensure that we explore only delegated roles trusted with the target. + # We assume conservation of delegated paths in the complete tree of + # delegations. Note that the call to _ensure_all_targets_allowed in + # _update_metadata should already ensure that all targets metadata is + # valid; i.e. that the targets signed by a delegatee is a proper + # subset of the targets delegated to it by the delegator. + # Nevertheless, we check it again here for performance and safety + # reasons. + for child_role_path in child_role_paths: + + # A child role path may be a filepath or directory. Explore + # directories which may contain 'target_filepath'. + prefix = os.path.commonprefix([target_filepath, child_role_path]) + if target_filepath in child_role_paths: + + # The metadata for 'child_role_name' will be retrieved on the next + # iteration of the while-loop. + role_names.append(child_role_name) + else: + + # 'role_name' should have been validated when it was downloaded. + # The 'paths' or 'path_hash_prefix' fields should not be missing, + # but log a warning if this else clause is reached. + message = repr(child_role)+' unexpectedly did not contain one of '+\ + 'the required fields ("paths" or "path_hash_prefix").' + logger.warn(message) except: raise finally: From f214d9019e1938c586596b112bddbf28f13d3f15 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 9 Aug 2013 10:43:26 -0400 Subject: [PATCH 2/6] Expand comment and add missing prefix comparison in updater.target() --- tuf/client/updater.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index acc6daf5..5fe9d0a9 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1619,10 +1619,11 @@ def target(self, target_filepath): # reasons. for child_role_path in child_role_paths: - # A child role path may be a filepath or directory. Explore - # directories which may contain 'target_filepath'. + # A child role path may be a filepath or directory. The child + # role 'child_role_name' is added if 'target_filepath' is located + # under 'child_role_path'. Explicit filepaths are also added. prefix = os.path.commonprefix([target_filepath, child_role_path]) - if target_filepath in child_role_paths: + if prefix == child_role_path: # The metadata for 'child_role_name' will be retrieved on the next # iteration of the while-loop. From e5731749bfa1f3f715a7e8426273b287652d39cc Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 9 Aug 2013 12:13:01 -0400 Subject: [PATCH 3/6] Modify _ensure_all_targets_allowed() to also work with path_hash_prefix --- tuf/client/updater.py | 88 +++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 5fe9d0a9..83f5f878 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -912,7 +912,9 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): under 'paths'. A parent role may delegate trust to all files under a particular directory, including files in subdirectories, by simply listing the directory (e.g., 'packages/source/Django/', the equivalent - of 'packages/source/Django/*'). + of 'packages/source/Django/*'). Targets listed in hashed bins are + also validated (i.e., its calculated path hash prefix must be delegated + by the parent role. metadata_role: @@ -928,7 +930,8 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): tuf.RepositoryError: If the targets of 'metadata_role' are not allowed according to - the parent's metadata file. + the parent's metadata file. The 'paths' and 'path_hash_prefix' fields + are verified. None. @@ -938,6 +941,13 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): """ + # The algorithm used by the repository to generate the hashes of the + # target filepaths. The repository may optionally organize + # targets into hashed bins to ease target delegations and role metadata + # management. The use of consistent hashing allows for a uniform + # distribution of targets into bins. + HASH_PATH_ALGORITHM = 'sha256' + # Return if 'metadata_role' is 'targets'. 'targets' is not # a delegated role. if metadata_role == 'targets': @@ -955,30 +965,60 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): role_index = tuf.repo.signerlib.find_delegated_role(roles, metadata_role) # Ensure the delegated role exists prior to extracting trusted paths - # from the parent's 'paths'. + # from the parent's 'paths', or trusted path hash prefixes from the parent's + # 'path_hash_prefix'. if role_index is not None: role = roles[role_index] - allowed_child_paths = role['paths'] + allowed_child_paths = role.get('paths') + allowed_child_path_hash_prefix = role.get('path_hash_prefix') actual_child_targets = metadata_object['targets'].keys() - - # Check that each delegated target is either explicitly listed or a parent - # directory is found under role['paths'], otherwise raise an exception. - # If the parent role explicitly lists target file paths in 'paths', - # this loop will run in O(n^2), the worst-case. The repository - # maintainer will likely delegate entire directories, and opt for - # explicit file paths if the targets in a directory are delegated to - # different roles/developers. - for child_target in actual_child_targets: - for allowed_child_path in allowed_child_paths: - prefix = os.path.commonprefix([child_target, allowed_child_path]) - if prefix == allowed_child_path: - break - else: - message = 'Role '+repr(metadata_role)+' specifies target '+\ - repr(child_target)+' which is not an allowed path according '+\ - 'to the delegations set by '+repr(parent_role)+'.' - raise tuf.RepositoryError(message) + if allowed_child_path_hash_prefix is not None: + for child_target in actual_child_targets: + # Calculate the hash of 'child_target' to determine if it has been + # placed in the correct bin. The client currently assumes the + # repository uses 'HASH_PATH_ALGORITHM' to generate hashes. + # TODO: Should the TUF spec restrict the repository to one particular + # algorithm? Should we allow the repository to specify in the role + # dictionary the algorithm used for these generated hashed paths? + digest_object = tuf.hash.digest(HASH_PATH_ALGORITHM) + digest_object.update(child_target) + child_target_path_hash = digest_object.hexdigest() + + if not child_target_path_hash.startswith(allowed_child_path_hash_prefix): + message = 'Role '+repr(metadata_role)+' specifies target '+\ + repr(child_target)+ ' which does not have a path hash prefix '+\ + 'matching the prefix listed by the parent role '+\ + repr(parent_role)+'.' + raise tuf.RepositoryError(message) + elif allowed_child_paths is not None: + + # Check that each delegated target is either explicitly listed or a parent + # directory is found under role['paths'], otherwise raise an exception. + # If the parent role explicitly lists target file paths in 'paths', + # this loop will run in O(n^2), the worst-case. The repository + # maintainer will likely delegate entire directories, and opt for + # explicit file paths if the targets in a directory are delegated to + # different roles/developers. + for child_target in actual_child_targets: + for allowed_child_path in allowed_child_paths: + prefix = os.path.commonprefix([child_target, allowed_child_path]) + if prefix == allowed_child_path: + break + else: + message = 'Role '+repr(metadata_role)+' specifies target '+\ + repr(child_target)+' which is not an allowed path according '+\ + 'to the delegations set by '+repr(parent_role)+'.' + raise tuf.RepositoryError(message) + else: + + # 'role' should have been validated when it was downloaded. + # The 'paths' or 'path_hash_prefix' fields should not be missing, + # so log a warning if this else clause is reached. + message = repr(role)+' unexpectedly did not contain one of '+\ + 'the required fields ("paths" or "path_hash_prefix").' + logger.warn(message) + # Raise an exception if the parent has not delegated to the specified # 'metadata_role' child role. else: @@ -1014,7 +1054,7 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): dict conforms to 'tuf.formats.FILEINFO_SCHEMA' and has the form: {'length': 23423 - 'hashes': {'sha256': adfbc32343..}} + 'hashes': {'sha256': /dfbc32343..}} None. @@ -1632,7 +1672,7 @@ def target(self, target_filepath): # 'role_name' should have been validated when it was downloaded. # The 'paths' or 'path_hash_prefix' fields should not be missing, - # but log a warning if this else clause is reached. + # so log a warning if this else clause is reached. message = repr(child_role)+' unexpectedly did not contain one of '+\ 'the required fields ("paths" or "path_hash_prefix").' logger.warn(message) From 28601181cf6b27bce57809a02e13eb200522e9fc Mon Sep 17 00:00:00 2001 From: vladdd Date: Mon, 12 Aug 2013 12:20:48 -0400 Subject: [PATCH 4/6] Review and confirm issue #63 --- tuf/repo/keystore.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tuf/repo/keystore.py b/tuf/repo/keystore.py index c2ca98c0..c8fe5afd 100755 --- a/tuf/repo/keystore.py +++ b/tuf/repo/keystore.py @@ -147,7 +147,7 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): directory_name: The name of the directory containing the key files ('.key'), - conformant to tuf.formats.RELPATH_SCHEMA. + conformant to 'tuf.formats.RELPATH_SCHEMA'. keyids: A list containing the keyids of the signing keys to load. @@ -188,10 +188,8 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): logger.info('Loading private key(s) from '+repr(directory_name)) - # Make sure the directory exists. - if not os.path.exists(directory_name): - logger.warn('...no such directory. Keystore cannot be loaded.') - else: + # Load the private key(s) if 'directory_name' exists, otherwise log a warning. + if os.path.exists(directory_name): # Decrypt the keys we can from those stored in 'keyids'. for keyid in keyids: try: @@ -243,7 +241,11 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): logger.warn(repr(full_filepath)+' contains an invalid key type.') continue + else: + logger.warn('...no such directory. Keystore cannot be loaded.') + logger.info('Done.') + return loaded_keys From ae3b51ab3d97889af7324fbd80bb9ddff0186a61 Mon Sep 17 00:00:00 2001 From: vladdd Date: Mon, 12 Aug 2013 12:47:26 -0400 Subject: [PATCH 5/6] Confirm unit tests run properly individually & fix test_keystore "test_keystore.py" logged TUF messages if run individually. --- tuf/tests/test_keystore.py | 7 +++++++ tuf/tests/test_push.py | 0 tuf/tests/test_pushtoolslib.py | 0 3 files changed, 7 insertions(+) mode change 100644 => 100755 tuf/tests/test_push.py mode change 100644 => 100755 tuf/tests/test_pushtoolslib.py diff --git a/tuf/tests/test_keystore.py b/tuf/tests/test_keystore.py index 196bb7d4..816f400a 100755 --- a/tuf/tests/test_keystore.py +++ b/tuf/tests/test_keystore.py @@ -19,12 +19,19 @@ import unittest import shutil import os +import logging import tuf.repo.keystore import tuf.rsa_key import tuf.formats import tuf.util +logger = logging.getLogger('tuf') + +# Disable all logging calls of level CRITICAL and below. +# Comment the line below to enable logging. +logging.disable(logging.CRITICAL) + # We'll need json module for testing '_encrypt()' and '_decrypt()' # internal function. json = tuf.util.import_json() diff --git a/tuf/tests/test_push.py b/tuf/tests/test_push.py old mode 100644 new mode 100755 diff --git a/tuf/tests/test_pushtoolslib.py b/tuf/tests/test_pushtoolslib.py old mode 100644 new mode 100755 From 5cbb8c48f920542e1d1cface90b29d03459136ac Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 13 Aug 2013 12:19:31 -0400 Subject: [PATCH 6/6] Refactor log.py and change the default logging behavior Previously, logging messages were written to "tuf.log" *and* and the console, by default. Modules had to explicitly disable the logger to silence console messages. TUF, when integrated by a software updater, should not log messages to console by default. The design change now forces modules to call tuf.log.add_console_handler() to enable logging messages to console. The logger, file, and console handlers may have independent logging levels. --- tuf/log.py | 187 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 31 deletions(-) diff --git a/tuf/log.py b/tuf/log.py index cd61ecf7..f3c018bc 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -12,10 +12,9 @@ See LICENSE for licensing information. - A central location for all logging-related configuration. - This module should be imported once by the main program. - If other modules wish to incorporate 'tuf' logging, they - should do the following: + A central location for all logging-related configuration. This module should + be imported once by the main program. If other modules wish to incorporate + 'tuf' logging, they should do the following: import logging logger = logging.getLogger('tuf') @@ -26,18 +25,28 @@ instance. In this 'log.py' module, we perform the initial setup for the name 'tuf'. The 'log.py' module should only be imported once by the main program. When any other module does a logging.getLogger('tuf'), it is referring to the - same 'tuf' instance and its associated settings we set up here in 'log.py'. - See http://docs.python.org/library/logging.html#logger-objects - for more information. + same 'tuf' instance, and its associated settings, set here in 'log.py'. + See http://docs.python.org/library/logging.html#logger-objects for more + information. We use multiple handlers to process log messages in various ways and to configure each one independently. Instead of using one single manner of processing log messages, we can use two built-in handlers that have already been configured for us. For example, the built-in FileHandler will catch - log message and dump them to a file. If we wanted, we could set this file - handler to only catch CRITICAL (and greater) messages and save them to a - file. The other stream handler would still handle DEBUG-level (and greater) - messages. + log messages and dump them to a file. If we wanted, we could set this file + handler to only catch CRITICAL (and greater) messages and save them to a + file. Other handlers (e.g., StreamHandler) could handle INFO-level + (and greater) messages. + + Logging Levels: + + --Level-- --Value-- + logging.CRITICAL 50 + logging.ERROR 40 + logging.WARNING 30 + logging.INFO 20 + logging.DEBUG 10 + logging.NOTSET 0 """ @@ -45,35 +54,46 @@ import logging import time +import tuf -_DEFAULT_LOG_LEVEL = logging.INFO +# Setting a handler's log level filters only logging messages of that level +# (and above). For example, setting the built-in StreamHandler's log level to +# 'logging.WARNING' will cause the stream handler to only process messages +# of levels: WARNING, ERROR, and CRITICAL. _DEFAULT_LOG_FILENAME = 'tuf.log' +_DEFAULT_LOG_LEVEL = logging.DEBUG +_DEFAULT_CONSOLE_LOG_LEVEL = logging.INFO +_DEFAULT_FILE_LOG_LEVEL = logging.DEBUG # Set the format for logging messages. +# Example format for '_FORMAT_STRING': +# [2013-08-13 15:21:18,068 UTC] [tuf] [INFO][_update_metadata:851@updater.py] _FORMAT_STRING = '[%(asctime)s UTC] [%(name)s] [%(levelname)s]'+\ '[%(funcName)s:%(lineno)s@%(filename)s] %(message)s' + + logging.Formatter.converter = time.gmtime formatter = logging.Formatter(_FORMAT_STRING) -# Set the handlers for the logger. -# The built-in stream handler will log -# messages to 'sys.stderr' and capture -# '_DEFAULT_LOG_LEVEL' messages. -stream_handler = logging.StreamHandler() -stream_handler.setLevel(_DEFAULT_LOG_LEVEL) -stream_handler.setFormatter(formatter) +# Set the handlers for the logger. The console handler is unset by default. A +# module importing 'log.py' should explicitly set the console handler if +# outputting log messages to the screen is needed. Adding a console handler +# can be done with tuf.log.add_console_handler(). Logging messages to a file +# *is* set by default. +console_handler = None -# Set the built-in file handler. Messages -# will be logged to '_DEFAULT_LOG_FILENAME' -# and use the logger's default log level. -# The file will be opened in append mode. +# Set the built-in file handler. Messages will be logged to +# '_DEFAULT_LOG_FILENAME', and only those messages with a log level of +# '_DEFAULT_LOG_LEVEL'. The log level of messages handled by 'file_handler' +# may be modified with 'set_filehandler_log_level()'. '_DEFAULT_LOG_FILENAME' +# will be opened in append mode. file_handler = logging.FileHandler(_DEFAULT_LOG_FILENAME) +file_handler.setLevel(_DEFAULT_LOG_LEVEL) file_handler.setFormatter(formatter) # Set the logger and its settings. logger = logging.getLogger('tuf') logger.setLevel(_DEFAULT_LOG_LEVEL) -logger.addHandler(stream_handler) logger.addHandler(file_handler) # Silently ignore logger exceptions. @@ -83,27 +103,132 @@ -def set_log_level(log_level): +def set_log_level(log_level=_DEFAULT_LOG_LEVEL): """ Allow the default log level to be overridden. log_level: - The log level to set for the logger and handler(s). - E.g., logging.INFO; logging.CRITICAL. + The log level to set for the 'log.py' file handler. + 'log_level' examples: logging.INFO; logging.CRITICAL. None. - Overrides the logging level for the internal - 'logger' and 'handler'. + Overrides the logging level for the 'log.py' file handler. None. """ - + + # Does 'log_level' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.LENGTH_SCHEMA.check_match(log_level) + logger.setLevel(log_level) - stream_handler.setLevel(log_level) + + + + + +def set_filehandler_log_level(log_level=_DEFAULT_FILE_LOG_LEVEL): + """ + + Allow the default file handler log level to be overridden. + + + log_level: + The log level to set for the 'log.py' file handler. + 'log_level' examples: logging.INFO; logging.CRITICAL. + + + None. + + + Overrides the logging level for the 'log.py' file handler. + + + None. + + """ + + # Does 'log_level' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.LENGTH_SCHEMA.check_match(log_level) + + file_handler.setLevel(log_level) + + + + + +def set_console_log_level(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): + """ + + Allow the default log level for console messages to be overridden. + + + log_level: + The log level to set for the console handler. + 'log_level' examples: logging.INFO; logging.CRITICAL. + + + tuf.Error, if the 'log.py' console handler has not been set yet with + add_console_handler(). + + + Overrides the logging level for the console handler. + + + None. + + """ + + # Does 'log_level' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.LENGTH_SCHEMA.check_match(log_level) + + if console_handler is not None: + console_handler.setLevel(log_level) + else: + message = 'The console handler has not been set with add_console_handler().' + raise tuf.Error(message) + + + + +def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): + """ + + Add a console handler and set its log level to 'log_level'. + + + log_level: + The log level to set for the console handler. + 'log_level' examples: logging.INFO; logging.CRITICAL. + + + None. + + + Adds a console handler to the 'log.py' logger and sets its logging level to + 'log_level'. + + + None. + + """ + + # Does 'log_level' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.LENGTH_SCHEMA.check_match(log_level) + + # Set the console handler for the logger. The built-in console handler will + # log messages to 'sys.stderr' and capture 'log_level' messages. + console_handler = logging.StreamHandler() + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler)