Test Failed
Push — master ( c6929c...db4166 )
by Thomas
10:57
created

exabgp.application.healthcheck   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 642
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 455
dl 0
loc 642
rs 2
c 0
b 0
f 0
wmc 112

11 Functions

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