bind10_control.py 18.2 KB
Newer Older
1
# Copyright (C) 2011-2012  Internet Systems Consortium.
Jelte Jansen's avatar
Jelte Jansen committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Jelte Jansen's avatar
Jelte Jansen committed
16
from lettuce import *
17
import time
Jelte Jansen's avatar
Jelte Jansen committed
18
import subprocess
19
import re
20
import json
Jelte Jansen's avatar
Jelte Jansen committed
21

22
23
24
25
26
27
28
29
@step('sleep for (\d+) seconds')
def wait_seconds(step, seconds):
    """Sleep for some seconds.
       Parameters:
       seconds number of seconds to sleep for.
    """
    time.sleep(float(seconds))

30
@step('start bind10(?: with configuration (\S+))?' +\
31
32
33
34
      '(?: with cmdctl port (\d+))?' +\
      '(?: with msgq socket file (\S+))?' +\
      '(?: as (\S+))?')
def start_bind10(step, config_file, cmdctl_port, msgq_sockfile, process_name):
Jelte Jansen's avatar
Jelte Jansen committed
35
36
37
38
39
40
41
42
43
    """
    Start BIND 10 with the given optional config file, cmdctl port, and
    store the running process in world with the given process name.
    Parameters:
    config_file ('with configuration <file>', optional): this configuration
                will be used. The path is relative to the base lettuce
                directory.
    cmdctl_port ('with cmdctl port <portnr>', optional): The port on which
                b10-cmdctl listens for bindctl commands. Defaults to 47805.
44
45
    msgq_sockfile ('with msgq socket file', optional): The msgq socket file
                that will be used for internal communication
Jelte Jansen's avatar
Jelte Jansen committed
46
47
48
49
50
51
52
53
54
    process_name ('as <name>', optional). This is the name that can be used
                 in the following steps of the scenario to refer to this
                 BIND 10 instance. Defaults to 'bind10'.
    This call will block until BIND10_STARTUP_COMPLETE or BIND10_STARTUP_ERROR
    is logged. In the case of the latter, or if it times out, the step (and
    scenario) will fail.
    It will also fail if there is a running process with the given process_name
    already.
    """
55
    args = [ 'bind10', '-v' ]
Jelte Jansen's avatar
Jelte Jansen committed
56
    if config_file is not None:
57
58
        args.append('-p')
        args.append("configurations/")
Jelte Jansen's avatar
Jelte Jansen committed
59
        args.append('-c')
60
        args.append(config_file)
61
    if cmdctl_port is None:
62
        args.append('--cmdctl-port=47805')
63
64
    else:
        args.append('--cmdctl-port=' + cmdctl_port)
65
66
    if process_name is None:
        process_name = "bind10"
67
68
69
    else:
        args.append('-m')
        args.append(process_name + '_msgq.socket')
Jelte Jansen's avatar
Jelte Jansen committed
70

71
72
    world.processes.add_process(step, process_name, args)

Jelte Jansen's avatar
Jelte Jansen committed
73
    # check output to know when startup has been completed
74
75
76
77
    (message, line) = world.processes.wait_for_stderr_str(process_name,
                                                     ["BIND10_STARTUP_COMPLETE",
                                                      "BIND10_STARTUP_ERROR"])
    assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(line)
Jelte Jansen's avatar
Jelte Jansen committed
78

79
80
@step('wait for bind10 auth (?:of (\w+) )?to start')
def wait_for_auth(step, process_name):
Jelte Jansen's avatar
Jelte Jansen committed
81
82
83
84
85
86
    """Wait for b10-auth to run. This is done by blocking until the message
       AUTH_SERVER_STARTED is logged.
       Parameters:
       process_name ('of <name', optional): The name of the BIND 10 instance
                    to wait for. Defaults to 'bind10'.
    """
87
88
    if process_name is None:
        process_name = "bind10"
89
90
    world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'],
                                        False)
Jelte Jansen's avatar
Jelte Jansen committed
91

Jelte Jansen's avatar
Jelte Jansen committed
92
93
94
95
96
97
98
99
100
101
102
103
104
105
@step('wait for bind10 xfrout (?:of (\w+) )?to start')
def wait_for_xfrout(step, process_name):
    """Wait for b10-xfrout to run. This is done by blocking until the message
       XFROUT_NEW_CONFIG_DONE is logged.
       Parameters:
       process_name ('of <name', optional): The name of the BIND 10 instance
                    to wait for. Defaults to 'bind10'.
    """
    if process_name is None:
        process_name = "bind10"
    world.processes.wait_for_stderr_str(process_name,
                                        ['XFROUT_NEW_CONFIG_DONE'],
                                        False)

