exabgp.application.healthcheck   F
last analyzed

Complexity

Total Complexity 114

Size/Duplication

Total Lines 554
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 383
dl 0
loc 554
rs 2
c 0
b 0
f 0
wmc 114

13 Functions

Rating   Name   Duplication   Size   Complexity  
A enum() 0 3 1
B remove_ips() 0 21 6
B setargs() 0 50 1
C setup_ips() 0 20 9
C loopback_ips() 0 44 11
A parse() 0 22 2
A drop_privileges() 0 20 5
A loopback() 0 5 2
B main() 0 29 6
B setup_logging() 0 33 8
F loop() 0 172 55
A cmdline() 0 3 1
B check() 0 41 7

How to fix   Complexity   

Complexity

Complex classes like exabgp.application.healthcheck often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python3
2
3
"""Healthchecker for exabgp.
4
5
This program is to be used as a process for exabgp. It will announce
6
some VIP depending on the state of a check whose a third-party program
7
wrapped by this program.
8
9
To use, declare this program as a process in your
10
:file:`/etc/exabgp/exabgp.conf`::
11
12
    neighbor 192.0.2.1 {
13
       router-id 192.0.2.2;
14
       local-as 64496;
15
       peer-as 64497;
16
    }
17
    process watch-haproxy {
18
       run python -m exabgp healthcheck --cmd "curl -sf http://127.0.0.1/healthcheck" --label haproxy;
19
    }
20
    process watch-mysql {
21
       run python -m exabgp healthcheck --cmd "mysql -u check -e 'SELECT 1'" --label mysql;
22
    }
23
24
Use :option:`--help` to get options accepted by this program. A
25
configuration file is also possible. Such a configuration file looks
26
like this::
27
28
     debug
29
     name = haproxy
30
     interval = 10
31
     fast-interval = 1
32
     command = curl -sf http://127.0.0.1/healthcheck
33
34
The left-part of each line is the corresponding long option.
35
36
When using label for loopback selection, the provided value should
37
match the beginning of the label without the interface prefix. In the
38
example above, this means that you should have addresses on lo
39
labelled ``lo:haproxy1``, ``lo:haproxy2``, etc.
40
41
"""
42
43
import sys
44
import os
45
import subprocess
46
import re
47
import logging
48
import logging.handlers
49
import argparse
50
import signal
51
import time
52
import collections
53
54
from ipaddress import ip_network
55
from ipaddress import ip_address
56
57
58
logger = logging.getLogger("healthcheck")
59
60
61
def enum(*sequential):
62
    """Create a simple enumeration."""
63
    return type(str("Enum"), (), dict(zip(sequential, sequential)))
64
65
66
def setargs(parser):
67
    # fmt: off
68
    g = parser.add_mutually_exclusive_group()
69
    g.add_argument("--silent", "-s", action="store_true", default=False, help="don't log to console")
70
    g.add_argument("--syslog-facility", "-sF", metavar="FACILITY", nargs='?', const="daemon", default="daemon", help="log to syslog using FACILITY, default FACILITY is daemon")
71
    g.add_argument("--no-syslog", action="store_true", help="disable syslog logging")
72
73
    parser.add_argument("--debug", "-d", action="store_true", default=False, help="enable debugging")
74
    parser.add_argument("--no-ack", "-a", action="store_true", default=False, help="set for exabgp 3.4 or 4.x when exabgp.api.ack=false")
75
    parser.add_argument("--sudo", action="store_true", default=False, help="use sudo to setup ip addresses")
76
    parser.add_argument("--name", "-n", metavar="NAME", help="name for this healthchecker")
77
    parser.add_argument("--config", "-F", metavar="FILE", type=open, help="read configuration from a file")
78
    parser.add_argument("--pid", "-p", metavar="FILE", type=argparse.FileType('w'), help="write PID to the provided file")
79
    parser.add_argument("--user", metavar="USER", help="set user after setting loopback addresses")
80
    parser.add_argument("--group", metavar="GROUP", help="set group after setting loopback addresses")
81
82
    g = parser.add_argument_group("checking healthiness")
83
    g.add_argument("--interval", "-i", metavar='N', default=5, type=float, help="wait N seconds between each healthcheck (zero to exit after first announcement)")
