Passed
Pull Request — master (#908)
by
unknown
12:52
created

exabgp.application.healthcheck.remove_ips()   B

Complexity

Conditions 6

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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