106
107
108
109
@step('have bind10 running(?: with configuration ([\S]+))?' +\
      '(?: with cmdctl port (\d+))?' +\
      '(?: as ([\S]+))?')
def have_bind10_running(step, config_file, cmdctl_port, process_name):
Jelte Jansen's avatar
Jelte Jansen committed
110
111
    """
    Compound convenience step for running bind10, which consists of
112
    start_bind10.
Jelte Jansen's avatar
Jelte Jansen committed
113
114
    Currently only supports the 'with configuration' option.
    """
115
116
117
118
119
120
    start_step = 'start bind10 with configuration ' + config_file
    if cmdctl_port is not None:
        start_step += ' with cmdctl port ' + str(cmdctl_port)
    if process_name is not None:
        start_step += ' as ' + process_name
    step.given(start_step)
121

122
# function to send lines to bindctl, and store the result
Jelte Jansen's avatar
Jelte Jansen committed
123
def run_bindctl(commands, cmdctl_port=None):
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    """Run bindctl.
       Parameters:
       commands: a sequence of strings which will be sent.
       cmdctl_port: a port number on which cmdctl is listening, is converted
                    to string if necessary. If not provided, or None, defaults
                    to 47805

       bindctl's stdout and stderr streams are stored (as one multiline string
       in world.last_bindctl_stdout/stderr.
       Fails if the return code is not 0
    """
    if cmdctl_port is None:
        cmdctl_port = 47805
    args = ['bindctl', '-p', str(cmdctl_port)]
    bindctl = subprocess.Popen(args, 1, None, subprocess.PIPE,
                               subprocess.PIPE, None)
    for line in commands:
        bindctl.stdin.write(line + "\n")
    (stdout, stderr) = bindctl.communicate()
    result = bindctl.returncode
    world.last_bindctl_stdout = stdout
    world.last_bindctl_stderr = stderr
    assert result == 0, "bindctl exit code: " + str(result) +\
                        "\nstdout:\n" + str(stdout) +\
                        "stderr:\n" + str(stderr)


151
152
@step('last bindctl( stderr)? output should( not)? contain (\S+)( exactly)?')
def check_bindctl_output(step, stderr, notv, string, exactly):
153
154
155
156
157
158
159
    """Checks the stdout (or stderr) stream of the last run of bindctl,
       fails if the given string is not found in it (or fails if 'not' was
       set and it is found
       Parameters:
       stderr ('stderr'): Check stderr instead of stdout output
       notv ('not'): reverse the check (fail if string is found)
       string ('contain <string>') string to look for
160
       exactly ('exactly'): Make an exact match delimited by whitespace
161
162
163
164
165
166
    """
    if stderr is None:
        output = world.last_bindctl_stdout
    else:
        output = world.last_bindctl_stderr
    found = False
167
168
169
170
171
172
    if exactly is None:
        if string in output:
            found = True
    else:
        if re.search(r'^\s+' + string + r'\s+', output, re.IGNORECASE | re.MULTILINE) is not None:
            found = True
173
174
175
176
177
178
179
180
181
    if notv is None:
        assert found == True, "'" + string +\
                              "' was not found in bindctl output:\n" +\
                              output
    else:
        assert not found, "'" + string +\
                          "' was found in bindctl output:\n" +\
                          output

182
183
184
185
186
187
188
189
190
def parse_bindctl_output_as_data_structure():
    """Helper function for data-related command tests: evaluates the
       last output of bindctl as a data structure that can then be
       inspected.
       If the bindctl output is not valid (json) data, this call will
       fail with an assertion failure.
       If it is valid, it is parsed and returned as whatever data
       structure it represented.
    """
191
192
193
194
195
196
197
    # strip any extra output after a charater that commonly terminates a valid
    # JSON expression, i.e., ']', '}' and '"'.  (The extra output would
    # contain 'Exit from bindctl' message, and depending on environment some
    # other control-like characters...but why is this message even there?)
    # Note that this filter is not perfect.  For example, it cannot recognize
    # a simple expression of true/false/null.
    output = re.sub("(.*)([^]}\"]*$)", r"\1", world.last_bindctl_stdout)
198
199
200
201
    try:
        return json.loads(output)
    except ValueError as ve:
        assert False, "Last bindctl output does not appear to be a " +\
Jelte Jansen's avatar
Jelte Jansen committed
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
                      "parseable data structure: '" + output + "': " + str(ve)

