deckard.py 12 KB
Newer Older
1
#!/usr/bin/env python
Ivana Krumlova's avatar
Ivana Krumlova committed
2 3
from __future__ import print_function

Marek Vavruša's avatar
Marek Vavruša committed
4 5 6 7 8 9 10 11 12 13 14 15
import sys
import os
import fileinput
import subprocess
import tempfile
import shutil
import socket
import time
import signal
import stat
import errno
import jinja2
16
import dns.rdatatype
Marek Vavruša's avatar
Marek Vavruša committed
17 18
from pydnstest import scenario, testserver, test
from datetime import datetime
Grigorii Demidov's avatar
Grigorii Demidov committed
19 20
import random
import string
21
import itertools
Grigorii Demidov's avatar
Grigorii Demidov committed
22
import calendar
Marek Vavruša's avatar
Marek Vavruša committed
23 24

def str2bool(v):
Petr Špaček's avatar
Petr Špaček committed
25
    """ Return conversion of JSON-ish string value to boolean. """
Marek Vavruša's avatar
Marek Vavruša committed
26 27
    return v.lower() in ('yes', 'true', 'on')

28 29

def del_files(path_to, delpath):
Marek Vavruša's avatar
Marek Vavruša committed
30 31 32
    for root, dirs, files in os.walk(path_to):
        for f in files:
            os.unlink(os.path.join(root, f))
33
    if delpath == True:
34 35 36 37
        try:
            os.rmdir(path_to);
        except:
            pass
Marek Vavruša's avatar
Marek Vavruša committed
38

Marek Vavrusa's avatar
Marek Vavrusa committed
39
VERBOSE = 0
Marek Vavruša's avatar
Marek Vavruša committed
40 41 42
DEFAULT_IFACE = 0
CHILD_IFACE = 0
TMPDIR = ""
43
OWN_TMPDIR = False
44
INSTALLDIR = os.path.dirname(os.path.abspath(__file__))
45 46
DEFAULT_FEATURE_LIST_DELIM = ';'
DEFAULT_FEATURE_PAIR_DELIM = '='
Marek Vavruša's avatar
Marek Vavruša committed
47 48

