python-tuf/tuf/pushtools/receivetools/receive.py
2013-03-11 16:34:22 -04:00

860 lines
33 KiB
Python
Executable file

#!/usr/bin/env python
"""
<Program Name>
receive.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
September 2012. Based on a previous version by Geremy Condra.
<Copyright>
See LICENSE for licensing information.
<Purpose>
This script can be run on a repository to import new targets metadata and
target files into the repository. This is intended to work with the
developer 'push.py' tool. When this script finds a new directory pushed
by a developer, it checks the metadata and target files and, if everything
is correct, adds the files to the repository.
Like the 'push.py' script, 'receive.py' is provided as an optional tool for
maintainers who wish to support the remote updating of target files. The
target files are provided by an outside developer. The developer generates a
correctly signed 'targets.txt' metatada file, along with the target files
specified in it, and uploads them to his/her developer directory on the
repository with 'push.py'. A repository maintainer would then run this script
to ensure a valid targets metadata file is provided and the target files match
to what is listed. Lastly, the maintainer manually generates the new
'release.txt' and 'timestamp.txt' metadata files so that clients may download
the newly added target files.
Details:
The script looks in a set of pre-defined push locations that one or more
developers may have uploaded files to using the push tool. If it finds
a valid push, it moves the push directory to a 'processing' directory and
also copies the pushed files to a temporary directory (these files are the
ones used by this script).
Once the repository has received and copied a set of target files and the
corresponding targets metadata file, it performs the following checks:
* The metadata file is newer than the last metadata file of that type.
* The metadata has not expired.
* The metadata is signed by a threshold of keys that belong to the
appropriate role.
* The target files described in the metadata are the same target files as
were provided.
Once the verification is complete, the script backs up the files to be
replaced or obsoleted, and then adds the new files to the repository. The
script then moves the push directory from the pushroot's 'processing'
directory to its 'processed' directory and writes a 'received.result' file
to the push directory. The 'received.result' file contains either the word
SUCCESS or FAILURE. There may also be a 'received.log' file written. The
client can check these files to determine whether the push was accepted and,
if not, what the problem was.
This script does not generate a new 'release.txt' file or 'timestamp.txt' file.
That needs to be done after this script runs if any pushes have been received.
In some cases, it may make sense to have this script operate on a non-live
copy of the repository and then rsync the files after all changes have been
made.
This script does not handle delegated targets metadata. When the time comes to
implement that here, care needs to be taken to ensure that a delegated targets
metadata file can't replace a target it shouldn't. Such untrusted files would
not trick clients, but they would prevent clients from obtaining updates. It
may be the case that making this script general enough to handle delegated
targets metadata may not be worth it. Such situations may be better suited to
customization per-project because the script could then leverage knowledge
about how the delegation is supposed to be done.
Usage:
$ python receive.py --config <config path>
Options:
--config <config path>
--verbose <1-5>
Example output of this script:
$ python receive.py --verbose 1 --config ./receive.cfg
[2012-09-23 21:25:35,822] [tuf.receive] [DEBUG] Looking for pushes in pushroot
/home/user/pushes
[2012-09-23 21:25:35,822] [tuf.receive] [INFO] Processing /home/user/pushes/
1348449811.39
[2012-09-23 21:25:35,822] [tuf.receive] [DEBUG] Moving push directory to
/home/user/pushes/processing/1348449811.39
[2012-09-23 21:25:35,828] [tuf.receive] [DEBUG] New metadata timestamp is
2012-09-23 23:24:17. Replacing old metadata with timestamp 2012-09-23 23:14:19
[2012-09-23 21:25:35,829] [tuf.receive] [DEBUG] Metadata will expire at
2013-09-23 23:24:17
[2012-09-23 21:25:35,834] [tuf.receive] [DEBUG] {'unknown_method_sigs': [],
'untrusted_sigs': [], 'bad_sigs': [], 'threshold': 1, 'good_sigs':
[u'efed647da99d1759637a80d225fc18e1d2a778812dd753f2d98b0311f19f26a1'],
'unknown_sigs': []}
[2012-09-23 21:25:35,834] [tuf.receive] [INFO] Number of targets specified: 3
[2012-09-23 21:25:35,835] [tuf.receive] [DEBUG] Size of target
/tmp/tmpQr4P_j/push/targets/helloworld.py is correct (19 bytes).
[2012-09-23 21:25:35,835] [tuf.receive] [DEBUG] 1 hash(es) to check.
[2012-09-23 21:25:35,835] [tuf.receive] [DEBUG] sha256 hash of target
/tmp/tmpQr4P_j/push/targets/helloworld.py is correct (9df93f8cd91e085db74d88c
788ed00c9b865370fd484884c8db077f979788376).
[2012-09-23 21:25:35,835] [tuf.receive] [DEBUG] Size of target /tmp/tmpQr4P_j
/push/targets/LICENSE is correct (12 bytes).
[2012-09-23 21:25:35,836] [tuf.receive] [DEBUG] 1 hash(es) to check.
[2012-09-23 21:25:35,836] [tuf.receive] [DEBUG] sha256 hash of target /tmp/tmp
Qr4P_j/push/targets/LICENSE is correct (f9f661288421a20acf49017975e51dd09a662b
8e6b3ca5f676d9d1feb153986c).
[2012-09-23 21:25:35,836] [tuf.receive] [DEBUG] Size of target /tmp/tmpQr4P_j/
push/targets/new_file.txt is correct (10 bytes).
[2012-09-23 21:25:35,836] [tuf.receive] [DEBUG] 1 hash(es) to check.
[2012-09-23 21:25:35,836] [tuf.receive] [DEBUG] sha256 hash of target /tmp/tmp
Qr4P_j/push/targets/new_file.txt is correct (f1fc221623f24cc1a31d972ddba368481
dd03b8bb124632fef78544342797215).
[2012-09-23 21:25:35,837] [tuf.receive] [INFO] Backing up target /var/tuf/test
-repo/targets/helloworld.py to /var/tuf/test-repo/replaced/1348449811.39aicLFk
/targets/helloworld.py
[2012-09-23 21:25:35,837] [tuf.receive] [INFO] Backing up target /var/tuf/test-
repo/targets/LICENSE to /var/tuf/test-repo/replaced/1348449811.39aicLFk/target
s/LICENSE
[2012-09-23 21:25:35,837] [tuf.receive] [INFO] Backing up old metadata /var/tuf
/src/tuf/test-repo/metadata/targets.txt to /var/tuf/test-repo/replaced/13484498
11.39aicLFk/targets.txt
[2012-09-23 21:25:35,838] [tuf.receive] [INFO] Adding target to repository: /va
r/tuf/test-repo/targets/helloworld.py
[2012-09-23 21:25:35,838] [tuf.receive] [INFO] Adding target to repository: /va
r/tuf/test-repo/targets/LICENSE
[2012-09-23 21:25:35,839] [tuf.receive] [INFO] Adding target to repository: /va
r/tuf/test-repo/targets/new_file.txt
[2012-09-23 21:25:35,839] [tuf.receive] [INFO] Adding new targets metadata to
repository: /var/tuf/test-repo/metadata/targets.txt
[2012-09-23 21:25:35,840] [tuf.receive] [DEBUG] Moving push directory to /home
/user/pushes/processed/1348449811.39
[2012-09-23 21:25:35,840] [tuf.receive] [INFO] Completed processing of all pus
hes. Push successes = 1, failures = 0.
"""
import errno
import os
import shutil
import sys
import tempfile
import time
import logging
import optparse
import tuf.formats
import tuf.keydb
import tuf.roledb
import tuf.sig
import tuf.hash
import tuf.util
import tuf.log
import tuf.pushtools.pushtoolslib
# See 'log.py' to learn how logging is handled in TUF.
logger = logging.getLogger('tuf.receive')
def receive(config_filepath):
"""
<Purpose>
Locate and process the pushes found in any of the pushroots directories.
The pushroots are specified in the 'receive.cfg' configuration file.
pushroot
|
===============================================
| | | |
processed processing 12345(push1) 54321(push2)
<Arguments>
config_filepath:
The receive configuration file (i.e., 'receive.cfg').
<Exceptions>
tuf.FormatError, if any of the arguments are incorrectly formatted.
tuf.Error, if there was error processing the receive.
<Side Effects>
If a push is processed successfully, the repository specified in the
configuration file is updated with new target files and a 'targets.txt'
metadata file.
<Returns>
None.
"""
# Do the arguments have the correct format?
# Raise 'tuf.FormatError' if there is a mismatch.
tuf.formats.PATH_SCHEMA.check_match(config_filepath)
# Save a reference to the 'tuf.pushtools.pushtoolslib' module
# to avoid long lines of code. 'pushtoolslib' is needed here
# to read the 'receive.cfg' configuration file.
pushtoolslib = tuf.pushtools.pushtoolslib
# Is the path to the configuration file valid?
if not os.path.isfile(config_filepath):
message = 'The configuration file path is invalid.'
raise tuf.Error(message)
config_filepath = os.path.abspath(config_filepath)
# Retrieve the configuration settings required by 'receive'.
# Raise ('tuf.FormatError', 'tuf.Error') if a valid configuration file
# cannot be retrieved.
config_dict = pushtoolslib.read_config_file(config_filepath, 'receive')
# These are the locations where developers may push files using the
# push tools. Each push will be in its own directory with the
# developer's push root. Each developer's push must have the directories
# 'processed' and 'processing' writable by this script.
pushroots = config_dict['general']['pushroots']
# This is the directory where the repository resides. As far as this
# script is concerned, this is the live repository. Changes will be made
# to this repository directory.
repository_directory = config_dict['general']['repository_directory']
repository_directory = os.path.expanduser(repository_directory)
# This is the metadata directory within the repository.
# The successfully processed 'targets.txt' metadata file is saved here.
metadata_directory = config_dict['general']['metadata_directory']
metadata_directory = os.path.expanduser(metadata_directory)
# This is the targets directory within the repository.
# The successfully processed target files are saved here.
targets_directory = config_dict['general']['targets_directory']
targets_directory = os.path.expanduser(targets_directory)
# Where replaced files will be stored. This will be used globally rather
# than a separate backup/replaced files directory for each pushroot.
backup_directory = config_dict['general']['backup_directory']
backup_directory = os.path.expanduser(backup_directory)
# Check that the various defined directories exist.
# We don't check the 'pushroots' here because we consider it non-fatal
# if those are missing. A log message is issued if any of those are
# missing.
directories_to_check = {'repository': repository_directory,
'metadata': metadata_directory,
'targets': targets_directory,
'backup': backup_directory}
for directory_name, path in directories_to_check.items():
if not os.path.exists(path):
message = directory_name+' directory does not exist: '+str(path)
logger.error(message)
raise tuf.Error(message)
# Keep track of the number of pushes that were successfully processed,
# or that failed. These values are used to log/print detailed results
# after a pushroot is processed.
success_count = 0
failure_count = 0
# Process all the pushes for each of the pushroots.
for pushroot in pushroots:
if not os.path.exists(pushroot):
logger.error('The pushroot '+str(pushroot)+' does not exist. Skipping.')
continue
# Add the 'processed' and 'processing' directories if not present.
# These directories must exist so that we can properly process
# a push.
logger.debug('Looking for pushes in pushroot '+str(pushroot))
if not os.path.exists(os.path.join(pushroot, 'processed')):
os.mkdir(os.path.join(pushroot, 'processed'))
if not os.path.exists(os.path.join(pushroot, 'processing')):
os.mkdir(os.path.join(pushroot, 'processing'))
# Locate all the pushed directories and process them. 'pushname'
# should be a directory with a timestamp as its directory name.
# TODO: Use only the newest push and move the others to the 'processed'
# directory, adding an appropriate log file.
for pushname in os.listdir(pushroot):
# Skip over the 'processed' and 'processing' directories.
if pushname == 'processed' or pushname == 'processing':
continue
# Found a directory we can potentially process.
pushpath = os.path.join(pushroot, pushname)
if os.path.isdir(pushpath):
# Ensure the 'info' file exists. A successful push operation creates
# and saves this 'info' file to the push directory.
if not os.path.exists(os.path.join(pushpath, 'info')):
message = 'Skipping incomplete push '+str(pushpath)+' (no info file).'
logger.warn(message)
continue
# Process the new push and record if it was processed successfully.
# Raise 'tuf.Error' if a push processing error cannot be logged
# to a file properly.
success = _process_new_push(pushroot, pushname, metadata_directory,
targets_directory, backup_directory)
if success:
success_count += 1
else:
failure_count += 1
# Done. Log the result of processing the pushes for 'pushroot'.
message = 'Completed processing of all pushes. Push successes = '+\
str(success_count)+', failures = '+str(failure_count)+'.'
logger.info(message)
def _process_new_push(pushroot, pushname, metadata_directory,
targets_directory, backup_directory):
"""
<Purpose>
Process a push.
This will check the validity of targets metadata in the push (including
whether the signatures are trusted) and, if valid, will copy the targets
metadata and target files to the repository.
<Arguments>
pushroot:
The root directory containing the developer's pushes. This root is one
of multiple directories listed under the 'pushroots' entry in the
'receive.cfg' configuration file.
pushname:
The name of the directory (i.e., '1348449811.39') containing the pushed
files.
metadata_directory:
The directory where the repository's metadata files (e.g., 'targets.txt',
'root.txt') are stored.
targets_directory:
The directory where the repository's target files are stored.
backup_directory:
The directory where the pushed directories are saved after a
successful 'receive'.
<Exceptions>
tuf.Error, if a push processing error cannot be written to
'receive.result'.
<Side Effects>
Directories are created, the repository updated, and log files
added.
<Returns>
Boolean. True on success, False on failure.
"""
logger.info('Processing '+str(pushroot)+'/'+str(pushname))
# Move the pushed directory to the 'processing' directory.
pushpath = os.path.join(pushroot, 'processing', pushname)
logger.debug('Moving push directory to '+str(pushpath))
if os.path.isdir(pushpath) or os.path.isfile(pushpath):
os.remove(pushpath)
os.rename(os.path.join(pushroot, pushname), pushpath)
# Process 'pushpath' and log the appropriate results.
try:
try:
# Raise 'tuf.Error' if the copied push cannot be properly processed.
_process_copied_push(pushpath, metadata_directory,
targets_directory, backup_directory)
# Write the '{pushpath}/receive.result' file that indicates SUCCESS.
# The developer may later read this file to quickly determine if
# the push was successfully processed.
try:
file_object = open(os.path.join(pushpath, 'receive.result'), 'w')
except IOError, e:
raise tuf.Error('Unable to open "receive.result" file: '+str(e))
try:
file_object.write('SUCCESS')
file_object.write('\n')
finally:
file_object.close()
return True
except tuf.Error, e:
# Write the '{pushpath}/receive.result' file that indicates FAILURE.
try:
file_object = open(os.path.join(pushpath, 'receive.result'), 'w')
except IOError, e:
raise tuf.Error('Unable to open "receive.result" file: '+str(e))
try:
file_object.write("FAILURE")
file_object.write('\n')
finally:
file_object.close()
# Log the error message to {pushpath}/receive.log
# The developer may later search this log file for specific
# error messages on failed push attempts.
try:
file_object = open(os.path.join(pushpath, 'receive.log'), 'a')
except IOError, e:
raise tuf.Error('Unable to open receive log file: '+str(e))
try:
file_object.write(str(e))
file_object.write('\n')
finally:
file_object.close()
message = 'Could not process: '+str(pushroot)+'/'+str(pushname)
logger.exception(message)
return False
# On success or failure, move 'pushpath' to the processed directory.
finally:
processedpath = os.path.join(pushroot, 'processed', pushname)
logger.debug('Moving push directory to '+str(processedpath))
if os.path.isdir(processedpath) or os.path.isfile(processedpath):
os.remove(processedpath)
os.rename(pushpath, processedpath)
def _process_copied_push(pushpath, metadata_directory,
targets_directory, backup_directory):
"""
<Purpose>
Helper function for _process_new_push().
This does the actual work of copying pushpath to a temp directory,
checking the metadata and targets, and copying the files to the
repository on success. The push is valid and successfully processed
if no exception is raised.
<Arguments>
pushpath:
The push directory currently being processed (i.e., the 'processing'
directory on the developer's pushroot)
metadata_directory:
The directory where the repository's metadata files (e.g., 'targets.txt',
'root.txt') are stored.
targets_directory:
The directory where the repository's target files are stored.
backup_directory:
The directory where the pushed directories are saved after a
successful 'receive'.
<Exceptions>
tuf.Error, if there is an error processing the push.
<Side Effects>
The repository is updated if the push is successful.
<Returns>
None.
"""
# The push's timestamp directory name (e.g., '1348449811.39')
pushname = os.path.basename(pushpath)
# Copy the contents of pushpath to a temp directory. We don't want the
# user modifying the files we work with. The temp directory is only
# accessible by the calling process.
temporary_directory = tempfile.mkdtemp()
push_temporary_directory = os.path.join(temporary_directory, 'push')
shutil.copytree(pushpath, push_temporary_directory)
# Read the 'root' metadata of the current repository. 'root.txt'
# is needed to authorize the 'targets' metadata file.
root_metadatapath = os.path.join(metadata_directory, 'root.txt')
root_signable = tuf.util.load_json_file(root_metadatapath)
# Ensure 'root_signable' is properly formatted.
try:
tuf.formats.check_signable_object_format(root_signable)
except tuf.FormatError, e:
raise tuf.Error('The repository contains an invalid "root.txt".')
# Extract the metadata object and load the key and role databases.
# The keys and roles are needed to verify the signatures of the
# metadata files.
root_metadata = root_signable['signed']
tuf.keydb.create_keydb_from_root_metadata(root_metadata)
tuf.roledb.create_roledb_from_root_metadata(root_metadata)
# Determine the name of the targets metadata file that was pushed.
# The required 'info' file should list the metadata file that was
# pushed by the developer. Only 'targets.txt' currently supported
# (i.e., no delegated roles are accepted).
new_targets_metadata_file = None
try:
file_object = open(os.path.join(push_temporary_directory, 'info'), 'r')
except IOError, e:
raise tuf.Error('Unable to open push "info" file: '+str(e))
try:
# Inspect each line of the 'info' file, searching for the line that
# specifies the targets metadata file. Raise an exception if all
# the lines are processed without finding the 'metadata=' line.
for line in file_object:
# Search 'info' for a 'metadata=.../targets.txt' line.
parts = line.strip().split('=')
if parts[0] == 'metadata':
metadata_basename = os.path.basename(parts[1])
if metadata_basename != 'targets.txt':
message = 'No support yet for pushing delegated targets metadata.'
raise tuf.Error(message)
else:
new_targets_metadata_file = parts[1]
break
else:
raise tuf.Error('No "metadata=" line in push info file.')
finally:
file_object.close()
# Read the new targets metadata that was pushed.
new_targets_metadatapath = os.path.join(push_temporary_directory,
new_targets_metadata_file)
new_targets_signable = tuf.util.load_json_file(new_targets_metadatapath)
# Ensure 'new_targets_signable' is properly formatted.
try:
tuf.formats.check_signable_object_format(new_targets_signable)
except tuf.FormatError, e:
raise tuf.Error('The pushed targets metadata file is invalid.')
# Read the existing targets metadata from the repository.
targets_metadatapath = os.path.join(metadata_directory, 'targets.txt')
# Check the metadata. This is mostly to make sure we don't replace good
# metadata with bad metadata as clients do their own security checking.
# This is what we check:
# * it is newer than the last metadata.
# * it has not expired.
# * all signatures valid.
# * a threshold of trusted signatures. only check the delegating
# role rather than the trust hierachy all the way up.
# * all of the files listed in the metadata were provided and have
# the sizes and hashes listed in the metadata.
# Check that the new metadata file is newer than the existing metadata.
if os.path.exists(targets_metadatapath):
targets_signable = tuf.util.load_json_file(targets_metadatapath)
# Ensure 'targets_signable' is properly formatted.
try:
tuf.formats.check_signable_object_format(targets_signable)
except tuf.FormatError, e:
raise tuf.Error('The repository\'s targets metadata file is invalid.')
# Extract the timestamp value of the current targets metadata.
# This value is used to determine if the new metadata is newer.
timestamp = targets_signable['signed']['ts']
formatted_timestamp = tuf.formats.parse_time(timestamp)
# Extract the timestamp of the new targets metadata.
new_timestamp = new_targets_signable['signed']['ts']
new_formatted_timestamp = tuf.formats.parse_time(new_timestamp)
# Allowing equality makes testing/development easier.
if formatted_timestamp > new_formatted_timestamp:
message = 'Existing metadata timestamp '+str(timestamp)+' is newer '+\
'than the new metadata\'s timestamp '+str(new_timestamp)
raise tuf.Error(message)
else:
message = 'New metadata timestamp is '+str(new_timestamp)+'. '+\
' Replacing old metadata with timestamp '+str(timestamp)
logger.debug(message)
# There appears to be no 'targets.txt' metadata file on the repository.
else:
message = 'The old targets metadata file '+str(targets_metadatapath)+'. '+\
'doesn\'t exist in the repo. Skipping the timestamp check.'
logger.warn(message)
# Ensure the new metadata is not expired.
expiration = new_targets_signable['signed']['expires']
formatted_expiration = tuf.formats.parse_time(expiration)
if formatted_expiration <= time.time():
message = 'Pushed metadata expired at '+str(expiration)
raise tuf.Error(message)
else:
message = 'Metadata will expire at '+str(expiration)
logger.debug(message)
# Verify the signatures of the new targets metadata.
if not tuf.sig.verify(new_targets_signable, 'targets'):
message = 'The pushed targets metadata file does not '+\
'have the required number of good signatures.'
raise tuf.Error(message)
# Log the status of the signatures. For example, the number of good,
# bad, untrusted, unknown, signatures.
status = tuf.sig.get_signature_status(new_targets_signable, 'targets')
logger.debug(str(status))
# Log the number of targets specified in the new targets metadata file.
targets_count = len(new_targets_signable['signed']['targets'].keys())
message = 'Number of targets specified: '+str(targets_count)
logger.info(message)
# Verify the files of the new targets metadata file.
new_targets_dict = new_targets_signable['signed']['targets']
for target_relativepath, target_info in new_targets_dict.items():
targets_basename = os.path.basename(targets_directory)
targetpath = os.path.join(push_temporary_directory, targets_basename,
target_relativepath)
# Check that the target was provided.
if not os.path.exists(targetpath):
message = 'The specified target file was not provided: '+\
str(target_relativepath)
raise tuf.Error(message)
# Check the target's size. A valid size is required of target files.
target_size = os.path.getsize(targetpath)
if target_size != target_info['length']:
message = 'The size of target file '+str(target_relativepath)+\
' is incorrect: was '+str(target_size)+', expected '+\
str(target_info['length'])
raise tuf.Error(message)
else:
message = 'Size of target '+str(targetpath)+' is correct '+\
'('+str(target_size)+' bytes).'
logger.debug(message)
# Check hashes. Valid target files is required.
hash_count = len(target_info['hashes'].items())
if hash_count == 0:
message = str(targetpath)+' contains an empty hashes dictionary.'
raise tuf.Error(message)
else:
logger.debug(str(hash_count)+' hash(es) to check.')
for algorithm, digest in target_info['hashes'].items():
digest_object = tuf.hash.digest_filename(targetpath, algorithm=algorithm)
if digest_object.hexdigest() != digest:
message = str(algorithm)+' hash does not match: '+\
' was '+str(digest_object.hexdigest())+', expected '+\
str(digest)
raise tuf.Error(message)
else:
message = str(algorithm)+' hash of target '+str(targetpath)+\
' is correct ('+str(digest)+').'
logger.debug(message)
# At this point, the targets metadata and all specified files have been
# verified. Remove the files referenced by the old targets metadata as
# well as the old targets metadata itself.
# Raise 'tuf.Error' if there is an error backing up the old targets.
_remove_old_files(targets_metadatapath, pushname,
targets_directory, backup_directory)
# Copy the new target files into place on the repository.
for target_relativepath in new_targets_signable['signed']['targets'].keys():
targets_basename = os.path.basename(targets_directory)
source_path = os.path.join(push_temporary_directory, targets_basename,
target_relativepath)
destination_path = os.path.join(targets_directory, target_relativepath)
logger.info('Adding target to repository: '+str(destination_path))
destination_directory = os.path.dirname(destination_path)
if not os.path.exists(destination_directory):
os.mkdir(destination_directory)
shutil.copy(source_path, destination_path)
# Copy the new targets metadata file into place on the repository.
message = 'Adding new targets metadata to repository: '+str(targets_metadatapath)
logger.info(message)
shutil.copy(new_targets_metadatapath, targets_metadatapath)
def _remove_old_files(targets_metadatapath, pushname,
targets_directory, backup_directory):
"""
<Purpose>
Remove metadata and target files that will be replaced.
This does not take into account any targets that are the same between
the old and new metadata. For simplicity, all old targets are removed
and thus even targets that remained the same will need to be copied
into place after this has been called.
This function currently assumes that the the metadata file is the
top-level 'targets.txt' file rather than a delegated metadata file
and that the arguments have been validated (i.e., exist, correct, etc).
<Arguments>
targets_metadatapath:
The old targets metadata file to be replaced, along with all of
its referenced targets.
pushname:
The name of the directory (i.e., timestamp name) containing the pushed
files.
targets_directory:
The directory where the repository's target files are stored.
backup_directory:
The directory where the pushed directories are saved to after a
successful 'receive'.
<Exceptions>
tuf.Error, if there is an error backing up the old targets.
<Side Effects>
Replaces the old 'targets.txt' metadata file and removes all of the old
target files.
<Returns>
None.
"""
# Create the backup destination directories. The old target files
# and target metadata are backed up to these directories.
backup_destdirectory = os.path.join(backup_directory, pushname)
os.mkdir(backup_destdirectory)
targets_basename = os.path.basename(targets_directory)
backup_targetsdirectory = os.path.join(backup_destdirectory, targets_basename)
os.mkdir(backup_targetsdirectory)
# Load the old 'targets.txt' file and determine all the targets to be replaced.
# Need to ensure we only remove target files specified by 'targets.txt'.
targets_signable = tuf.util.load_json_file(targets_metadatapath)
for target_relativepath in targets_signable['signed']['targets'].keys():
targetpath = os.path.join(targets_directory, target_relativepath)
backup_targetpath = os.path.join(backup_targetsdirectory, target_relativepath)
message = 'Backing up target '+str(targetpath)+' to '+str(backup_targetpath)
logger.info(message)
# Move the old target file to the backup directory. Create any
# directories along the way.
if os.path.exists(targetpath):
try:
os.makedirs(os.path.dirname(backup_targetpath))
except OSError, e:
if e.errno == errno.EEXIST:
pass
else:
raise tuf.Error(str(e))
os.rename(targetpath, backup_targetpath)
else:
message = 'The old target '+str(targetpath)+' doesn\'t exist in the repo.'
logger.warn(message)
# Backup the old 'targets.txt' metadata file.
backup_targets_metadatafile = os.path.join(backup_destdirectory, 'targets.txt')
message = 'Backing up old metadata '+str(targets_metadatapath)+\
' to '+str(backup_targets_metadatafile)
logger.info(message)
if os.path.isfile(backup_targets_metadatafile):
os.remove(backup_targets_metadatafile)
os.rename(targets_metadatapath, backup_targets_metadatafile)
def parse_options():
"""
<Purpose>
Parse the command-line options. 'receive.py' expects the '--config'
option to be set by the user.
Example:
$ python receive.py --config ./receive.cfg
The '--config' option accepts a path argument to the receive configuration
file (i.e., 'receive.cfg'). If the required option is unset, a parser error
is printed and the script exits.
The '--verbose' option sets the verbosity level of the TUF logger. Accepts
values 1-5.
<Arguments>
None.
<Exceptions>
None.
<Side Effects>
Sets the logging level of the TUF logger.
<Returns>
The options object returned by the parser's parse_args() method.
"""
usage = 'usage: %prog --config <config path>'
option_parser = optparse.OptionParser(usage=usage)
# Add the options supported by 'receive' to the option parser.
option_parser.add_option('--config', action='store', type='string',
help='Specify the "receive.cfg" configuration file.')
option_parser.add_option('--verbose', dest='VERBOSE', type=int, default=2,
help='Set the verbosity level (1-5) of logging '
'messages. The lower the setting, the greater the '
'verbosity.')
(options, remaining_arguments) = option_parser.parse_args()
# Ensure the '--config' option is set. If the required option is unset,
# option_parser.error() will print an error message and exit.
if options.config is None:
message = '"--config" must be set on the command-line.'
option_parser.error(message)
# Set the logging level.
if options.VERBOSE == 5:
tuf.log.set_log_level(logging.CRITICAL)
elif options.VERBOSE == 4:
tuf.log.set_log_level(logging.ERROR)
elif options.VERBOSE == 3:
tuf.log.set_log_level(logging.WARNING)
elif options.VERBOSE == 2:
tuf.log.set_log_level(logging.INFO)
elif options.VERBOSE == 1:
tuf.log.set_log_level(logging.DEBUG)
else:
tuf.log.set_log_level(logging.NOTSET)
return options
if __name__ == '__main__':
options = parse_options()
# Perform a 'receive' of the pushroots specified in the configuration file.
try:
receive(options.config)
except (tuf.FormatError, tuf.Error), e:
sys.stderr.write('Error: '+str(e)+'\n')
sys.exit(1)
# The 'receive' and command-line options were processed successfully.
sys.exit(0)