def find_process_pid(step, process_name):
    """Helper function to request the running processes from Boss, and
       return the pid of the process with the given process_name.
       Fails with an assert if the response from boss is not valid JSON,
       or if the process with the given name is not found.
    """
    # show_processes output is a list of lists, where the inner lists
    # are of the form [ pid, "name" ]
    # Not checking data form; errors will show anyway (if these turn
    # out to be too vague, we can change this)
    step.given('send bind10 the command Boss show_processes')
    running_processes = parse_bindctl_output_as_data_structure()

    for process in running_processes:
        if process[1] == process_name:
            return process[0]
    assert False, "Process named " + process_name +\
                  " not found in output of Boss show_processes";
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

@step("remember the pid of process ([\S]+)")
def remember_pid(step, process_name):
    """Stores the PID of the process with the given name as returned by
       Boss show_processes command.
       Fails if the process with the given name does not appear to exist.
       Stores the component_name->pid value in the dict world.process_pids.
       This should only be used by the related step
       'the pid of process <name> should (not) have changed'
       Arguments:
       process name ('process <name>') the name of the component to store
                                       the pid of.
    """
    if world.process_pids is None:
        world.process_pids = {}
Jelte Jansen's avatar
Jelte Jansen committed
237
    world.process_pids[process_name] = find_process_pid(step, process_name)
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

@step('pid of process ([\S]+) should not have changed')
def check_pid(step, process_name):
    """Checks the PID of the process with the given name as returned by
       Boss show_processes command.
       Fails if the process with the given name does not appear to exist.
       Fails if the process with the given name exists, but has a different
       pid than it had when the step 'remember the pid of process' was
       called.
       Fails if that step has not been called (since world.process_pids
       does not exist).
    """
    assert world.process_pids is not None, "No process pids stored"
    assert process_name in world.process_pids, "Process named " +\
                                               process_name +\
                                               " was not stored"
Jelte Jansen's avatar
Jelte Jansen committed
254
255
    pid = find_process_pid(step, process_name)
    assert world.process_pids[process_name] == pid,\
Jelte Jansen's avatar
Jelte Jansen committed
256
                   "Expected pid: " + str(world.process_pids[process_name]) +\
Jelte Jansen's avatar
Jelte Jansen committed
257
                   " Got pid: " + str(pid)
258

Jelte Jansen's avatar
Jelte Jansen committed
259
@step('set bind10 configuration (\S+) to (.*)(?: with cmdctl port (\d+))?')
260
def config_set_command(step, name, value, cmdctl_port):
Jelte Jansen's avatar
Jelte Jansen committed
261
262
263
264
265
266
267
268
269
    """
    Run bindctl, set the given configuration to the given value, and commit it.
    Parameters:
    name ('configuration <name>'): Identifier of the configuration to set
    value ('to <value>'): value to set it to.
    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                the command to. Defaults to 47805.
    Fails if cmdctl does not exit with status code 0.
    """
270
271
272
273
274
    commands = ["config set " + name + " " + value,
                "config commit",
                "quit"]
    run_bindctl(commands, cmdctl_port)

275
276
277
278
279
280
281
282
283
284
285
286
287
288
@step('send bind10 the following commands(?: with cmdctl port (\d+))?')
def send_multiple_commands(step, cmdctl_port):
    """
    Run bindctl, and send it the given multiline set of commands.
    A quit command is always appended.
    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                the command to. Defaults to 47805.
    Fails if cmdctl does not exit with status code 0.
    """
    commands = step.multiline.split("\n")
    # Always add quit
    commands.append("quit")
    run_bindctl(commands, cmdctl_port)

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
@step('remove bind10 configuration (\S+)(?: value (\S+))?(?: with cmdctl port (\d+))?')
def config_remove_command(step, name, value, cmdctl_port):
    """
    Run bindctl, remove the given configuration item, and commit it.
    Parameters:
    name ('configuration <name>'): Identifier of the configuration to remove
    value ('value <value>'): if name is a named set, use value to identify
                             item to remove
    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                the command to. Defaults to 47805.
    Fails if cmdctl does not exit with status code 0.
    """
    cmd = "config remove " + name
    if value is not None:
        cmd = cmd + " " + value
    commands = [cmd,
                "config commit",
                "quit"]
    run_bindctl(commands, cmdctl_port)
308

309
310
@step('send bind10(?: with cmdctl port (\d+))? the command (.+)')
def send_command(step, cmdctl_port, command):
311
    """
312
    Run bindctl, send the given command, and exit bindctl.
313
    Parameters:
314
    command ('the command <command>'): The command to send.
315
316
317
318
    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                the command to. Defaults to 47805.
    Fails if cmdctl does not exit with status code 0.
    """
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
    commands = [command,
                "quit"]
    run_bindctl(commands, cmdctl_port)