84
    g.add_argument("--fast-interval", "-f", metavar='N', default=1, type=float, dest="fast", help="when a state change is about to occur, wait N seconds between each healthcheck")
85
    g.add_argument("--timeout", "-t", metavar='N', default=5, type=int, help="wait N seconds for the check command to execute")
86
    g.add_argument("--rise", metavar='N', default=3, type=int, help="check N times before considering the service up")
87
    g.add_argument("--fall", metavar='N', default=3, type=int, help="check N times before considering the service down")
88
    g.add_argument("--disable", metavar='FILE', type=str, help="if FILE exists, the service is considered disabled")
89
    g.add_argument("--command", "--cmd", "-c", metavar='CMD', type=str, help="command to use for healthcheck")
90
91
    g = parser.add_argument_group("advertising options")
92
    g.add_argument("--next-hop", "-N", metavar='IP', type=ip_address, help="self IP address to use as next hop")
93
    g.add_argument("--ip", metavar='IP', type=ip_network, dest="ips", action="append", help="advertise this IP address or network (CIDR notation)")
94
    g.add_argument("--local-preference", metavar='P', type=int, default=-1, help="advertise with local preference P")
95
    g.add_argument("--deaggregate-networks", dest="deaggregate_networks", action="store_true", help="Deaggregate Networks specified in --ip")
96
    g.add_argument("--no-ip-setup", action="store_false", dest="ip_setup", help="don't setup missing IP addresses")
97
    g.add_argument("--dynamic-ip-setup", default=False, action="store_true", dest="ip_dynamic", help="delete existing loopback ips on state down and " "disabled, then restore loopback when up")
98
    g.add_argument("--label", default=None, help="use the provided label to match loopback addresses")
99
    g.add_argument("--start-ip", metavar='N', type=int, default=0, help="index of the first IP in the list of IP addresses")
100
    g.add_argument("--up-metric", metavar='M', type=int, default=100, help="first IP get the metric M when the service is up")
101
    g.add_argument("--down-metric", metavar='M', type=int, default=1000, help="first IP get the metric M when the service is down")
102
    g.add_argument("--disabled-metric", metavar='M', type=int, default=500, help="first IP get the metric M when the service is disabled")
103
    g.add_argument("--increase", metavar='M', type=int, default=1, help="for each additional IP address, increase metric value by M")
104
    g.add_argument("--community", metavar="COMMUNITY", type=str, default=None, help="announce IPs with the supplied community")
105
    g.add_argument("--extended-community", metavar="EXTENDEDCOMMUNITY", type=str, default=None, help="announce IPs with the supplied extended community")
106
    g.add_argument("--large-community", metavar="LARGECOMMUNITY", type=str, default=None, help="announce IPs with the supplied large community")
107
    g.add_argument("--disabled-community", metavar="DISABLEDCOMMUNITY", type=str, default=None, help="announce IPs with the supplied community when disabled")
108
    g.add_argument("--as-path", metavar="ASPATH", type=str, default=None, help="announce IPs with the supplied as-path")
109
    g.add_argument("--withdraw-on-down", action="store_true", help="Instead of increasing the metric on health failure, withdraw the route")
110
111
    g = parser.add_argument_group("reporting")
112
    g.add_argument("--execute", metavar='CMD', type=str, action="append", help="execute CMD on state change")
113
    g.add_argument("--up-execute", metavar='CMD', type=str, action="append", help="execute CMD when the service becomes available")
114
    g.add_argument("--down-execute", metavar='CMD', type=str, action="append", help="execute CMD when the service becomes unavailable")
115
    g.add_argument("--disabled-execute", metavar='CMD', type=str, action="append", help="execute CMD when the service is disabled")
116
    # fmt: on
117
118
119
def parse():
120
    """Parse arguments"""
121
    formatter = argparse.RawDescriptionHelpFormatter
122
    parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__, formatter_class=formatter)
123
    setargs(parser)
124
125
    options = parser.parse_args()
126
    if options.config is not None:
127
        # A configuration file has been provided. Read each line and
128
        # build an equivalent command line.
129
        args = sum(
130
            [
131
                "--{0}".format(l.strip()).split("=", 1)
132
                for l in options.config.readlines()
133
                if not l.strip().startswith("#") and l.strip()
134
            ],
135
            [],
136
        )