if "SOCKET_WRAPPER_DEFAULT_IFACE" in os.environ:
Petr Špaček's avatar
Petr Špaček committed
49
    DEFAULT_IFACE = int(os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"])
Marek Vavruša's avatar
Marek Vavruša committed
50
if DEFAULT_IFACE < 2 or DEFAULT_IFACE > 254 :
51
    DEFAULT_IFACE = 2
Marek Vavruša's avatar
Marek Vavruša committed
52 53 54 55 56 57
    os.environ["SOCKET_WRAPPER_DEFAULT_IFACE"]="{}".format(DEFAULT_IFACE)

if "KRESD_WRAPPER_DEFAULT_IFACE" in os.environ:
    CHILD_IFACE = int(os.environ["KRESD_WRAPPER_DEFAULT_IFACE"])
if CHILD_IFACE < 2 or CHILD_IFACE > 254 or CHILD_IFACE == DEFAULT_IFACE:
    OLD_CHILD_IFACE = CHILD_IFACE
58 59
    CHILD_IFACE = 254
    if CHILD_IFACE == DEFAULT_IFACE:
Pieter Lexis's avatar
Pieter Lexis committed
60
        CHILD_IFACE = 253
Marek Vavruša's avatar
Marek Vavruša committed
61 62
    os.environ["KRESD_WRAPPER_DEFAULT_IFACE"] = "{}".format(CHILD_IFACE)

63

Marek Vavruša's avatar
Marek Vavruša committed
64 65 66 67 68
if "SOCKET_WRAPPER_DIR" in os.environ:
    TMPDIR = os.environ["SOCKET_WRAPPER_DIR"]
if TMPDIR == "" or os.path.isdir(TMPDIR) is False:
    OLDTMPDIR = TMPDIR
    TMPDIR = tempfile.mkdtemp(suffix='', prefix='tmp')
69
    OWN_TMPDIR = True
Marek Vavruša's avatar
Marek Vavruša committed
70 71
    os.environ["SOCKET_WRAPPER_DIR"] = TMPDIR

Marek Vavrusa's avatar
Marek Vavrusa committed
72 73 74 75 76
if "VERBOSE" in os.environ:
    try:
        VERBOSE = int(os.environ["VERBOSE"])
    except: pass

Marek Vavruša's avatar
Marek Vavruša committed
77 78 79 80 81 82 83 84 85 86 87 88 89
def find_objects(path):
    """ Recursively scan file/directory for scenarios. """
    result = []
    if os.path.isdir(path):
        for e in os.listdir(path):
            result += find_objects(os.path.join(path, e))
    elif os.path.isfile(path):
        if path.endswith('.rpl'):
            result.append(path)
    return result

def write_timestamp_file(path, tst):
    time_file = open(path, 'w')
90 91
    time_file.write(datetime.fromtimestamp(tst).strftime('@%Y-%m-%d %H:%M:%S'))
    time_file.flush()
Marek Vavruša's avatar
Marek Vavruša committed
92 93
    time_file.close()

94
def setup_env(scenario, child_env, config, config_name_list, j2template_list):
Marek Vavruša's avatar
Marek Vavruša committed
95 96
    """ Set up test environment and config """
    # Clear test directory
97
    del_files(TMPDIR, False)
Marek Vavruša's avatar
Marek Vavruša committed
98 99 100 101 102
    # Set up libfaketime
    os.environ["FAKETIME_NO_CACHE"] = "1"
    os.environ["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR
    child_env["FAKETIME_NO_CACHE"] = "1"
    child_env["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR
103
    write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], int (time.time()))
Petr Špaček's avatar
Petr Špaček committed
104
    # Set up child process env()
Marek Vavruša's avatar
Marek Vavruša committed
105 106
    child_env["SOCKET_WRAPPER_DEFAULT_IFACE"] = "%i" % CHILD_IFACE
    child_env["SOCKET_WRAPPER_DIR"] = TMPDIR
107 108 109
    # do not pass SOCKET_WRAPPER_PCAP_FILE into child to avoid duplicate packets in pcap
    if "SOCKET_WRAPPER_PCAP_FILE" in child_env:
        del child_env["SOCKET_WRAPPER_PCAP_FILE"]
110
    no_minimize = os.environ.get("NO_MINIMIZE", "true")
111
    trust_anchor_list = []
Marek Vavruša's avatar
Marek Vavruša committed
112
    stub_addr = ""
113 114 115
    features = {}
    feature_list_delimiter = DEFAULT_FEATURE_LIST_DELIM
    feature_pair_delimiter = DEFAULT_FEATURE_PAIR_DELIM
116
    selfaddr = testserver.get_local_addr_str(socket.AF_INET, DEFAULT_IFACE)
Marek Vavruša's avatar
Marek Vavruša committed
117 118 119 120 121
    for k,v in config:
        # Enable selectively for some tests
        if k == 'query-minimization' and str2bool(v):
            no_minimize = "false"
        elif k == 'trust-anchor':
122
            trust_anchor_list.append(v.strip('"\''))
Grigorii Demidov's avatar
Grigorii Demidov committed
123 124 125
        elif k == 'val-override-timestamp':
            override_timestamp_str = v.strip('"\'')
            write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], int(override_timestamp_str))
Marek Vavruša's avatar
Marek Vavruša committed
126 127
        elif k == 'val-override-date':
            override_date_str = v.strip('"\'')
Grigorii Demidov's avatar
Grigorii Demidov committed
128 129 130 131 132 133 134 135 136 137
            ovr_yr = override_date_str[0:4]
            ovr_mnt = override_date_str[4:6]
            ovr_day = override_date_str[6:8]
            ovr_hr = override_date_str[8:10]
            ovr_min = override_date_str[10:12]
            ovr_sec = override_date_str[12:]
            override_date_str_arg = '{0} {1} {2} {3} {4} {5}'.format(ovr_yr,ovr_mnt,ovr_day,ovr_hr,ovr_min,ovr_sec)
            override_date = time.strptime(override_date_str_arg,"%Y %m %d %H %M %S")
            override_date_timestamp = calendar.timegm(override_date)
            write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], override_date_timestamp)
Marek Vavruša's avatar
Marek Vavruša committed
138 139
        elif k == 'stub-addr':
            stub_addr = v.strip('"\'')
140
        elif k == 'features':
141
            feature_list = v.split(feature_list_delimiter)
142
            try :
143
                for f_item in feature_list:
144 145 146 147 148 149 150
                    if f_item.find(feature_pair_delimiter) != -1:
                        f_key, f_value = [x.strip() for x in f_item.split(feature_pair_delimiter,1)]
                    else:
                        f_key = f_item.strip()
                        f_value = ""
                    features[f_key] = f_value
            except Exception as e:
151 152 153 154 155 156
                raise Exception ("can't parse features (%s) in config section (%s)" % (v,str(e)));
        elif k == 'feature-list':
            try :
                f_key, f_value = [x.strip() for x in v.split(feature_pair_delimiter,1)]
                if f_key not in features:
                    features[f_key] = []
Grigorii Demidov's avatar
Grigorii Demidov committed
157
                f_value = f_value.replace("{{INSTALL_DIR}}",INSTALLDIR)
