boulder/test/integration-test.py

321 lines
12 KiB
Python

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""
This file contains basic infrastructure for running the integration test cases.
Most test cases are in v1_integration.py and v2_integration.py. There are a few
exceptions: Test cases that don't test either the v1 or v2 API are in this file,
and test cases that have to run at a specific point in the cycle (e.g. after all
other test cases) are also in this file.
"""
import argparse
import atexit
import datetime
import inspect
import json
import os
import random
import re
import requests
import subprocess
import signal
import startservers
import chisel
from chisel import auth_and_issue
import v1_integration
import v2_integration
from helpers import *
from acme import challenges
import requests
import challtestsrv
challSrv = challtestsrv.ChallTestServer()
def setup_seventy_days_ago():
"""Do any setup that needs to happen 70 days in the past, for tests that
will run in the 'present'.
"""
# Issue a certificate with the clock set back, and save the authzs to check
# later that they are expired (404).
_, v1_integration.old_authzs = auth_and_issue([random_domain()])
def setup_twenty_days_ago():
"""Do any setup that needs to happen 20 day in the past, for tests that
will run in the 'present'.
"""
# Issue a certificate with the clock set back, and save the authzs to check
# later that they are valid (200). They should however require rechecking for
# CAA purposes.
_, v1_integration.caa_authzs = auth_and_issue(["recheck.good-caa-reserved.com"], client=v1_integration.caa_client)
def setup_zero_days_ago():
"""Do any setup that needs to happen at the start of a test run."""
# Issue a certificate and save the authzs to check that they still exist
# at a later point.
_, v1_integration.new_authzs = auth_and_issue([random_domain()])
def run_client_tests():
root = os.environ.get("CERTBOT_PATH")
assert root is not None, (
"Please set CERTBOT_PATH env variable to point at "
"initialized (virtualenv) client repo root")
cmd = os.path.join(root, 'tests', 'boulder-integration.sh')
run(cmd, cwd=root)
def run_expired_authz_purger():
# Note: This test must be run after all other tests that depend on
# authorizations added to the database during setup
# (e.g. test_expired_authzs_404).
def expect(target_time, num, table):
out = get_future_output("./bin/expired-authz-purger --config cmd/expired-authz-purger/config.json", target_time)
if 'via FAKECLOCK' not in out:
raise Exception("expired-authz-purger was not built with `integration` build tag")
if num is None:
return
expected_output = 'Deleted a total of %d expired authorizations from %s' % (num, table)
if expected_output not in out:
raise Exception("expired-authz-purger did not print '%s'. Output:\n%s" % (
expected_output, out))
now = datetime.datetime.utcnow()
# Run the purger once to clear out any backlog so we have a clean slate.
expect(now+datetime.timedelta(days=+365), None, "")
# Make an authz, but don't attempt its challenges.
chisel.make_client().request_domain_challenges("eap-test.com")
# Run the authz twice: Once immediate, expecting nothing to be purged, and
# once as if it were the future, expecting one purged authz.
after_grace_period = now + datetime.timedelta(days=+14, minutes=+3)
expect(now, 0, "pendingAuthorizations")
expect(after_grace_period, 1, "pendingAuthorizations")
auth_and_issue([random_domain()])
after_grace_period = now + datetime.timedelta(days=+67, minutes=+3)
expect(now, 0, "authz")
expect(after_grace_period, 1, "authz")
def test_single_ocsp():
"""Run the single-ocsp command, which is used to generate OCSP responses for
intermediate certificates on a manual basis. Then start up an
ocsp-responder configured to respond using the output of single-ocsp,
check that it successfully answers OCSP requests, and shut the responder
back down.
This is a non-API test.
"""
run("./bin/single-ocsp -issuer test/test-root.pem \
-responder test/test-root.pem \
-target test/test-ca2.pem \
-pkcs11 test/test-root.key-pkcs11.json \
-thisUpdate 2016-09-02T00:00:00Z \
-nextUpdate 2020-09-02T00:00:00Z \
-status 0 \
-out /tmp/issuer-ocsp-responses.txt")
p = subprocess.Popen(
'./bin/ocsp-responder --config test/issuer-ocsp-responder.json', shell=True)
# Verify that the static OCSP responder, which answers with a
# pre-signed, long-lived response for the CA cert, works.
wait_for_ocsp_good("test/test-ca2.pem", "test/test-root.pem", "http://localhost:4003")
p.send_signal(signal.SIGTERM)
p.wait()
def test_stats():
"""Fetch Prometheus metrics from a sample of Boulder components to check
they are present.
This is a non-API test.
"""
def expect_stat(port, stat):
url = "http://localhost:%d/metrics" % port
response = requests.get(url)
if not stat in response.content:
print(response.content)
raise Exception("%s not present in %s" % (stat, url))
expect_stat(8000, "\nresponse_time_count{")
expect_stat(8000, "\ngo_goroutines ")
expect_stat(8000, '\ngrpc_client_handling_seconds_count{grpc_method="NewRegistration",grpc_service="ra.RegistrationAuthority",grpc_type="unary"} ')
expect_stat(8002, '\ngrpc_server_handling_seconds_sum{grpc_method="PerformValidation",grpc_service="ra.RegistrationAuthority",grpc_type="unary"} ')
expect_stat(8001, "\ngo_goroutines ")
def setup_mock_dns(caa_account_uri=None):
"""
setup_mock_dns adds mock DNS entries to the running pebble-challtestsrv.
Integration tests depend on this mock data.
"""
if caa_account_uri is None:
caa_account_uri = os.environ.get("ACCOUNT_URI")
goodCAA = "happy-hacker-ca.invalid"
badCAA = "sad-hacker-ca.invalid"
caa_records = [
{"domain": "bad-caa-reserved.com", "value": badCAA},
{"domain": "good-caa-reserved.com", "value": goodCAA},
{"domain": "accounturi.good-caa-reserved.com", "value":"{0}; accounturi={1}".format(goodCAA, caa_account_uri)},
{"domain": "recheck.good-caa-reserved.com", "value":badCAA},
{"domain": "dns-01-only.good-caa-reserved.com", "value": "{0}; validationmethods=dns-01".format(goodCAA)},
{"domain": "http-01-only.good-caa-reserved.com", "value": "{0}; validationmethods=http-01".format(goodCAA)},
{"domain": "dns-01-or-http01.good-caa-reserved.com", "value": "{0}; validationmethods=dns-01,http-01".format(goodCAA)},
]
for policy in caa_records:
challSrv.add_caa_issue(policy["domain"], policy["value"])
exit_status = 1
def main():
parser = argparse.ArgumentParser(description='Run integration tests')
parser.add_argument('--all', dest="run_all", action="store_true",
help="run all of the clients' integration tests")
parser.add_argument('--certbot', dest='run_certbot', action='store_true',
help="run the certbot integration tests")
parser.add_argument('--chisel', dest="run_chisel", action="store_true",
help="run integration tests using chisel")
parser.add_argument('--load', dest="run_loadtest", action="store_true",
help="run load-generator")
parser.add_argument('--filter', dest="test_case_filter", action="store",
help="Regex filter for test cases")
parser.add_argument('--skip-setup', dest="skip_setup", action="store_true",
help="skip integration test setup")
# allow any ACME client to run custom command for integration
# testing (without having to implement its own busy-wait loop)
parser.add_argument('--custom', metavar="CMD", help="run custom command")
parser.set_defaults(run_all=False, run_certbot=False, run_chisel=False,
run_loadtest=False, test_case_filter="", skip_setup=False)
args = parser.parse_args()
if not (args.run_all or args.run_certbot or args.run_chisel or args.run_loadtest or args.custom is not None):
raise Exception("must run at least one of the letsencrypt or chisel tests with --all, --certbot, --chisel, --load or --custom")
caa_client = None
if not args.skip_setup:
now = datetime.datetime.utcnow()
seventy_days_ago = now+datetime.timedelta(days=-70)
if not startservers.start(race_detection=True, fakeclock=fakeclock(seventy_days_ago)):
raise Exception("startservers failed (mocking seventy days ago)")
setup_seventy_days_ago()
v1_integration.caa_client = caa_client = chisel.make_client()
startservers.stop()
now = datetime.datetime.utcnow()
twenty_days_ago = now+datetime.timedelta(days=-20)
if not startservers.start(race_detection=True, fakeclock=fakeclock(twenty_days_ago)):
raise Exception("startservers failed (mocking twenty days ago)")
setup_twenty_days_ago()
startservers.stop()
caa_account_uri = caa_client.account.uri if caa_client is not None else None
if not startservers.start(race_detection=True, account_uri=caa_account_uri):
raise Exception("startservers failed")
if not args.skip_setup:
setup_zero_days_ago()
setup_mock_dns(caa_account_uri)
if args.run_all or args.run_chisel:
run_chisel(args.test_case_filter)
if args.run_all or args.run_certbot:
run_client_tests()
if args.custom:
run(args.custom)
run_cert_checker()
# Skip load-balancing check when test case filter is on, since that usually
# means there's a single issuance and we don't expect every RPC backend to get
# traffic.
if not args.test_case_filter:
check_balance()
if not CONFIG_NEXT:
run_expired_authz_purger()
# Run the load-generator last. run_loadtest will stop the
# pebble-challtestsrv before running the load-generator and will not restart
# it.
if args.run_all or args.run_loadtest:
run_loadtest()
if not startservers.check():
raise Exception("startservers.check failed")
global exit_status
exit_status = 0
def run_chisel(test_case_filter):
for key, value in inspect.getmembers(v1_integration):
if callable(value) and key.startswith('test_') and re.search(test_case_filter, key):
value()
for key, value in inspect.getmembers(v2_integration):
if callable(value) and key.startswith('test_') and re.search(test_case_filter, key):
value()
for key, value in globals().items():
if callable(value) and key.startswith('test_') and re.search(test_case_filter, key):
value()
def run_loadtest():
"""Run the ACME v2 load generator."""
latency_data_file = "%s/integration-test-latency.json" % tempdir
# Stop the global pebble-challtestsrv - it will conflict with the
# load-generator's internal challtestsrv. We don't restart it because
# run_loadtest() is called last and there are no remaining tests to run that
# might benefit from the pebble-challtestsrv being restarted.
startservers.stopChallSrv()
run("./bin/load-generator \
-config test/load-generator/config/integration-test-config.json\
-results %s" % latency_data_file)
def check_balance():
"""Verify that gRPC load balancing across backends is working correctly.
Fetch metrics from each backend and ensure the grpc_server_handled_total
metric is present, which means that backend handled at least one request.
"""
addresses = [
"sa1.boulder:8003",
"sa2.boulder:8103",
"publisher1.boulder:8009",
"publisher2.boulder:8109",
"va1.boulder:8004",
"va2.boulder:8104",
"ca1.boulder:8001",
"ca2.boulder:8104",
"ra1.boulder:8002",
"ra2.boulder:8102",
]
for address in addresses:
metrics = requests.get("http://%s/metrics" % address)
if not "grpc_server_handled_total" in metrics.text:
raise Exception("no gRPC traffic processed by %s; load balancing problem?"
% address)
def run_cert_checker():
run("./bin/cert-checker -config %s/cert-checker.json" % default_config_dir)
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as e:
raise Exception("%s. Output:\n%s" % (e, e.output))
@atexit.register
def stop():
if exit_status == 0:
print("\n\nSUCCESS")
else:
print("\n\nFAILURE")