137
        args = [x.strip() for x in args]
138
        args.extend(sys.argv[1:])
139
        options = parser.parse_args(args)
140
    return options
141
142
143
def setup_logging(debug, silent, name, syslog_facility, syslog):
144
    """Setup logger"""
145
146
    def syslog_address():
147
        """Return a sensible syslog address"""
148
        if sys.platform == "darwin":
149
            return "/var/run/syslog"
150
        if sys.platform.startswith("freebsd"):
151
            return "/var/run/log"
152
        if sys.platform.startswith("netbsd"):
153
            return "/var/run/log"
154
        if sys.platform.startswith("linux"):
155
            return "/dev/log"
156
        raise EnvironmentError("Unable to guess syslog address for your " "platform, try to disable syslog")
157
158
    logger.setLevel(debug and logging.DEBUG or logging.INFO)
159
    enable_syslog = syslog and not debug
160
    # To syslog
161
    if enable_syslog:
162
        facility = getattr(logging.handlers.SysLogHandler, "LOG_{0}".format(syslog_facility.upper()))
163
        sh = logging.handlers.SysLogHandler(address=str(syslog_address()), facility=facility)
164
        if name:
165
            healthcheck_name = "healthcheck-{0}".format(name)
166
        else:
167
            healthcheck_name = "healthcheck"
168
        sh.setFormatter(logging.Formatter("{0}[{1}]: %(message)s".format(healthcheck_name, os.getpid())))
169
        logger.addHandler(sh)
170
    # To console
171
    toconsole = hasattr(sys.stderr, "isatty") and sys.stderr.isatty() and not silent  # pylint: disable=E1101
172
    if toconsole:
173
        ch = logging.StreamHandler()
174
        ch.setFormatter(logging.Formatter("%(levelname)s[%(name)s] %(message)s"))
175
        logger.addHandler(ch)
176
177
178
def loopback_ips(label, label_only):
179
    """Retrieve loopback IP addresses"""
180
    logger.debug("Retrieve loopback IP addresses")
181
    addresses = []
182
183
    if sys.platform.startswith("linux"):
184
        # Use "ip" (ifconfig is not able to see all addresses)
185
        ipre = re.compile(r"^(?P<index>\d+):\s+(?P<name>\S+)\s+inet6?\s+" r"(?P<ip>[\da-f.:]+)/(?P<mask>\d+)\s+.*")
186
        labelre = re.compile(r".*\s+lo:(?P<label>\S+).*")
187
        cmd = subprocess.Popen("/sbin/ip -o address show dev lo".split(), shell=False, stdout=subprocess.PIPE)
188
    else:
189
        # Try with ifconfig
190
        ipre = re.compile(
191
            r"^inet6?\s+(alias\s+)?(?P<ip>[\da-f.:]+)\s+"
192
            r"(?:netmask 0x(?P<netmask>[0-9a-f]+)|"
193
            r"prefixlen (?P<mask>\d+)).*"
194
        )
195
        cmd = subprocess.Popen("/sbin/ifconfig lo0".split(), shell=False, stdout=subprocess.PIPE)
196
        labelre = re.compile(r"")
197
    for line in cmd.stdout:
198
        line = line.decode("ascii", "ignore").strip()
199
        mo = ipre.match(line)
200
        if not mo:
201
            continue
202
        if mo.group("mask"):
203
            mask = int(mo.group("mask"))
204
        else:
205
            mask = bin(int(mo.group("netmask"), 16)).count("1")
206
        try:
207
            ip = ip_network("{0}/{1}".format(mo.group("ip"), mask))
208
        except ValueError:
209
            continue
210
        if not ip.is_loopback:
211
            if label:
212
                lmo = labelre.match(line)
213
                if not lmo:
214
                    continue
215
                if lmo.groupdict().get("label", "").startswith(label):
216
                    addresses.append(ip)
217
            elif not label_only:
218
                addresses.append(ip)
219
220
    logger.debug("Loopback addresses: %s", addresses)
221
    return addresses
222
223
224
def loopback():
225
    lo = "lo0"
226
    if sys.platform.startswith("linux"):
