~ubuntuone-hackers/conn-check/trunk

« back to all changes in this revision

Viewing changes to conn_check/main.py

[r=wesmason] Add firewall rule yaml output options

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
from argparse import ArgumentParser
2
 
from collections import defaultdict, OrderedDict
 
2
from collections import defaultdict
3
3
import sys
4
4
from threading import Thread
5
5
import time
16
16
from .check_impl import (
17
17
    FailureCountingResultWrapper,
18
18
    parallel_check,
 
19
    skipping_check,
19
20
    ResultTracker,
20
21
    )
21
22
from .checks import CHECK_ALIASES, CHECKS, load_tls_certs
33
34
 
34
35
    check = CHECKS.get(_type, None)
35
36
    if check is None:
36
 
        raise AssertionError("Unknown check type: {}, available checks: {}".format(
37
 
            _type, CHECKS.keys()))
 
37
        raise AssertionError("Unknown check type: {}, available checks: {}"
 
38
                             .format(_type, CHECKS.keys()))
38
39
    for arg in check['args']:
39
40
        if arg not in check_description:
40
41
            raise AssertionError('{} missing from check: {}'.format(arg,
41
 
                check_description))
 
42
                                 check_description))
 
43
 
42
44
    res = check['fn'](**check_description)
43
45
    return res
44
46
 
 
47
 
45
48
def filter_tags(check, include, exclude):
46
49
    if not include and not exclude:
47
50
        return True
57
60
 
58
61
 
59
62
def build_checks(check_descriptions, connect_timeout, include_tags,
60
 
                 exclude_tags):
 
63
                 exclude_tags, skip_checks=False):
61
64
    def set_timeout(desc):
62
65
        new_desc = dict(timeout=connect_timeout)
63
66
        new_desc.update(desc)
64
67
        return new_desc
 
68
 
65
69
    check_descriptions = filter(
66
70
        lambda c: filter_tags(c, include_tags, exclude_tags),
67
71
        check_descriptions)
