libp2p-test-plans/pubsub/scripts/run.py

257 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python
import toml
import jinja2
import json
import argparse
import os
import pathlib
import subprocess
import time
import analyze
import re
TESTGROUND_BIN = 'testground'
DEFAULT_GS_VERSION = 'latest'
# setting this build tag lets us compile test code that targets the new API from the hardening branch
HARDENED_API_BUILD_TAG = 'hardened_api'
# Testground build/run config settings to use when running on kubernetes
K8S_BUILD_CONFIG = {'bypass_cache': True, 'push_registry': True, 'registry_type': 'aws'}
K8S_RUN_CONFIG = {}
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('param_files', nargs='*',
help='name of one or more parameter files to use when generating composition from template. ' +
'if a param is defined in multiple files, last one wins.')
parser.add_argument('--name',
help='name of composition. will be used to create output directory.')
parser.add_argument('--template_dir',
default='./templates/baseline',
help='path to directory containing composition template and param files')
parser.add_argument('-o', '--output',
help='directory to write composition file and test outputs to',
default='./output')
parser.add_argument('--branch',
default='master',
help='configures the test to use the API from the given gossipsub branch')
parser.add_argument('--commit',
help='configures the test to use the API from the given gossipsub commit')
parser.add_argument('--k8s', action='store_true', default=False,
help='runs the test on kubernetes')
parser.add_argument('--dry-run', dest='dry_run', action='store_true', default=False,
help='skip running tests, just write composition files and exit')
parser.add_argument('--instances', type=int,
help='override the total number of test instances. equivalent to -D N_NODES=x')
parser.add_argument('-D', '--define', dest='definitions', action='append',
metavar='<key=value>',
help='set template variable `key` to `value`, e.g. -D T_RUN=10m')
parser.add_argument('--skip-analysis', dest='skip_analysis', action='store_true', default=False,
help='skip analysis phase after test run (can run manually later with analyze.py)')
return parser.parse_args()
def get_param_filepath(template_dir, param_filename):
p = param_filename
if os.path.exists(p):
return p
p = os.path.join(template_dir, 'params', p)
if os.path.exists(p):
return p
p = param_filename + '.toml'
if os.path.exists(p):
return p
p = os.path.join(template_dir, 'params', p)
if os.path.exists(p):
return p
raise ValueError("can't find param file " + param_filename)
def load_params(template_dir, param_files):
base_params_path = os.path.join(template_dir, 'params', '_base.toml')
paths = [get_param_filepath(template_dir, p) for p in param_files]
params = dict()
for path in [base_params_path] + paths:
p = toml.load(path)
for k, v in p.items():
params[k] = v
return params
# The TOPOLOGY parameter can be either
# - a JSON representation of a topology
# - a path to a file in that format
def parse_topology(params):
if 'TOPOLOGY' not in params:
params['TOPOLOGY'] = None
return
# If it's already JSON, we're all set
top = params['TOPOLOGY']
if len(top) > 0 and top[0] == '{':
return
# It's not JSON so assume it's a file path
if not os.path.exists(top):
raise ValueError("can't find topology file " + top)
# Make sure the file contents is JSON
with open(top, 'r') as infile:
contents = infile.read()
jsonstr = json.loads(contents)
params['TOPOLOGY'] = jsonstr
# N_CONTAINER_NODES_TOTAL is the total number of nodes including multiple nodes
# per container
def parse_n_container_nodes_total(params):
n_nodes = params.get('N_NODES', 20)
n_nodes_cont_honest = params.get('N_HONEST_PEERS_PER_NODE', 1)
total = n_nodes * n_nodes_cont_honest
params['N_CONTAINER_NODES_TOTAL'] = total
def render_template(template_dir, params):
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
template = env.get_template('template.toml.j2')
return template.render(**params)
def composition_name():
ts = time.strftime("%Y%m%d-%H%M%S")
return 'pubsub-test-{}'.format(ts)
def mkdirp(dirpath):
pathlib.Path(dirpath).mkdir(parents=True, exist_ok=True)
def run_composition(comp_filepath, output_dir, k8s=False):
archive_type = 'tgz' if k8s else 'zip'
outfilename = 'test-output.{}'.format(archive_type)
outpath = os.path.join(output_dir, outfilename)
print('running testground composition {}'.format(comp_filepath))
print("writing test outputs to {}".format(output_dir))
cmd = [TESTGROUND_BIN, 'run', 'composition', '-f', comp_filepath, '--collect', '-o', outpath]
subprocess.run(cmd, check=True)
print('test completed successfully!')
return outpath
def pubsub_commit(ref_str):
# if the input looks like a git commit already, just return it as-is
if re.match(r'\b([a-f0-9]{40})\b', ref_str):
return ref_str
out = subprocess.run(['git', 'ls-remote', 'git://github.com/libp2p/go-libp2p-pubsub'],
check=True, capture_output=True, text=True)
# look for matching branch or tag in output
pattern = r'^\b([a-f0-9]{40})\b.*refs/(heads|tags)/' + ref_str + '$'
m = re.search(pattern, out.stdout, re.MULTILINE)
if not m:
raise ValueError('no branch or tag found matching {}'.format(ref_str))
return m.group(1)
def run():
args = parse_args()
template_dir = args.template_dir
params = load_params(template_dir, args.param_files)
branch = None
if args.branch:
branch = args.branch
params['GS_VERSION'] = pubsub_commit(args.branch)
if args.commit:
params['GS_VERSION'] = args.commit
if branch is None and args.commit is None:
params['GS_VERSION'] = 'latest'
gs_version_msg = 'Using go-libp2p-pubsub commit ' + params['GS_VERSION']
if branch:
gs_version_msg += ' (' + branch + ')'
print(gs_version_msg)
if args.k8s:
params['TEST_RUNNER'] = 'cluster:k8s'
params['BUILD_CONFIG'] = toml.dumps(K8S_BUILD_CONFIG)
params['RUN_CONFIG'] = toml.dumps(K8S_RUN_CONFIG)
if args.instances is not None:
params['N_NODES'] = args.instances
if args.definitions is not None:
for defstr in args.definitions:
[k, v] = defstr.split('=')
try:
v = float(v)
if v.is_integer():
v = int(v)
params[k] = v
except:
params[k] = v
parse_topology(params)
parse_n_container_nodes_total(params)
comp = composition_name()
if args.name:
comp += '-' + args.name
workdir = os.path.join(args.output, comp)
pathlib.Path(workdir).mkdir(parents=True, exist_ok=True)
comp_filepath = os.path.join(workdir, 'composition.toml')
with open(comp_filepath, 'w', encoding='utf8') as f:
f.write(render_template(template_dir, params))
param_filepath = os.path.join(workdir, 'template-params.toml')
with open(param_filepath, 'wt') as f:
toml.dump(params, f)
print('wrote composition file to {}'.format(comp_filepath))
if args.dry_run:
print('dry run. skipping test execution')
return
test_output_archive = run_composition(comp_filepath, workdir, args.k8s)
if not args.skip_analysis:
print('extracting test output data')
analyze.extract_test_outputs(test_output_archive)
if __name__ == "__main__":
run()