227
        lo = "lo"
228
    return lo
229
230
231
def setup_ips(ips, label, sudo=False):
232
    """Setup missing IP on loopback interface"""
233
234
    existing = set(loopback_ips(label, False))
235
    toadd = set([ip_network(ip) for net in ips for ip in net]) - existing
236
    for ip in toadd:
237
        logger.debug("Setup loopback IP address %s", ip)
238
        with open(os.devnull, "w") as fnull:
239
            cmd = ["ip", "address", "add", str(ip), "dev", loopback()]
240
            if sudo:
241
                cmd.insert(0, "sudo")
242
            if label:
243
                cmd += ["label", "{0}:{1}".format(loopback(), label)]
244
                try:
245
                    subprocess.check_call(cmd, stdout=fnull, stderr=fnull)
246
                except subprocess.CalledProcessError as e:
247
                    # the IP address is already setup, ignoring
248
                    if cmd[0] == "ip" and cmd[2] == "add" and e.returncode == 2:
249
                        continue
250
                    raise e
251
252
253
def remove_ips(ips, label, sudo=False):
254
    """Remove added IP on loopback interface"""
255
    existing = set(loopback_ips(label, True))
256
257
    # Get intersection of IPs (ips setup, and IPs configured by ExaBGP)
258
    toremove = set([ip_network(ip) for net in ips for ip in net]) & existing
259
    for ip in toremove:
260
        logger.debug("Remove loopback IP address %s", ip)
261
        with open(os.devnull, "w") as fnull:
262
            cmd = ["ip", "address", "delete", str(ip), "dev", loopback()]
263
            if sudo:
264
                cmd.insert(0, "sudo")
265
            if label:
266
                cmd += ["label", "{0}:{1}".format(loopback(), label)]
267
            try:
268
                subprocess.check_call(cmd, stdout=fnull, stderr=fnull)
269
            except subprocess.CalledProcessError:
270
                logger.warn(
271
                    "Unable to remove loopback IP address %s - is \
272
                    healthcheck running as root?",
273
                    str(ip),
274
                )
275
276
277
def drop_privileges(user, group):
278
    """Drop privileges to specified user and group"""
279
    if group is not None:
280
        import grp
281
282
        gid = grp.getgrnam(group).gr_gid
283
        logger.debug("Dropping privileges to group {0}/{1}".format(group, gid))
284
        try:
285
            os.setresgid(gid, gid, gid)
286
        except AttributeError:
287
            os.setregid(gid, gid)
288
    if user is not None:
289
        import pwd
290
291
        uid = pwd.getpwnam(user).pw_uid
292
        logger.debug("Dropping privileges to user {0}/{1}".format(user, uid))
293
        try:
294
            os.setresuid(uid, uid, uid)
295
        except AttributeError:
296
            os.setreuid(uid, uid)
297
298
299
def check(cmd, timeout):
300
    """Check the return code of the given command.
301
302
    :param cmd: command to execute. If :keyword:`None`, no command is executed.
303
    :param timeout: how much time we should wait for command completion.
304
    :return: :keyword:`True` if the command was successful or
305
             :keyword:`False` if not or if the timeout was triggered.
306
    """
307
    if cmd is None:
308
        return True
309
310
    class Alarm(Exception):
311
        """Exception to signal an alarm condition."""
312
313
        pass
314
315
    def alarm_handler(number, frame):  # pylint: disable=W0613
316
        """Handle SIGALRM signal."""
317
        raise Alarm()
0 ignored issues
show
introduced by
The variable Alarm does not seem to be defined for all execution paths.
Loading history...
318
319
    logger.debug("Checking command %s", repr(cmd))
320
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, preexec_fn=os.setpgrp)
321
    if timeout:
322
        signal.signal(signal.SIGALRM, alarm_handler)
323
        signal.alarm(timeout)
324
    try:
325
        stdout = None
326
        stdout, _ = p.communicate()
327
        if timeout:
328
            signal.alarm(0)
329
        if p.returncode != 0:
330
            logger.warn("Check command was unsuccessful: %s", p.returncode)
331
            if stdout.strip():
332
                logger.info("Output of check command: %s", stdout)
333
            return False
