156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
import rclone
|
||
|
import os
|
||
|
import sys
|
||
|
import argparse
|
||
|
|
||
|
|
||
|
DEFAULT_S3_BUCKET = 'gossipsub-test-outputs'
|
||
|
DEFAULT_REGION = 'eu-central-1'
|
||
|
RCLONE_CONFIG_TEMPLATE = """
|
||
|
[s3]
|
||
|
type = s3
|
||
|
provider = AWS
|
||
|
env_auth = true
|
||
|
region = {region}
|
||
|
location_constraint = "{region}"
|
||
|
acl = public-read
|
||
|
"""
|
||
|
|
||
|
|
||
|
def rclone_config(region):
|
||
|
return RCLONE_CONFIG_TEMPLATE.format(region=region)
|
||
|
|
||
|
|
||
|
class OutputSyncer(object):
|
||
|
def __init__(self, region=DEFAULT_REGION, bucket=DEFAULT_S3_BUCKET):
|
||
|
self.config = rclone_config(region)
|
||
|
self.bucket = bucket
|
||
|
self._ensure_rclone_exists()
|
||
|
|
||
|
def _ensure_rclone_exists(self):
|
||
|
result = rclone.with_config(self.config).listremotes()
|
||
|
if result['code'] == -20:
|
||
|
raise EnvironmentError("the 'rclone' command must be present on the $PATH")
|
||
|
|
||
|
def list_outputs(self):
|
||
|
path = 's3:/{}/'.format(self.bucket)
|
||
|
result = rclone.with_config(self.config).run_cmd('lsd', [path])
|
||
|
if result['code'] != 0:
|
||
|
raise ValueError('failed to list output bucket: {}'.format(result))
|
||
|
out = result['out'].decode('utf8')
|
||
|
dirs = []
|
||
|
for line in out.splitlines():
|
||
|
name = line.split()[-1]
|
||
|
dirs.append(name)
|
||
|
return dirs
|
||
|
|
||
|
def fetch(self, name, dest_dir):
|
||
|
src = 's3:/{}/{}'.format(self.bucket, name)
|
||
|
dest = os.path.join(dest_dir, name)
|
||
|
result = rclone.with_config(self.config).sync(src, dest)
|
||
|
if result['code'] != 0:
|
||
|
print('error fetching {}: {}'.format(name, result['error']), file=sys.stderr)
|
||
|
|
||
|
def fetch_all(self, dest_dir):
|
||
|
src = 's3:/{}/'.format(self.bucket)
|
||
|
result = rclone.with_config(self.config).sync(src, dest_dir)
|
||
|
if result['code'] != 0:
|
||
|
print('error fetching all test outputs: {}'.format(result['error']), file=sys.stderr)
|
||
|
|
||
|
def store_single(self, test_run_dir):
|
||
|
"""
|
||
|
:param test_run_dir: path to local dir containing a single test run output, e.g. ./output/pubsub-test-20200409-152658
|
||
|
"""
|
||
|
name = os.path.basename(test_run_dir)
|
||
|
dest = 's3:/{}/{}'.format(self.bucket, name)
|
||
|
result = rclone.with_config(self.config).sync(test_run_dir, dest)
|
||
|
if result['code'] != 0:
|
||
|
print('error storing {}: {}'.format(name, result['error']), file=sys.stderr)
|
||
|
|
||
|
def store_all(self, src_dir, ignore=[]):
|
||
|
"""
|
||
|
:param src_dir: path to local dir containing multiple test run dirs, e.g. ./output
|
||
|
:param ignore: list of subdirectories to ignore
|
||
|
"""
|
||
|
for f in os.listdir(src_dir):
|
||
|
if f in ignore:
|
||
|
continue
|
||
|
src = os.path.join(src_dir, f)
|
||
|
dest = 's3:/{}/{}'.format(self.bucket, f)
|
||
|
|
||
|
print('syncing {} to {}'.format(src, dest))
|
||
|
result = rclone.with_config(self.config).sync(src, dest)
|
||
|
if result['code'] != 0:
|
||
|
print('error storing {}: {}'.format(f, result['error']), file=sys.stderr)
|
||
|
|
||
|
|
||
|
def parse_args():
|
||
|
parser = argparse.ArgumentParser(description="sync test outputs to/from an s3 bucket")
|
||
|
parser.add_argument('--region', default=DEFAULT_REGION, help='AWS region containing test output bucket')
|
||
|
parser.add_argument('--bucket', default=DEFAULT_S3_BUCKET, help='name of s3 bucket to store and fetch test outputs')
|
||
|
|
||
|
commands = parser.add_subparsers()
|
||
|
ls_cmd = commands.add_parser('list', aliases=['ls'], help='list test outputs in the s3 bucket')
|
||
|
ls_cmd.set_defaults(subcommand='list')
|
||
|
|
||
|
fetch_cmd = commands.add_parser('fetch', help='fetch one or more named test outputs from the s3 bucket')
|
||
|
fetch_cmd.set_defaults(subcommand='fetch')
|
||
|
fetch_cmd.add_argument('names', nargs='+', help='name of a test output directory to fetch')
|
||
|
fetch_cmd.add_argument('--dest', default='./output', help='directory to store fetched test output')
|
||
|
|
||
|
fetch_all_cmd = commands.add_parser('fetch-all', help='fetch all test outputs from the s3 bucket to a local dir')
|
||
|
fetch_all_cmd.set_defaults(subcommand='fetch-all')
|
||
|
fetch_all_cmd.add_argument('dest', help='directory to store fetched test output')
|
||
|
|
||
|
store_cmd = commands.add_parser('store', help='store one or more test outputs in s3')
|
||
|
store_cmd.set_defaults(subcommand='store')
|
||
|
store_cmd.add_argument('paths', nargs='+', help='path to a test output directory to store')
|
||
|
|
||
|
store_all_cmd = commands.add_parser('store-all', help='send all test outputs in a directory to s3')
|
||
|
store_all_cmd.set_defaults(subcommand='store-all')
|
||
|
store_all_cmd.set_defaults(ignore=['failed'])
|
||
|
store_all_cmd.add_argument('dir', help='local dir containing test output directories')
|
||
|
store_all_cmd.add_argument('--ignore', help='subdirectory to ignore (e.g. failed outputs)',
|
||
|
action='append')
|
||
|
|
||
|
return parser.parse_args()
|
||
|
|
||
|
|
||
|
def run():
|
||
|
args = parse_args()
|
||
|
|
||
|
syncer = OutputSyncer(region=args.region, bucket=args.bucket)
|
||
|
if args.subcommand == 'list':
|
||
|
outputs = syncer.list_outputs()
|
||
|
print('\n'.join(outputs))
|
||
|
return
|
||
|
|
||
|
if args.subcommand == 'fetch':
|
||
|
dest_dir = args.dest
|
||
|
for name in args.names:
|
||
|
print('fetching {} from s3://{} to {}'.format(name, args.bucket, dest_dir))
|
||
|
syncer.fetch(name, dest_dir)
|
||
|
return
|
||
|
|
||
|
if args.subcommand == 'fetch-all':
|
||
|
dest_dir = args.dest
|
||
|
print('fetching all test outputs from s3://{}'.format(args.bucket))
|
||
|
syncer.fetch_all(dest_dir)
|
||
|
return
|
||
|
|
||
|
if args.subcommand == 'store':
|
||
|
for p in args.paths:
|
||
|
print('syncing {} to s3://{}'.format(p, args.bucket))
|
||
|
syncer.store_single(p)
|
||
|
return
|
||
|
|
||
|
if args.subcommand == 'store-all':
|
||
|
print('syncing all subdirs of {} to s3://{} - excluding {}'.format(args.dir, args.bucket, args.ignore))
|
||
|
syncer.store_all(args.dir, ignore=args.ignore)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
run()
|