test_utils.py 12.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
"""
Utilities and mock modules for unittests of statistics modules

"""
import os
import io
import time
import sys
import threading
import tempfile
Naoki Kambe's avatar
Naoki Kambe committed
11
import json
12
import signal
13
14

import msgq
15
import isc.config.cfgmgr
16
17
18
import stats
import stats_httpd

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class SignalHandler():
    """A signal handler class for deadlock in unittest"""
    def __init__(self, fail_handler, timeout=20):
        """sets a schedule in SIGARM for invoking the handler via
        unittest.TestCase after timeout seconds (default is 20)"""
        self.fail_handler = fail_handler
        self.orig_handler = signal.signal(signal.SIGALRM, self.sig_handler)
        signal.alarm(timeout)

    def reset(self):
        """resets the schedule in SIGALRM"""
        signal.alarm(0)
        signal.signal(signal.SIGALRM, self.orig_handler)

    def sig_handler(self, signal, frame):
        """envokes unittest.TestCase.fail as a signal handler"""
        self.fail_handler("A deadlock might be detected")

Naoki Kambe's avatar
Naoki Kambe committed
37
38
def send_command(command_name, module_name, params=None, session=None, nonblock=False, timeout=None):
    if session is not None:
39
        cc_session = session
Naoki Kambe's avatar
Naoki Kambe committed
40
41
42
43
44
    else:
        cc_session = isc.cc.Session()
    if timeout is not None:
        orig_timeout = cc_session.get_timeout()
        cc_session.set_timeout(timeout * 1000)
45
46
47
48
49
50
51
52
53
    command = isc.config.ccsession.create_command(command_name, params)
    seq = cc_session.group_sendmsg(command, module_name)
    try:
        (answer, env) = cc_session.group_recvmsg(nonblock, seq)
        if answer:
            return isc.config.ccsession.parse_answer(answer)
    except isc.cc.SessionTimeout:
        pass
    finally:
Naoki Kambe's avatar
Naoki Kambe committed
54
        if timeout is not None:
55
            cc_session.set_timeout(orig_timeout)
Naoki Kambe's avatar
Naoki Kambe committed
56
57
        if session is None:
            cc_session.close()
58

Naoki Kambe's avatar
Naoki Kambe committed
59
60
def send_shutdown(module_name, **kwargs):
    return send_command("shutdown", module_name, **kwargs)
61
62

class ThreadingServerManager:
Naoki Kambe's avatar
Naoki Kambe committed
63
    def __init__(self, server, *args, **kwargs):
64
        self.server = server(*args, **kwargs)
Naoki Kambe's avatar
Naoki Kambe committed
65
        self.server_name = server.__name__
66
        self.server._thread = threading.Thread(
Naoki Kambe's avatar
Naoki Kambe committed
67
            name=self.server_name, target=self.server.run)
68
        self.server._thread.daemon = True
69

70
71
72
    def run(self):
        self.server._thread.start()
        self.server._started.wait()
73
        self.server._started.clear()
74
75
76

    def shutdown(self):
        self.server.shutdown()
Naoki Kambe's avatar
Naoki Kambe committed
77
        self.server._thread.join(0) # timeout is 0
78

Naoki Kambe's avatar
Naoki Kambe committed
79
80
81
82
83
84
85
86
def do_nothing(*args, **kwargs): pass

class dummy_sys:
    """Dummy for sys"""
    class dummy_io:
        write = do_nothing
    stdout = stderr = dummy_io()

87
class MockMsgq:
88
    def __init__(self):
89
        self._started = threading.Event()
Naoki Kambe's avatar
Naoki Kambe committed
90
91
92
93
        # suppress output to stdout and stderr
        msgq.sys = dummy_sys()
        msgq.print = do_nothing
        self.msgq = msgq.MsgQ(verbose=False)
94
95
96
97
98
99
100
101
102
103
        result = self.msgq.setup()
        if result:
            sys.exit("Error on Msgq startup: %s" % result)

    def run(self):
        self._started.set()
        try:
            self.msgq.run()
        except Exception:
            pass
104
105
106
107
108
        finally:
            # explicitly shut down the socket of the msgq before
            # shutting down the msgq
            self.msgq.listen_socket.shutdown(msgq.socket.SHUT_RDWR)
            self.msgq.shutdown()
109
110

    def shutdown(self):
Naoki Kambe's avatar
Naoki Kambe committed
111
        # do nothing
Naoki Kambe's avatar
Naoki Kambe committed
112
        pass
113
114

class MockCfgmgr:
115
    def __init__(self):
116
117
118
119
120
121
122
        self._started = threading.Event()
        self.cfgmgr = isc.config.cfgmgr.ConfigManager(
            os.environ['CONFIG_TESTDATA_PATH'], "b10-config.db")
        self.cfgmgr.read_config()

    def run(self):
        self._started.set()
Naoki Kambe's avatar
Naoki Kambe committed
123
124
125
126
        try:
            self.cfgmgr.run()
        except Exception:
            pass