334
        logger.debug("Command was executed successfully %s %s", p.returncode, stdout)
335
        return True
336
    except Alarm:
337
        logger.warn("Timeout (%s) while running check command %s", timeout, cmd)
338
        os.killpg(p.pid, signal.SIGKILL)
339
        return False
340
341
342
def loop(options):
343
    """Main loop."""
344
    states = enum(
345
        "INIT",  # Initial state
346
        "DISABLED",  # Disabled state
347
        "RISING",  # Checks are currently succeeding.
348
        "FALLING",  # Checks are currently failing.
349
        "UP",  # Service is considered as up.
350
        "DOWN",  # Service is considered as down.
351
        "EXIT",  # Exit state
352
        "END",  # End state, exiting but without removing loopback and/or announced routes
353
    )
354
355
    def exabgp(target):
356
        """Communicate new state to ExaBGP"""
357
        if target not in (states.UP, states.DOWN, states.DISABLED, states.EXIT, states.END):
358
            return
359
        if target in (states.END,):
360
            return
361
        # dynamic ip management. When the service fail, remove the loopback
362
        if target in (states.EXIT,) and (options.ip_dynamic or options.ip_setup):
363
            logger.info("exiting, deleting loopback ips")
364
            remove_ips(options.ips, options.label, options.sudo)
365
        # dynamic ip management. When the service fail, remove the loopback
366
        if target in (states.DOWN, states.DISABLED) and options.ip_dynamic:
367
            logger.info("service down, deleting loopback ips")
368
            remove_ips(options.ips, options.label, options.sudo)
369
        # if ips was deleted with dyn ip, re-setup them
370
        if target == states.UP and options.ip_dynamic:
371
            logger.info("service up, restoring loopback ips")
372
            setup_ips(options.ips, options.label, options.sudo)
373
374
        logger.info("send announces for %s state to ExaBGP", target)
375
        metric = vars(options).get("{0}_metric".format(str(target).lower()), 0)
376
        for ip in options.ips:
377
            if options.withdraw_on_down or target is states.EXIT:
378
                command = "announce" if target is states.UP else "withdraw"
379
            else:
380
                command = "announce"
381
            announce = "route {0} next-hop {1}".format(str(ip), options.next_hop or "self")
382
383
            if command == "announce":
384
                announce = "{0} med {1}".format(announce, metric)
385
                if options.local_preference >= 0:
386
                    announce = "{0} local-preference {1}".format(announce, options.local_preference)
387
                if options.community or options.disabled_community:
388
                    community = options.community
389
                    if target in (states.DOWN, states.DISABLED):
390
                        if options.disabled_community:
391
                            community = options.disabled_community
392
                    if community:
393
                        announce = "{0} community [ {1} ]".format(announce, community)
394
                if options.extended_community:
395
                    announce = "{0} extended-community [ {1} ]".format(announce, options.extended_community)
396
                if options.large_community:
397
                    announce = "{0} large-community [ {1} ]".format(announce, options.large_community)
398
                if options.as_path:
399
                    announce = "{0} as-path [ {1} ]".format(announce, options.as_path)
400
401
            metric += options.increase
402
403
            # Send and flush command
404
            logger.debug("exabgp: {0} {1}".format(command, announce))
405
            print("{0} {1}".format(command, announce))
406
            sys.stdout.flush()
407
408
            # Wait for confirmation from ExaBGP if expected
409
            if options.no_ack:
410
                continue
411
            # if the program is not ran manually, do not read the input
412
            if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
413
                continue
414
            sys.stdin.readline()
415
416
    def trigger(target):
417
        """Trigger a state change and execute the appropriate commands"""
418
        # Shortcut for RISING->UP and FALLING->UP
419
        if target == states.RISING and options.rise <= 1:
420
            target = states.UP
421
        elif target == states.FALLING and options.fall <= 1:
422
            target = states.DOWN
423
424
        # Log and execute commands
425
        logger.debug("Transition to %s", str(target))
426
        cmds = []
427
        cmds.extend(vars(options).get("{0}_execute".format(str(target).lower()), []) or [])
428
        cmds.extend(vars(options).get("execute", []) or [])
429
        for cmd in cmds:
430
            logger.debug("Transition to %s, execute `%s`", str(target), cmd)