158 159 160
                features[f_key].append(f_value)
            except Exception as e:
                raise Exception ("can't parse feature-list (%s) in config section (%s)" % (v,str(e)));
161 162 163 164 165 166 167
        elif k == 'force-ipv6' and v.upper() == 'TRUE':
            scenario.force_ipv6 = True

    self_sockfamily = socket.AF_INET
    if scenario.force_ipv6 == True:
        self_sockfamily = socket.AF_INET6

168
    if stub_addr != "":
Marek Vavruša's avatar
Marek Vavruša committed
169 170
        selfaddr = stub_addr
    else:
171 172
        selfaddr = testserver.get_local_addr_str(self_sockfamily, DEFAULT_IFACE)
    childaddr = testserver.get_local_addr_str(self_sockfamily, CHILD_IFACE)
Marek Vavruša's avatar
Marek Vavruša committed
173 174
    # Prebind to sockets to create necessary files
    # @TODO: this is probably a workaround for socket_wrapper bug
175 176 177 178 179
    if 'NOPRELOAD' not in os.environ:
        for sock_type in (socket.SOCK_STREAM, socket.SOCK_DGRAM):
            sock = socket.socket(self_sockfamily, sock_type)
            sock.setsockopt(self_sockfamily, socket.SO_REUSEADDR, 1)
            sock.bind((childaddr, 53))
Ivana Krumlova's avatar
Ivana Krumlova committed
180
            if sock_type & socket.SOCK_STREAM:
181
                sock.listen(5)
182 183 184
    # Generate configuration files
    j2template_loader = jinja2.FileSystemLoader(searchpath=os.path.dirname(os.path.abspath(__file__)))
    j2template_env = jinja2.Environment(loader=j2template_loader)
Marek Vavruša's avatar
Marek Vavruša committed
185 186 187 188
    j2template_ctx = {
        "ROOT_ADDR" : selfaddr,
        "SELF_ADDR" : childaddr,
        "NO_MINIMIZE" : no_minimize,
189
        "TRUST_ANCHORS" : trust_anchor_list,
Marek Vavruša's avatar
Marek Vavruša committed
190
        "WORKING_DIR" : TMPDIR,
191 192
        "INSTALL_DIR" : INSTALLDIR,
        "FEATURES" : features
Marek Vavruša's avatar
Marek Vavruša committed
193
    }
Ivana Krumlova's avatar
Ivana Krumlova committed
194
    for template_name, config_name in zip(j2template_list,config_name_list):
195 196 197 198 199
        j2template = j2template_env.get_template(template_name)
        cfg_rendered = j2template.render(j2template_ctx)
        f = open(os.path.join(TMPDIR,config_name), 'w')
        f.write(cfg_rendered)
        f.close()
Marek Vavruša's avatar
Marek Vavruša committed
200 201 202 203 204

def play_object(path, binary_name, config_name, j2template, binary_additional_pars):
    """ Play scenario from a file object. """

    # Parse scenario
205
    case, config = scenario.parse_file(fileinput.input(path))
Marek Vavruša's avatar
Marek Vavruša committed
206 207 208

    # Setup daemon environment
    daemon_env = os.environ.copy()
209
    setup_env(case, daemon_env, config, config_name, j2template)
210

211
    server = testserver.TestServer(case, config, DEFAULT_IFACE)
212 213
    server.start()

214
    ignore_exit = bool(os.environ.get('IGNORE_EXIT_CODE', 0))
Marek Vavruša's avatar
Marek Vavruša committed
215 216 217 218 219
    # Start binary
    daemon_proc = None
    daemon_log = open('%s/server.log' % TMPDIR, 'w')
    daemon_args = [binary_name] + binary_additional_pars
    try :
Petr Špaček's avatar
Petr Špaček committed
220 221
        daemon_proc = subprocess.Popen(daemon_args, stdout=daemon_log, stderr=daemon_log,
                                       cwd=TMPDIR, preexec_fn=os.setsid, env=daemon_env)
Marek Vavruša's avatar
Marek Vavruša committed
222
    except Exception as e:
223
        server.stop()
Marek Vavruša's avatar
Marek Vavruša committed
224
        raise Exception("Can't start '%s': %s" % (daemon_args, str(e)))
225

Marek Vavruša's avatar
Marek Vavruša committed
226
    # Wait until the server accepts TCP clients
227
    sockfamily = socket.AF_INET
228
    if case.force_ipv6 == True:
229 230
        sockfamily = socket.AF_INET6
    sock = socket.socket(sockfamily, socket.SOCK_STREAM)
Marek Vavruša's avatar
Marek Vavruša committed
231
    while True:
232
        time.sleep(0.1)
Marek Vavruša's avatar
Marek Vavruša committed
233
        if daemon_proc.poll() != None:
234
            server.stop()
Marek Vavruša's avatar
Marek Vavruša committed
235 236 237
            print(open('%s/server.log' % TMPDIR).read())
            raise Exception('process died "%s", logs in "%s"' % (os.path.basename(binary_name), TMPDIR))
        try:
238
            sock.connect((testserver.get_local_addr_str(sockfamily, CHILD_IFACE), 53))
Marek Vavruša's avatar
Marek Vavruša committed
239 240
        except: continue
        break
241 242
    sock.close()

243 244
    # Bind to test servers
    for r in case.ranges:
245 246 247
        for addr in r.addresses:
            family = socket.AF_INET6 if ':' in addr else socket.AF_INET
            server.start_srv((addr, 53), family)
248 249 250 251 252 253 254 255 256 257 258 259
    # Bind addresses in ad-hoc REPLYs
    for s in case.steps:
        if s.type == 'REPLY':
            reply = s.data[0].message
            for rr in itertools.chain(reply.answer,reply.additional,reply.question,reply.authority):
                for rd in rr:
                    if rd.rdtype == dns.rdatatype.A:
                        server.start_srv((rd.address, 53), socket.AF_INET)
                    elif rd.rdtype == dns.rdatatype.AAAA:
                        server.start_srv((rd.address, 53), socket.AF_INET6)

    # Play test scenario
Marek Vavruša's avatar
Marek Vavruša committed
260
    try:
261
        server.play(CHILD_IFACE)
Ivana Krumlova's avatar
Ivana Krumlova committed
262 263
    except Exception as exc:
        ex = exc
264
        raise
Ivana Krumlova's avatar
Ivana Krumlova committed
265 266
    else:
        ex = None
Marek Vavruša's avatar
Marek Vavruša committed
267 268 269 270
    finally:
        server.stop()
        daemon_proc.terminate()
        daemon_proc.wait()
271
        if VERBOSE or ex or (daemon_proc.returncode and not ignore_exit):
272
            print(open('%s/server.log' % TMPDIR).read())
273
        if daemon_proc.returncode != 0 and not ignore_exit:
274 275
            raise ValueError('process terminated with return code %s'
                             % daemon_proc.returncode)
Marek Vavruša's avatar
Marek Vavruša committed
276
    # Do not clear files if the server crashed (for analysis)
277
    del_files(TMPDIR, OWN_TMPDIR)
Marek Vavruša's avatar
Marek Vavruša committed
278 279 280 281 282 283 284 285

def test_platform(*args):
    if sys.platform == 'windows':
        raise Exception('not supported at all on Windows')

if __name__ == '__main__':

    if len(sys.argv) < 5:
Ivana Krumlova's avatar
Ivana Krumlova committed
286 287 288 289 290 291
        print("Usage: test_integration.py <scenario> <binary> <template> <config name> [<additional>]")
        print("\t<scenario> - path to scenario")
        print("\t<binary> - executable to test")
        print("\t<template> - colon-separated list of jinja2 template files")
        print("\t<config name> - colon-separated list of files to be generated")
        print("\t<additional> - additional parameters for <binary>")
Marek Vavruša's avatar
Marek Vavruša committed
292 293 294 295 296
        sys.exit(0)

    test_platform()
    path_to_scenario = ""
    binary_name = ""
297 298
    template_name_list = ""
    config_name_list = ""
Marek Vavruša's avatar
Marek Vavruša committed
299 300 301 302 303
    binary_additional_pars = []

    if len(sys.argv) > 4:
        path_to_scenario = sys.argv[1]
        binary_name = sys.argv[2]
304 305 306
        template_name_list = sys.argv[3].split(':')
        config_name_list = sys.argv[4].split(':')
        if len(template_name_list) != len (config_name_list):
Petr Špaček's avatar
Petr Špaček committed
307 308 309
            print("ERROR: Number of j2 template files not equal to number of file names to be generated")
            print("i.e. len(<template>) != len(<config name>), see usage")
            sys.exit(0)
Marek Vavruša's avatar
Marek Vavruša committed
310 311 312 313 314 315 316 317 318

    if len(sys.argv) > 5:
        binary_additional_pars = sys.argv[5:]

    # Scan for scenarios
    test = test.Test()
    for arg in [path_to_scenario]:
        objects = find_objects(arg)
        for path in objects:
319
            test.add(path, play_object, path, binary_name, config_name_list, template_name_list, binary_additional_pars)
Marek Vavruša's avatar
Marek Vavruša committed
320
    sys.exit(test.run())