source: mod_gnutls/test/runtest.py @ d2f2f62

proxy-ticket
Last change on this file since d2f2f62 was b0695c6, checked in by Fiona Klute <fiona.klute@…>, 7 months ago

Pass parsed YAML config to mgstest.tests.run_test_conf

Parsing the config in runtest.py has the advantage that the parsed
config is available there, too, e.g. to be read by hooks.

  • Property mode set to 100755
File size: 9.5 KB
Line 
1#!/usr/bin/python3
2# PYTHON_ARGCOMPLETE_OK
3
4# Copyright 2019-2020 Fiona Klute
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import contextlib
19import os
20import os.path
21import subprocess
22import sys
23import tempfile
24import yaml
25from unittest import SkipTest
26
27import mgstest.hooks
28import mgstest.valgrind
29from mgstest import lockfile, TestExpectationFailed
30from mgstest.services import ApacheService, TestService
31from mgstest.tests import run_test_conf
32
33
34
35def find_testdir(number, dir):
36    """Find the configuration directory for a test based on its
37    number. The given directory must contain exactly one directory
38    with a name matching "NUMBER_*", otherwise a LookupError is
39    raised.
40
41    """
42    with os.scandir(dir) as it:
43        found = None
44        for entry in it:
45            if entry.is_dir():
46                num = int(entry.name.split('_', maxsplit=1)[0])
47                if number == num:
48                    if found:
49                        # duplicate numbers are an error
50                        raise LookupError('Multiple directories found for '
51                                          f'test number {number}: '
52                                          f'{found.name} and {entry.name}')
53                    else:
54                        found = entry
55        if found == None:
56            raise LookupError('No test directory found for test number '
57                              f'{number}!')
58        else:
59            return (found.path, found.name)
60
61def temp_logfile():
62    return tempfile.SpooledTemporaryFile(max_size=4096, mode='w+',
63                                         prefix='mod_gnutls', suffix=".log")
64
65
66
67
68def check_ocsp_responder():
69    # Check if OCSP responder works
70    issuer_cert = 'authority/x509.pem'
71    check_cert = 'authority/server/x509.pem'
72    command = ['ocsptool', '--ask', '--nonce',
73               '--load-issuer', issuer_cert,
74               '--load-cert', check_cert]
75    return subprocess.run(command).returncode == 0
76
77def check_msva():
78    # Check if MSVA is up
79    cert_file = 'authority/client/x509.pem'
80    uid_file = 'authority/client/uid'
81    with open(uid_file, 'r') as file:
82        uid = file.read().strip()
83        command = ['msva-query-agent', 'https', uid, 'x509pem', 'client']
84        with open(cert_file, 'r') as cert:
85            return subprocess.run(command, stdin=cert).returncode == 0
86
87
88
89def main(args):
90    # The Automake environment always provides srcdir, the default is
91    # for manual use.
92    srcdir = os.path.realpath(os.environ.get('srcdir', '.'))
93    # ensure environment srcdir is absolute
94    os.environ['srcdir'] = srcdir
95
96    # Find the configuration directory for the test in
97    # ${srcdir}/tests/, based on the test number.
98    testdir, testname = find_testdir(args.test_number,
99                                     os.path.join(srcdir, 'tests'))
100    print(f'Found test {testname}, test dir is {testdir}')
101    os.environ['TEST_NAME'] = testname
102
103    # Load test config
104    try:
105        with open(os.path.join(testdir, 'test.yml'), 'r') as conf_file:
106            test_conf = yaml.load(conf_file, Loader=yaml.Loader)
107    except FileNotFoundError:
108        test_conf = None
109
110    # Load test case hooks (if any)
111    plugin_path = os.path.join(testdir, 'hooks.py')
112    plugin = mgstest.hooks.load_hooks_plugin(plugin_path)
113
114    # PID file name varies depending on whether we're using
115    # namespaces.
116    #
117    # TODO: Check if having the different names is really necessary.
118    pidaffix = ''
119    if 'USE_TEST_NAMESPACE' in os.environ:
120        pidaffix = f'-{testname}'
121
122    valgrind_log = None
123    if args.valgrind:
124        valgrind_log = os.path.join('logs', f'valgrind-{testname}.log')
125
126    # Define the available services
127    apache = ApacheService(config=os.path.join(testdir, 'apache.conf'),
128                           pidfile=f'apache2{pidaffix}.pid',
129                           valgrind_log=valgrind_log,
130                           valgrind_suppress=args.valgrind_suppressions)
131    backend = ApacheService(config=os.path.join(testdir, 'backend.conf'),
132                            pidfile=f'backend{pidaffix}.pid')
133    ocsp = ApacheService(config=os.path.join(testdir, 'ocsp.conf'),
134                         pidfile=f'ocsp{pidaffix}.pid',
135                         check=check_ocsp_responder)
136    msva = TestService(start=['monkeysphere-validation-agent'],
137                       env={'GNUPGHOME': 'msva.gnupghome',
138                            'MSVA_KEYSERVER_POLICY': 'never'},
139                       condition=lambda: 'USE_MSVA' in os.environ,
140                       check=check_msva)
141
142    # background services: must be ready before the main apache
143    # instance is started
144    bg_services = [backend, ocsp, msva]
145
146    # If VERBOSE is enabled, log the HTTPD build configuration
147    if 'VERBOSE' in os.environ:
148        apache2 = os.environ.get('APACHE2', 'apache2')
149        subprocess.run([apache2, '-f', f'{srcdir}/base_apache.conf', '-V'],
150                       check=True)
151
152    # This hook may modify the environment as needed for the test.
153    cleanup_callback = None
154    try:
155        if plugin.prepare_env:
156            cleanup_callback = plugin.prepare_env()
157    except SkipTest as skip:
158        print(f'Skipping: {skip!s}')
159        sys.exit(77)
160
161    if 'USE_MSVA' in os.environ:
162        os.environ['MONKEYSPHERE_VALIDATION_AGENT_SOCKET'] = \
163            f'http://127.0.0.1:{os.environ["MSVA_PORT"]}'
164
165    with contextlib.ExitStack() as service_stack:
166        if cleanup_callback:
167            service_stack.callback(cleanup_callback)
168        service_stack.enter_context(lockfile('test.lock', nolock='MGS_NETNS_ACTIVE' in os.environ))
169        service_stack.enter_context(ocsp.run())
170        service_stack.enter_context(backend.run())
171        service_stack.enter_context(msva.run())
172
173        # TEST_SERVICE_MAX_WAIT is in milliseconds
174        wait_timeout = \
175            int(os.environ.get('TEST_SERVICE_MAX_WAIT', 10000)) / 1000
176        for s in bg_services:
177            if s.condition():
178                s.wait_ready(timeout=wait_timeout)
179
180        # special case: expected to fail in a few cases
181        service_stack.enter_context(apache.run())
182        failed = apache.wait_ready()
183        if os.path.exists(os.path.join(testdir, 'fail.server')):
184            if failed:
185                print('Apache server failed to start as expected',
186                      file=sys.stderr)
187            else:
188                raise TestExpectationFailed(
189                    'Server start did not fail as expected!')
190
191        # Set TEST_TARGET for the request. Might be replaced with a
192        # parameter later.
193        if 'TARGET_IP' in os.environ:
194            os.environ['TEST_TARGET'] = os.environ['TARGET_IP']
195        else:
196            os.environ['TEST_TARGET'] = os.environ['TEST_HOST']
197
198        # Run the test connections
199        if plugin.run_connection:
200            plugin.run_connection(testname,
201                                  conn_log=args.log_connection,
202                                  response_log=args.log_responses)
203        else:
204            run_test_conf(test_conf,
205                          float(os.environ.get('TEST_QUERY_TIMEOUT', 5.0)),
206                          conn_log=args.log_connection,
207                          response_log=args.log_responses)
208
209    # run extra checks the test's hooks.py might define
210    if plugin.post_check:
211        args.log_connection.seek(0)
212        args.log_responses.seek(0)
213        plugin.post_check(conn_log=args.log_connection,
214                          response_log=args.log_responses)
215
216    if valgrind_log:
217        with open(valgrind_log) as log:
218            errors = mgstest.valgrind.error_summary(log)
219            print(f'Valgrind summary: {errors[0]} errors, '
220                  f'{errors[1]} suppressed')
221            if errors[0] > 0:
222                sys.exit(ord('V'))
223
224
225
226if __name__ == "__main__":
227    import argparse
228    parser = argparse.ArgumentParser(
229        description='Run a mod_gnutls server test')
230    parser.add_argument('--test-number', type=int,
231                        required=True, help='load YAML test configuration')
232    parser.add_argument('--log-connection', type=argparse.FileType('w+'),
233                        default=temp_logfile(),
234                        help='write connection log to this file')
235    parser.add_argument('--log-responses', type=argparse.FileType('w+'),
236                        default=temp_logfile(),
237                        help='write HTTP responses to this file')
238    parser.add_argument('--valgrind', action='store_true',
239                        help='run primary Apache instance with Valgrind')
240    parser.add_argument('--valgrind-suppressions', action='append',
241                        default=[],
242                        help='use Valgrind suppressions file')
243
244    # enable bash completion if argcomplete is available
245    try:
246        import argcomplete
247        argcomplete.autocomplete(parser)
248    except ImportError:
249        pass
250
251    args = parser.parse_args()
252
253    with contextlib.ExitStack() as stack:
254        stack.enter_context(contextlib.closing(args.log_connection))
255        stack.enter_context(contextlib.closing(args.log_responses))
256        main(args)
Note: See TracBrowser for help on using the repository browser.