431
            env = os.environ.copy()
432
            env.update({"STATE": str(target)})
433
            with open(os.devnull, "w") as fnull:
434
                subprocess.call(cmd, shell=True, stdout=fnull, stderr=fnull, env=env)
435
436
        return target
437
438
    def one(checks, state):
439
        """Execute one loop iteration."""
440
        disabled = options.disable is not None and os.path.exists(options.disable)
441
        successful = disabled or check(options.command, options.timeout)
442
        # FSM
443
        if state != states.DISABLED and disabled:
444
            state = trigger(states.DISABLED)
445
        elif state == states.INIT:
446
            if successful and options.rise <= 1:
447
                state = trigger(states.UP)
448
            elif successful:
449
                state = trigger(states.RISING)
450
                checks = 1
451
            else:
452
                state = trigger(states.FALLING)
453
                checks = 1
454
        elif state == states.DISABLED:
455
            if not disabled:
456
                state = trigger(states.INIT)
457
        elif state == states.RISING:
458
            if successful:
459
                checks += 1
460
                if checks >= options.rise:
461
                    state = trigger(states.UP)
462
            else:
463
                state = trigger(states.FALLING)
464
                checks = 1
465
        elif state == states.FALLING:
466
            if not successful:
467
                checks += 1
468
                if checks >= options.fall:
469
                    state = trigger(states.DOWN)
470
            else:
471
                state = trigger(states.RISING)
472
                checks = 1
473
        elif state == states.UP:
474
            if not successful:
475
                state = trigger(states.FALLING)
476
                checks = 1
477
        elif state == states.DOWN:
478
            if successful:
479
                state = trigger(states.RISING)
480
                checks = 1
481
        else:
482
            raise ValueError("Unhandled state: {0}".format(str(state)))
483
484
        # Send announces. We announce them on a regular basis in case
485
        # we lose connection with a peer and the adj-rib-out is disabled.
486
        exabgp(state)
487
        return checks, state
488
489
    checks = 0
490
    state = states.INIT
491
    # Do cleanups on SIGTERM
492
    def sigterm_handler(signum, frame):  # pylint: disable=W0612,W0613
493
        exabgp(states.EXIT)
494
        sys.exit(0)
495
496
    signal.signal(signal.SIGTERM, sigterm_handler)
497
498
    while True:
499
        checks, state = one(checks, state)
500
501
        try:
502
            # How much we should sleep?
503
            if state in (states.FALLING, states.RISING):
504
                time.sleep(options.fast)
505
            elif options.interval == 0:
506
                logger.info("interval set to zero, exiting after the announcement")
507
                exabgp(states.END)
508
                break
509
            else:
510
                time.sleep(options.interval)
511
        except KeyboardInterrupt:
512
            exabgp(states.EXIT)
513
            break
514
515
516
def cmdline(cmdarg):
517
    sys.argv = [f'{sys.argv[0]} {sys.argv[1]}'] + sys.argv[2:]
518
    main()
519
520
521
def main():
522
    """Entry point."""
523
    options = parse()
524
    setup_logging(options.debug, options.silent, options.name, options.syslog_facility, not options.no_syslog)
525
    if options.pid:
526
        options.pid.write("{0}\n".format(os.getpid()))
527
        options.pid.close()
528
    try:
529
        # Setup IP to use
530
        options.ips = options.ips or loopback_ips(options.label, False)
531
        if not options.ips:
532
            logger.error("No IP found")
533
            sys.exit(1)
534
        if options.ip_setup:
535
            setup_ips(options.ips, options.label, options.sudo)
536
        drop_privileges(options.user, options.group)
537
538
        # Parse defined networks into a list of IPs for advertisement
539
        if options.deaggregate_networks:
540
            options.ips = [ip_network(ip) for net in options.ips for ip in net]
541
542
        options.ips = collections.deque(options.ips)
543
        options.ips.rotate(-options.start_ip)
544
        options.ips = list(options.ips)
545
        # Main loop
546
        loop(options)
547
    except Exception as e:  # pylint: disable=W0703
548
        logger.exception("Uncaught exception: %s", e)
549
        sys.exit(1)
550
551
552
if __name__ == "__main__":
553
    main()
554