Completed
Pull Request — master (#908)
by
unknown
12:15
created

exabgp.application.healthcheck.withdraw_ips()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nop 1
dl 0
loc 7
rs 10
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
        sys.stdin.readline()
367
368
def remove_ips(ips, label, sudo=False):
369
    """Remove added IP on loopback interface"""
370
    existing = set(loopback_ips(label))
371
372
    # Get intersection of IPs (ips setup, and IPs configured by ExaBGP)
373
    toremove = set([ip_network(ip) for net in ips for ip in net]) | existing
374
    for ip in toremove:
375
        logger.debug("Remove loopback IP address %s", ip)
376
        with open(os.devnull, "w") as fnull:
377
            cmd = ["ip", "address", "delete", str(ip), "dev", "lo"]
378
            if sudo:
379
                cmd.insert(0, "sudo")
380
            if label:
381
                cmd += ["label", "lo:{0}".format(label)]
382
            try:
383
                subprocess.check_call(
384
                    cmd, stdout=fnull, stderr=fnull)
385
            except subprocess.CalledProcessError:
386
                logger.warn("Unable to remove loopback IP address %s - is \
387
                    healthcheck running as root?", str(ip))
388
389
390
def drop_privileges(user, group):
391
    """Drop privileges to specified user and group"""
392
    if group is not None:
393
        import grp
394
        gid = grp.getgrnam(group).gr_gid
395
        logger.debug("Dropping privileges to group {0}/{1}".format(group, gid))
396
        try:
397
            os.setresgid(gid, gid, gid)
398
        except AttributeError:
399
            os.setregid(gid, gid)
400
    if user is not None:
401
        import pwd
402
        uid = pwd.getpwnam(user).pw_uid
403
        logger.debug("Dropping privileges to user {0}/{1}".format(user, uid))
404
        try:
405
            os.setresuid(uid, uid, uid)
406
        except AttributeError:
407
            os.setreuid(uid, uid)
408
409
410
def check(cmd, timeout):
411
    """Check the return code of the given command.
412
413
    :param cmd: command to execute. If :keyword:`None`, no command is executed.
414
    :param timeout: how much time we should wait for command completion.
415
    :return: :keyword:`True` if the command was successful or
416
             :keyword:`False` if not or if the timeout was triggered.
417
    """
418
    if cmd is None:
419
        return True
420
421
    class Alarm(Exception):
422
        """Exception to signal an alarm condition."""
423
        pass
424
425
    def alarm_handler(number, frame):  # pylint: disable=W0613
426
        """Handle SIGALRM signal."""
427
        raise Alarm()
0 ignored issues
show
introduced by
The variable Alarm does not seem to be defined for all execution paths.
Loading history...
428
429
    logger.debug("Checking command %s", repr(cmd))
430
    p = subprocess.Popen(cmd, shell=True,
431
                         stdout=subprocess.PIPE,
432
                         stderr=subprocess.STDOUT,
433
                         preexec_fn=os.setpgrp)
434
    if timeout:
435
        signal.signal(signal.SIGALRM, alarm_handler)
436
        signal.alarm(timeout)
437
    try:
438
        stdout = None
439
        stdout, _ = p.communicate()
440
        if timeout:
441
            signal.alarm(0)
442
        if p.returncode != 0:
443
            logger.warn("Check command was unsuccessful: %s",
444
                        p.returncode)
445
            if stdout.strip():
446
                logger.info("Output of check command: %s", stdout)
447
            return False
448
        logger.debug(
449
            "Command was executed successfully %s %s", p.returncode, stdout)
450
        return True
451
    except Alarm:
452
        logger.warn("Timeout (%s) while running check command %s",
453
                    timeout, cmd)
454
        os.killpg(p.pid, signal.SIGKILL)
455
        return False
456
457
458
def loop(options):
459
    """Main loop."""
460
    states = enum(
461
        "INIT",                 # Initial state
462
        "DISABLED",             # Disabled state
463
        "RISING",               # Checks are currently succeeding.
464
        "FALLING",              # Checks are currently failing.
465
        "UP",                   # Service is considered as up.
466
        "DOWN",                 # Service is considered as down.
467
    )
468
469
    def exabgp(target):
470
        """Communicate new state to ExaBGP"""
471
        if target not in (states.UP, states.DOWN, states.DISABLED):
472
            return
473
        # dynamic ip management. When the service fail, remove the loopback
474
        if target in (states.DOWN, states.DISABLED) and options.ip_dynamic:
475
            logger.info("service down, deleting loopback ips")
476
            remove_ips(options.ips, options.label, options.sudo)
477
        # if ips was deleted with dyn ip, re-setup them
478
        if target == states.UP and options.ip_dynamic:
479
            logger.info("service up, restoring loopback ips")
480
            setup_ips(options.ips, options.label, options.sudo)
481
482
        logger.info("send announces for %s state to ExaBGP", target)
483
        metric = vars(options).get("{0}_metric".format(str(target).lower()))
484
        for ip in options.ips:
485
            if options.withdraw_on_down:
486
                command = "announce" if target is states.UP else "withdraw"
487
            else:
488
                command = "announce"
489
            announce = "route {0} next-hop {1}".format(
490
                str(ip),
491
                options.next_hop or "self")
492
493
            if command == "announce":
494
                announce = "{0} med {1}".format(announce, metric)
495
                if options.local_preference >= 0:
496
                    announce = "{0} local-preference {1}".format(announce, options.local_preference)
497
                if options.community or options.disabled_community:
498
                    community = options.community
499
                    if target in (states.DOWN, states.DISABLED):
500
                        if options.disabled_community:
501
                            community = options.disabled_community
502
                    if community:
503
                        announce = "{0} community [ {1} ]".format(
504
                            announce, community)
505
                if options.extended_community:
506
                    announce = "{0} extended-community [ {1} ]".format(
507
                        announce,
508
                        options.extended_community)
509
                if options.large_community:
510
                    announce = "{0} large-community [ {1} ]".format(
511
                        announce,
512
                        options.large_community)
513
                if options.as_path:
514
                    announce = "{0} as-path [ {1} ]".format(
515
                        announce,
516
                        options.as_path)
517
518
            metric += options.increase
519
520
            # Send and flush command
521
            logger.debug("exabgp: {0} {1}".format(command, announce))
522
            print("{0} {1}".format(command, announce))
523
            sys.stdout.flush()
524
            
525
            # Wait for confirmation from ExaBGP if expected
526
            if options.no_ack:
527
                continue
528
            # if the program is not ran manually, do not read the input
529
            if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
530
                continue
531
            sys.stdin.readline()
532
533
    def trigger(target):
534
        """Trigger a state change and execute the appropriate commands"""
535
        # Shortcut for RISING->UP and FALLING->UP
536
        if target == states.RISING and options.rise <= 1:
537
            target = states.UP
538
        elif target == states.FALLING and options.fall <= 1:
539
            target = states.DOWN
540
541
        # Log and execute commands
542
        logger.debug("Transition to %s", str(target))
543
        cmds = []
544
        cmds.extend(vars(options).get("{0}_execute".format(
545
            str(target).lower()), []) or [])
546
        cmds.extend(vars(options).get("execute", []) or [])
547
        for cmd in cmds:
548
            logger.debug("Transition to %s, execute `%s`",
549
                         str(target), cmd)
550
            env = os.environ.copy()
551
            env.update({"STATE": str(target)})
552
            with open(os.devnull, "w") as fnull:
553
                subprocess.call(
554
                    cmd, shell=True, stdout=fnull, stderr=fnull, env=env)
555
556
        return target
557
558
    def one(checks, state):
559
        """Execute one loop iteration."""
560
        disabled = (options.disable is not None and
561
                    os.path.exists(options.disable))
562
        successful = disabled or check(options.command, options.timeout)
563
        # FSM
564
        if state != states.DISABLED and disabled:
565
            state = trigger(states.DISABLED)
566
        elif state == states.INIT:
567
            if successful and options.rise <= 1:
568
                state = trigger(states.UP)
569
            elif successful:
570
                state = trigger(states.RISING)
571
                checks = 1
572
            else:
573
                state = trigger(states.FALLING)
574
                checks = 1
575
        elif state == states.DISABLED:
576
            if not disabled:
577
                state = trigger(states.INIT)
578
        elif state == states.RISING:
579
            if successful:
580
                checks += 1
581
                if checks >= options.rise:
582
                    state = trigger(states.UP)
583
            else:
584
                state = trigger(states.FALLING)
585
                checks = 1
586
        elif state == states.FALLING:
587
            if not successful:
588
                checks += 1
589
                if checks >= options.fall:
590
                    state = trigger(states.DOWN)
591
            else:
592
                state = trigger(states.RISING)
593
                checks = 1
594
        elif state == states.UP:
595
            if not successful:
596
                state = trigger(states.FALLING)
597
                checks = 1
598
        elif state == states.DOWN:
599
            if successful:
600
                state = trigger(states.RISING)
601
                checks = 1
602
        else:
603
            raise ValueError("Unhandled state: {0}".format(str(state)))
604
605
        # Send announces. We announce them on a regular basis in case
606
        # we lose connection with a peer.
607
        exabgp(state)
608
        return checks, state
609
610
    checks = 0
611
    state = states.INIT
612
    while True:
613
        checks, state = one(checks, state)
614
615
        # How much we should sleep?
616
        if state in (states.FALLING, states.RISING):
617
            time.sleep(options.fast)
618
        else:
619
            time.sleep(options.interval)
620
621
622
def main():
623
    """Entry point."""
624
    options = parse()
625
    setup_logging(options.debug, options.silent, options.name,
626
                  options.syslog_facility, not options.no_syslog)
627
    if options.pid:
628
        options.pid.write("{0}\n".format(os.getpid()))
629
        options.pid.close()
630
    try:
631
        # Setup IP to use
632
        options.ips = options.ips or loopback_ips(options.label)
633
        if not options.ips:
634
            logger.error("No IP found")
635
            sys.exit(1)
636
        if options.ip_setup:
637
            setup_ips(options.ips, options.label, options.sudo)
638
        drop_privileges(options.user, options.group)
639
640
        # Parse defined networks into a list of IPs for advertisement
641
        if options.deaggregate_networks:
642
            options.ips = [ip_network(ip) for net in options.ips for ip in net]
643
644
        options.ips = collections.deque(options.ips)
645
        options.ips.rotate(-options.start_ip)
646
        options.ips = list(options.ips)
647
        # Main loop
648
        loop(options)
649
    except Exception as e:  # pylint: disable=W0703
650
        logger.exception("Uncaught exception: %s", e)
651
        sys.exit(1)
652
653
654
if __name__ == "__main__":
655
    main()
656