@step('bind10 module (\S+) should( not)? be running')
def module_is_running(step, name, not_str):
    """
    Convenience step to check if a module is running; can only work with
    default cmdctl port; sends a 'help' command with bindctl, then
    checks if the output contains the given name.
    Parameters:
    name ('module <name>'): The name of the module (case sensitive!)
    not ('not'): Reverse the check (fail if it is running)
    """
    if not_str is None:
        not_str = ""
    step.given('send bind10 the command help')
336
    step.given('last bindctl output should' + not_str + ' contain ' + name + ' exactly')
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364

@step('Configure BIND10 to run DDNS')
def configure_ddns_on(step):
    """
    Convenience compound step to enable the b10-ddns module.
    """
    step.behave_as("""
    When I send bind10 the following commands
        \"\"\"
        config add Boss/components b10-ddns
        config set Boss/components/b10-ddns/kind dispensable
        config set Boss/components/b10-ddns/address DDNS
        config commit
        \"\"\"
    """)

@step('Configure BIND10 to stop running DDNS')
def configure_ddns_off(step):
    """
    Convenience compound step to disable the b10-ddns module.
    """
    step.behave_as("""
    When I send bind10 the following commands
        \"\"\"
        config remove Boss/components b10-ddns
        config commit
        \"\"\"
    """)
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393

@step('query statistics(?: (\S+))? of bind10 module (\S+)(?: with cmdctl port (\d+))?')
def query_statistics(step, statistics, name, cmdctl_port):
    """
    query statistics data via bindctl.
    Parameters:
    statistics  ('statistics <statistics>', optional) : The queried statistics name.
    name ('module <name>'): The name of the module (case sensitive!)
    cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
                the command to.
    """
    port_str = ' with cmdctl port %s' % cmdctl_port \
        if cmdctl_port else ''
    step.given('send bind10%s the command Stats show owner=%s%s'\
        % (port_str, name,\
               ' name=%s' % statistics if statistics else ''))

def find_value(dictionary, key):
    """A helper method. Recursively find a value corresponding to the
    key of the dictionary and returns it. Returns None if the
    dictionary is not dict type."""
    if type(dictionary) is not dict:
        return
    if key in dictionary:
        return dictionary[key]
    else:
        for v in dictionary.values():
            return find_value(v, key)

394
395
@step('the statistics counter (\S+)(?: in the category (\S+))?'+ \
          '(?: for the zone (\S+))? should be' + \
396
          '(?:( greater than| less than| between))? (\-?\d+)(?: and (\-?\d+))?')
397
def check_statistics(step, counter, category, zone, gtltbt, number, upper):
398
399
400
401
402
    """
    check the output of bindctl for statistics of specified counter
    and zone.
    Parameters:
    counter ('counter <counter>'): The counter name of statistics.
403
    category ('category <category>', optional): The category of counter.
404
    zone ('zone <zone>', optional): The zone name.
405
406
    gtltbt (' greater than'|' less than'|' between', optional): greater than
          <number> or less than <number> or between <number> and <upper>.
407
408
    number ('<number>): The expect counter number. <number> is assumed
          to be an unsigned integer.
409
410
    upper ('<upper>, optional): The expect upper counter number when
          using 'between'.
411
412
413
    """
    output = parse_bindctl_output_as_data_structure()
    found = None
414
    category_str = ""
415
    zone_str = ""
416
417
418
419
    depth = []
    if category:
        depth.insert(0, category)
        category_str = " for category %s" % category
420
    if zone:
421
        depth.insert(0, zone)
422
        zone_str = " for zone %s" % zone
423
424
    for level in depth:
        output = find_value(output, level)
425
426
427
    else:
        found = find_value(output, counter)
    assert found is not None, \
428
429
        'Not found statistics counter %s%s%s' % \
            (counter, category_str, zone_str)
430
    msg = "Got %s, expected%s %s as counter %s%s" % \
431
432
433
434
435
436
437
        (found, gtltbt, number, counter, zone_str)
    if gtltbt and 'between' in gtltbt and upper:
        msg = "Got %s, expected%s %s and %s as counter %s%s" % \
            (found, gtltbt, number, upper, counter, zone_str)
        assert int(number) <= int(found) \
            and int(found) <= int(upper), msg
    elif gtltbt and 'greater' in gtltbt:
438
        assert int(found) > int(number), msg
439
    elif gtltbt and 'less' in gtltbt:
440
441
442
        assert int(found) < int(number), msg
    else:
        assert int(found) == int(number), msg