127
128
129

    def shutdown(self):
        self.cfgmgr.running = False
130

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
class MockBoss:
    spec_str = """\
{
  "module_spec": {
    "module_name": "Boss",
    "module_description": "Mock Master process",
    "config_data": [],
    "commands": [
      {
        "command_name": "sendstats",
        "command_description": "Send data to a statistics module at once",
        "command_args": []
      }
    ],
    "statistics": [
      {
        "item_name": "boot_time",
        "item_type": "string",
        "item_optional": false,
        "item_default": "1970-01-01T00:00:00Z",
        "item_title": "Boot time",
        "item_description": "A date time when bind10 process starts initially",
        "item_format": "date-time"
      }
    ]
  }
}
"""
    _BASETIME = (2011, 6, 22, 8, 14, 8, 2, 173, 0)

161
    def __init__(self):
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
        self._started = threading.Event()
        self.running = False
        self.spec_file = io.StringIO(self.spec_str)
        # create ModuleCCSession object
        self.mccs = isc.config.ModuleCCSession(
            self.spec_file,
            self.config_handler,
            self.command_handler)
        self.spec_file.close()
        self.cc_session = self.mccs._session
        self.got_command_name = ''

    def run(self):
        self.mccs.start()
        self.running = True
        self._started.set()
Naoki Kambe's avatar
Naoki Kambe committed
178
179
180
181
182
        try:
            while self.running:
                self.mccs.check_command(False)
        except Exception:
            pass
183
184
185
186
187
188
189
190

    def shutdown(self):
        self.running = False

    def config_handler(self, new_config):
        return isc.config.create_answer(0)

    def command_handler(self, command, *args, **kwargs):
Naoki Kambe's avatar
Naoki Kambe committed
191
        self._started.set()
192
        self.got_command_name = command
193
194
195
196
197
        params = { "owner": "Boss",
                   "data": {
                'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', self._BASETIME)
                }
                   }
198
        if command == 'sendstats':
199
200
201
202
            send_command("set", "Stats", params=params, session=self.cc_session)
            return isc.config.create_answer(0)
        elif command == 'getstats':
            return isc.config.create_answer(0, params)
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
        return isc.config.create_answer(1, "Unknown Command")

class MockAuth:
    spec_str = """\
{
  "module_spec": {
    "module_name": "Auth",
    "module_description": "Mock Authoritative service",
    "config_data": [],
    "commands": [
      {
        "command_name": "sendstats",
        "command_description": "Send data to a statistics module at once",
        "command_args": []
      }
    ],
    "statistics": [
      {
        "item_name": "queries.tcp",
        "item_type": "integer",
        "item_optional": false,
        "item_default": 0,
225
        "item_title": "Queries TCP",
226
227
228
229
230
231
232
233
234
        "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
      },
      {
        "item_name": "queries.udp",
        "item_type": "integer",
        "item_optional": false,
        "item_default": 0,
        "item_title": "Queries UDP",
        "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
235
236
      },
      {
237
        "item_name": "queries.perzone",
238
239
        "item_type": "list",
        "item_optional": false,
240
241
242
243
244
245
246
247
248
249
250
251
        "item_default": [
          {
            "zonename" : "test1.example",
            "queries.udp" : 1,
            "queries.tcp" : 2
          },
          {
            "zonename" : "test2.example",
            "queries.udp" : 3,
            "queries.tcp" : 4
          }
        ],
252
253
254
        "item_title": "Queries per zone",
        "item_description": "Queries per zone",
        "list_item_spec": {
255
          "item_name": "zones",
256
257
258
259
260
261
262
263
          "item_type": "map",
          "item_optional": false,
          "item_default": {},
          "map_item_spec": [
            {
              "item_name": "zonename",
              "item_type": "string",
              "item_optional": false,
Naoki Kambe's avatar
Naoki Kambe committed
264
265
266
              "item_default": "",
              "item_title": "Zonename",
              "item_description": "Zonename"
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
            },
            {
              "item_name": "queries.udp",
              "item_type": "integer",
              "item_optional": false,
              "item_default": 0,
              "item_title": "Queries UDP per zone",
              "item_description": "A number of UDP query counts per zone"
            },
            {
              "item_name": "queries.tcp",
              "item_type": "integer",
              "item_optional": false,
              "item_default": 0,
              "item_title": "Queries TCP per zone",
              "item_description": "A number of TCP query counts per zone"
            }
          ]
        }
286
287
288
289
290
      }
    ]
  }
}
"""
291
    def __init__(self):
292
293
294
295
296
297
298
299
300
301
302
303
304
        self._started = threading.Event()
        self.running = False
        self.spec_file = io.StringIO(self.spec_str)
        # create ModuleCCSession object
        self.mccs = isc.config.ModuleCCSession(
            self.spec_file,
            self.config_handler,
            self.command_handler)
        self.spec_file.close()
        self.cc_session = self.mccs._session
        self.got_command_name = ''
        self.queries_tcp = 3
        self.queries_udp = 2
