source: mod_gnutls/test/runtest.py @ 7e10018

asyncioproxy-ticket
Last change on this file since 7e10018 was 7e10018, checked in by Fiona Klute <fiona.klute@…>, 2 years ago

runtest.py: Get Apache debug info before running prepare_env hook

This minimizes the risk of exceptions between the hook and the with
block containing the services, allowing for the hook to return a
cleanup callback later.

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