68
 
    subchecks = map(check_from_description,
 
72
 
 
73
    subchecks = map(
 
74
        lambda c: check_from_description(c),
69
75
        map(set_timeout, check_descriptions))
70
 
    return parallel_check(subchecks)
 
76
 
 
77
    if skip_checks:
 
78
        strategy_wrapper = skipping_check
 
79
    else:
 
80
        strategy_wrapper = parallel_check
 
81
    return strategy_wrapper(subchecks)
71
82
 
72
83
 
73
84
@inlineCallbacks
82
93
class NagiosCompatibleArgsParser(ArgumentParser):
83
94
 
84
95
    def error(self, message):
85
 
        """A patched version of ArgumentParser.error which does the same
86
 
        thing, e.g. prints an error message and exits, but does so with
87
 
        an exit code of 3 rather than 2, to maintain compatibility with
88
 
        Nagios checks."""
 
96
        """A patched version of ArgumentParser.error.
 
97
 
 
98
        Does the same thing as ArgumentParser.error, e.g. prints an error
 
99
        message and exits, but does so with an exit code of 3 rather than 2,
 
100
        to maintain compatibility with Nagios checks.
 
101
        """
89
102
        self.print_usage(sys.stderr)
90
 
        self.exit(3, '%s: error: %s\n' % (self.prog, message))
 
103
        self.exit(3, '{}: error: {}\n'.format(self.prog, message))
91
104
 
92
105
 
93
106
class TimestampOutput(object):
97
110
        self.output = output
98
111
 
99
112
    def write(self, data):
100
 
        self.output.write("%.3f: %s" % (time.time() - self.start, data))
 
113
        self.output.write("{:.3f}: {}".format(time.time() - self.start, data))
101
114
 
102
115
 
103
116
class OrderedOutput(object):
 
117
    """Outputs check results ordered by FAILED, SUCCESSFUL, SKIPPED checks."""
104
118
 
105
119
    def __init__(self, output):
106
120
        self.output = output
140
154
 
141
155
 
142
156
class ConsoleOutput(ResultTracker):
143
 
    """Displays check results."""
 
157
    """Outputs check results to STDOUT."""
144
158
 
145
159
    def __init__(self, output, verbose, show_tracebacks, show_duration):
146
160
        """Initialize an instance."""
153
167
    def format_duration(self, duration):
154
168
        if not self.show_duration:
155
169
            return ""
156
 
        return ": (%.3f ms)" % duration
 
170
        return ": ({:.3f} ms)".format(duration)
157
171
 
158
172
    def notify_start(self, name, info):
159
173
        """Register the start of a check."""
160
174
        if self.verbose:
161
175
            if info:
162
 
                info = " (%s)" % (info,)
 
176
                info = " ({})".format(info)
163
177
            else:
164
178
                info = ''
165
 
            self.output.write("Starting %s%s...\n" % (name, info))
 
179
            self.output.write("Starting {}{}...\n".format(name, info))
166
180
 
167
181
    def notify_skip(self, name):
168
182
        """Register a check being skipped."""
169
 
        self.output.write("SKIPPED: %s\n" % (name,))
 
183
        self.output.write("SKIPPED: {}\n".format(name))
170
184
 
171
185
    def notify_success(self, name, duration):
172
186
        """Register a success."""
173
 
        self.output.write("%s OK%s\n" % (
 
187
        self.output.write("{} OK{}\n".format(
174
188
            name, self.format_duration(duration)))
175
189
 
176
190
    def notify_failure(self, name, info, exc_info, duration):
177
191
        """Register a failure."""
178
192
        message = str(exc_info[1]).split("\n")[0]
179
193
        if info:
180
 
            message = "(%s) %s" % (info, message)
181
 
        self.output.write("%s FAILED%s - %s\n" % (
 
194
            message = "({}) {}".format(info, message)
 
195
        self.output.write("{} FAILED{} - {}\n".format(
182
196
            name, self.format_duration(duration), message))
183
197
 
184
198
        if self.show_tracebacks:
189
203
            lines = "".join(formatted).split("\n")
190
204
            if len(lines) > 0 and len(lines[-1]) == 0:
191
205
                lines.pop()
192
 
            indented = "\n".join(["  %s" % (line,) for line in lines])
193
 
            self.output.write("%s\n" % (indented,))
194
 
 
195
 
 
196
 
def main(*args):
197
 
    """Parse arguments, then build and run checks in a reactor."""
198
 
 
199
 
    # We do this first because ArgumentParser won't let us mix and match
200
 
    # non-default positional argument with a flag argument
 
206
            indented = "\n".join(["  {}".format(line) for line in lines])
 
207
            self.output.write("{}\n".format(indented))
 
208
 
 
209
 
 
210
class Command(object):
 
211
    """CLI command runner for the main conn-check endpoint."""
 
212
 
 
213
    def __init__(self, args):
 
214
        self.make_arg_parser()
 
215
        self.parse_options(args)
 
216
        self.wrap_output(sys.stdout)
 
217
        self.load_descriptions()
 
218
 
 
219
    def make_arg_parser(self):
 
220
        """Set up an arg parser with our options."""
 
221
 
 
222
        parser = NagiosCompatibleArgsParser()
 
223
        parser.add_argument("config_file",
 
224
                            help="Config file specifying the checks to run.")
 
225
        parser.add_argument("patterns", nargs='*',
 
226
                            help="Patterns to filter the checks.")
 
227
        parser.add_argument("-v", "--verbose", dest="verbose",
 
228
                            action="store_true", default=False,
 
229
                            help="Show additional status")
 
230
        parser.add_argument("-d", "--duration", dest="show_duration",
 
231
                            action="store_true", default=False,
 
232
                            help="Show duration")
 
233
        parser.add_argument("-t", "--tracebacks", dest="show_tracebacks",
 
234
                            action="store_true", default=False,
 
235
                            help="Show tracebacks on failure")
 
236
        parser.add_argument("--validate", dest="validate",
 
237
                            action="store_true", default=False,
 
238
                            help="Only validate the config file,"
 
239
                            " don't run checks.")
 
240
        parser.add_argument("--version", dest="print_version",
 
241
                            action="store_true", default=False,
 
242
                            help="Print the currently installed version.")
 
243
        parser.add_argument("--tls-certs-path", dest="cacerts_path",
 
244
                            action="store", default="/etc/ssl/certs/",
 
245
                            help="Path to TLS CA certificates.")
 
246
        parser.add_argument("--max-timeout", dest="max_timeout", type=float,
 
247
                            action="store", help="Maximum execution time.")
 
248
        parser.add_argument("--connect-timeout", dest="connect_timeout",
 
249
                            action="store", default=10, type=float,
 
250
                            help="Network connection timeout.")
 
251
        parser.add_argument("-U", "--unbuffered-output", dest="buffer_output",
 
252
                            action="store_false", default=True,
 
253
                            help="Don't buffer output, write to STDOUT right "
 
254
                            "away.")
 
255
        parser.add_argument("--dry-run",
 
256
                            dest="dry_run", action="store_true",
 
257
                            default=False,
 
258
                            help="Skip all checks, just print out"
 
259
                            " what would be run.")
 
260
        group = parser.add_mutually_exclusive_group()
 
261
        group.add_argument("--include-tags", dest="include_tags",
 
262
                           action="store", default="",
 
263
                           help="Comma separated list of tags to include.")
 
264
        group.add_argument("--exclude-tags", dest="exclude_tags",
 
265
                           action="store", default="",
 
266
                           help="Comma separated list of tags to exclude.")
 
267
        self.parser = parser
 
268
 
 
269
    def setup_reactor(self):
 
270
        """Setup the Twisted reactor with required customisations."""
 
271
 
 
272
        def make_daemon_thread(*args, **kw):
 
273
            """Create a daemon thread."""
 
274
            thread = Thread(*args, **kw)
 
275
            thread.daemon = True
 
276
            return thread
 
277
 
 
278
        threadpool = ThreadPool(minthreads=1)
 
279
        threadpool.threadFactory = make_daemon_thread
 
280
        reactor.threadpool = threadpool
 
281
        reactor.callWhenRunning(threadpool.start)
 
282
 
 
283
        if self.options.max_timeout is not None:
 
284
            def terminator():
 
285
                # Hasta la vista, twisted
 
286
                reactor.stop()
 
287
                print('Maximum timeout reached: {}s'.format(
 
288
                      self.options.max_timeout))
 
289
 
 
290
            reactor.callLater(self.options.max_timeout, terminator)
 
291
 
 
292
    def parse_options(self, args):
 
293
        """Parse args (e.g. sys.argv) into options and set some config."""
 
294
 
 
295
        options = self.parser.parse_args(list(args))
 
296
 
 
297
        include_tags = []
 
298
        if options.include_tags:
 
299
            include_tags = options.include_tags.split(',')
 
300
            include_tags = [tag.strip() for tag in include_tags]
 
301
        options.include_tags = include_tags
 
302
 
 
303
        exclude_tags = []
 
304
        if options.exclude_tags:
 
305
            exclude_tags = options.exclude_tags.split(',')
 
306
            exclude_tags = [tag.strip() for tag in exclude_tags]
 
307
        options.exclude_tags = exclude_tags
 
308
 
 
309
        if options.patterns:
 
310
            self.patterns = SumPattern(map(SimplePattern, options.patterns))
 
311
        else:
 
312
            self.patterns = SimplePattern("*")
 
313
        self.options = options
 
314
 
 
315
    def wrap_output(self, output):
 
316
        """Wraps an output stream (e.g. sys.stdout) from options."""
 
317
 
 
318
        if self.options.show_duration:
 
319
            output = TimestampOutput(output)
 
320
        if self.options.buffer_output:
 
321
            # We buffer output so we can order it for human readable output
 
322
            output = OrderedOutput(output)
 
323
 
 
324
        results = ConsoleOutput(output=output,
 
325
                                show_tracebacks=self.options.show_tracebacks,
 
326
                                show_duration=self.options.show_duration,
 
327
                                verbose=self.options.verbose)
 
328
        if not self.options.dry_run:
 
329
            results = FailureCountingResultWrapper(results)
 
330
 
 
331
        self.output = output
 
332
        self.results = results
 
333
 
 
334
    def load_descriptions(self):
 
335
        """Pre-load YAML checks file into a descriptions property."""
 
336
 
 
337
        with open(self.options.config_file) as f:
 
338
            self.descriptions = yaml.load(f)
 
339
 
 
340
    def run(self):
 
341
        """Run/validate/dry-run the given command with options."""
 
342
 
 
343
        checks = build_checks(self.descriptions,
 
344
                              self.options.connect_timeout,
 
345
                              self.options.include_tags,
 
346
                              self.options.exclude_tags,
 
347
                              self.options.dry_run)
 
348
 
 
349
        if not self.options.validate:
 
350
            if not self.options.dry_run:
 
351
                load_tls_certs(self.options.cacerts_path)
 
352
 
 
353
            self.setup_reactor()
 
354
            reactor.callWhenRunning(run_checks, checks, self.patterns,
 
355
                                    self.results)
 
356
            reactor.run()
 
357
 
 
358
            # Flush output, this really only has an effect when running
 
359
            # buffered output
 
360
            self.output.flush()
 
361
 
 
362
            if not self.options.dry_run and self.results.any_failed():
 
363
                return 2
 
364
 
 
365
        return 0
 
366
 
 
367
 
 
368
def parse_version_arg():
 
369
    """Manually check for --version in args and output version info.
 
370
 
 
371
    We need to do this early because ArgumentParser won't let us mix
 
372
    and match non-default positional argument with a flag argument.
 
373
    """
201
374
    if '--version' in sys.argv:
202
375
        sys.stdout.write('conn-check {}\n'.format(get_version_string()))
 
376
        return True
 
377
 
 
378
 
 
379
def run(*args):
 
380
    if parse_version_arg():
203
381
        return 0
204
382
 
205
 
    parser = NagiosCompatibleArgsParser()
206
 
    parser.add_argument("config_file",
207
 
                        help="Config file specifying the checks to run.")
208
 
    parser.add_argument("patterns", nargs='*',
209
 
                        help="Patterns to filter the checks.")
210
 
    parser.add_argument("-v", "--verbose", dest="verbose",
211
 
                        action="store_true", default=False,
212
 
                        help="Show additional status")
213
 
    parser.add_argument("-d", "--duration", dest="show_duration",
214
 
                        action="store_true", default=False,
215
 
                        help="Show duration")
216
 
    parser.add_argument("-t", "--tracebacks", dest="show_tracebacks",
217
 
                        action="store_true", default=False,
218
 
                        help="Show tracebacks on failure")
219
 
    parser.add_argument("--validate", dest="validate",
220
 
                        action="store_true", default=False,
221
 
                        help="Only validate the config file, don't run checks.")
222
 
    parser.add_argument("--version", dest="print_version",
223
 
                        action="store_true", default=False,
224
 
                        help="Print the currently installed version.")
225
 
    parser.add_argument("--tls-certs-path", dest="cacerts_path",
226
 
                        action="store", default="/etc/ssl/certs/",
227
 
                        help="Path to TLS CA certificates.")
228
 
    parser.add_argument("--max-timeout", dest="max_timeout", type=float,
229
 
                        action="store", help="Maximum execution time.")
230
 
    parser.add_argument("--connect-timeout", dest="connect_timeout",
231
 
                        action="store", default=10, type=float,
232
 
                        help="Network connection timeout.")
233
 
    parser.add_argument("-U", "--unbuffered-output", dest="buffer_output",
234
 
                        action="store_false", default=True,
235
 
                        help="Don't buffer output, write to STDOUT right "
236
 
                             "away.")
237
 
    group = parser.add_mutually_exclusive_group()
238
 
    group.add_argument("--include-tags", dest="include_tags",
239
 
                       action="store", default="",
240
 
                       help="Comma separated list of tags to include.")
241
 
    group.add_argument("--exclude-tags", dest="exclude_tags",
242
 
                       action="store", default="",
243
 
                       help="Comma separated list of tags to exclude.")
244
 
    options = parser.parse_args(list(args))
245
 
 
246
 
    load_tls_certs(options.cacerts_path)
247
 
 
248
 
    if options.patterns:
249
 
        pattern = SumPattern(map(SimplePattern, options.patterns))
250
 
    else:
251
 
        pattern = SimplePattern("*")
252
 
 
253
 
    def make_daemon_thread(*args, **kw):
254
 
        """Create a daemon thread."""
255
 
        thread = Thread(*args, **kw)
256
 
        thread.daemon = True
257
 
        return thread
258
 
 
259
 
    threadpool = ThreadPool(minthreads=1)
260
 
    threadpool.threadFactory = make_daemon_thread
261
 
    reactor.threadpool = threadpool
262
 
    reactor.callWhenRunning(threadpool.start)
263
 
 
264
 
    output = sys.stdout
265
 
 
266
 
    if options.show_duration:
267
 
        output = TimestampOutput(output)
268
 
 
269
 
    if options.buffer_output:
270
 
        # We buffer output so we can order it for human readable output
271
 
        output = OrderedOutput(output)
272
 
 
273
 
    include = options.include_tags.split(',') if options.include_tags else []
274
 
    exclude = options.exclude_tags.split(',') if options.exclude_tags  else []
275
 
 
276
 
    results = ConsoleOutput(output=output,
277
 
                            show_tracebacks=options.show_tracebacks,
278
 
                            show_duration=options.show_duration,
279
 
                            verbose=options.verbose)
280
 
    results = FailureCountingResultWrapper(results)
281
 
    with open(options.config_file) as f:
282
 
        descriptions = yaml.load(f)
283
 
 
284
 
    checks = build_checks(descriptions, options.connect_timeout, include, exclude)
285
 
 
286
 
    if options.max_timeout is not None:
287
 
        def terminator():
288
 
            # Hasta la vista, twisted
289
 
            reactor.stop()
290
 
            print('Maximum timeout reached: {}s'.format(options.max_timeout))
291
 
 
292
 
        reactor.callLater(options.max_timeout, terminator)
293
 
 
294
 
    if not options.validate:
295
 
        reactor.callWhenRunning(run_checks, checks, pattern, results)
296
 
 
297
 
        reactor.run()
298
 
 
299
 
        # Flush output, this really only has an effect when running buffered
300
 
        # output
301
 
        output.flush()
302
 
 
303
 
        if results.any_failed():
304
 
            return 2
305
 
        else:
306
 
            return 0
307
 
 
308
 
 
309
 
def run():
310
 
    exit(main(*sys.argv[1:]))
 
383
    cmd = Command(args)
 
384
    return cmd.run()
 
385
 
 
386
 
 
387
def main():
 
388
    sys.exit(run(*sys.argv[1:]))
311
389
 
312
390
 
313
391
if __name__ == '__main__':
314
 
    run()
315
 
 
 
392
    main()