305
        self.queries_per_zone = [{
306
                'zonename': 'test1.example',
307
308
309
                'queries.tcp': 5,
                'queries.udp': 4
                }]
310
311
312
313
314

    def run(self):
        self.mccs.start()
        self.running = True
        self._started.set()
Naoki Kambe's avatar
Naoki Kambe committed
315
316
317
318
319
        try:
            while self.running:
                self.mccs.check_command(False)
        except Exception:
            pass
320
321
322
323
324
325
326
327
328
329
330
331

    def shutdown(self):
        self.running = False

    def config_handler(self, new_config):
        return isc.config.create_answer(0)

    def command_handler(self, command, *args, **kwargs):
        self.got_command_name = command
        if command == 'sendstats':
            params = { "owner": "Auth",
                       "data": { 'queries.tcp': self.queries_tcp,
332
333
                                 'queries.udp': self.queries_udp,
                                 'queries.per-zone' : self.queries_per_zone } }
334
335
336
337
            return send_command("set", "Stats", params=params, session=self.cc_session)
        return isc.config.create_answer(1, "Unknown Command")

class MyStats(stats.Stats):
338
    def __init__(self):
339
        self._started = threading.Event()
340
        stats.Stats.__init__(self)
341
342
343

    def run(self):
        self._started.set()
Naoki Kambe's avatar
Naoki Kambe committed
344
345
346
347
        try:
            self.start()
        except Exception:
            pass
348
349

    def shutdown(self):
Naoki Kambe's avatar
Naoki Kambe committed
350
        self.command_shutdown()
351

Jelte Jansen's avatar
Jelte Jansen committed
352
353
354
355
356
357
358
359
360
361
362
class MyModuleCCSession():
    def __init__(self):
        self.stopped = False
        self.closed = False

    def stop(self):
        self.stopped = True

    def close(self):
        self.closed = True

363
class MyStatsHttpd(stats_httpd.StatsHttpd):
Naoki Kambe's avatar
Naoki Kambe committed
364
365
    ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
    def __init__(self, *server_address):
366
        self._started = threading.Event()
Naoki Kambe's avatar
Naoki Kambe committed
367
        if server_address:
368
            stats_httpd.SPECFILE_LOCATION = self.create_specfile(*server_address)
Naoki Kambe's avatar
Naoki Kambe committed
369
370
371
            try:
                stats_httpd.StatsHttpd.__init__(self)
            finally:
372
373
                if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
                    stats_httpd.SPECFILE_LOCATION.close()
Naoki Kambe's avatar
Naoki Kambe committed
374
375
376
377
                stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
        else:
            stats_httpd.StatsHttpd.__init__(self)

378
379
380
381
382
383
384
385
386
387
388
389
390
391
    def create_specfile(self, *server_address):
        spec_io = open(self.ORIG_SPECFILE_LOCATION)
        try:
            spec = json.load(spec_io)
            spec_io.close()
            config = spec['module_spec']['config_data']
            for i in range(len(config)):
                if config[i]['item_name'] == 'listen_on':
                    config[i]['item_default'] = \
                        [ dict(address=a[0], port=a[1]) for a in server_address ]
                    break
            return io.StringIO(json.dumps(spec))
        finally:
            spec_io.close()
392
393
394

    def run(self):
        self._started.set()
Naoki Kambe's avatar
Naoki Kambe committed
395
396
397
398
        try:
            self.start()
        except Exception:
            pass
399
400

    def shutdown(self):
Naoki Kambe's avatar
Naoki Kambe committed
401
        self.command_handler('shutdown', None)
402
403

class BaseModules:
404
    def __init__(self):
405
        # MockMsgq
406
        self.msgq = ThreadingServerManager(MockMsgq)
407
        self.msgq.run()
Naoki Kambe's avatar
Naoki Kambe committed
408
409
        # Check whether msgq is ready. A SessionTimeout is raised here if not.
        isc.cc.session.Session().close()
410
        # MockCfgmgr
411
        self.cfgmgr = ThreadingServerManager(MockCfgmgr)
412
413
        self.cfgmgr.run()
        # MockBoss
414
        self.boss = ThreadingServerManager(MockBoss)
415
416
        self.boss.run()
        # MockAuth
417
        self.auth = ThreadingServerManager(MockAuth)
418
419
420
421
422
423
424
425
426
427
428
        self.auth.run()

    def shutdown(self):
        # MockAuth
        self.auth.shutdown()
        # MockBoss
        self.boss.shutdown()
        # MockCfgmgr
        self.cfgmgr.shutdown()
        # MockMsgq
        self.msgq.shutdown()
Naoki Kambe's avatar
Naoki Kambe committed
429
430
431
432
433
434
435
        # remove the unused socket file
        socket_file = self.msgq.server.msgq.socket_file
        try:
            if os.path.exists(socket_file):
                os.remove(socket_file)
        except OSError:
            pass