dhcp_configurator.py 6.14 KB
Newer Older
1
#!/usr/bin/env python
2
from __future__ import absolute_import, division, print_function, unicode_literals
3 4 5 6 7 8
import etcd
import subprocess
import logging
import logging.handlers
import time
import argparse
9
from daemonize import Daemonize
10
import socket
11
import json
12
from nova_router import setupLogfile
13 14

CONF_FILE = "/etc/haproxy/haproxy.cfg"
15 16
APP = "nova_dhcp_configurator"
DESCRIPTION = "Generate dhcp server config based on neighbor list in etcd"
Morten Knutsen's avatar
Morten Knutsen committed
17 18 19
INPUT_RANGE_FILE = "/etc/dhcp/dhcp_range.json"
RANGES_FILE = "/etc/dhcp/generated_ranges.conf"
HOSTS_FILE = "/etc/dhcp/generated_hosts.conf"
20 21


22 23 24 25 26 27 28 29 30 31 32
def parse_ip(ip):
    result = 0
    for octet in ip.split("."):
        result = result * 256 + int(octet)
    return result


def format_ip(num):
    return ".".join(str(x) for x in (num >> 24, (num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff))


33
class Generator(object):
34
    def __init__(self, etcd_host, cert, key, cacert, logfile=None):
Morten Knutsen's avatar
Morten Knutsen committed
35
        self.etcd_client = etcd.Client(host=etcd_host, cert=(cert, key),
36
                                       ca_cert=cacert, protocol='https')
37
        if logfile:
38
            setupLogfile(logfile)
Sigmund Augdal's avatar
Sigmund Augdal committed
39

40
        logging.info("Process starting")
Sigmund Augdal's avatar
Sigmund Augdal committed
41

42 43
        dhcp_range = json.load(open(INPUT_RANGE_FILE))
        self.range = [parse_ip(x) for x in dhcp_range]
44 45

    def next_free(self, i, hosts):
46
        while format_ip(i) in hosts and i <= self.range[1]:
47 48 49 50 51 52
            i += 1
        if i > self.range[1]:
            return None
        return i

    def next_used(self, i, hosts):
53
        while format_ip(i) not in hosts and i <= self.range[1]:
54 55 56 57 58 59 60 61 62 63 64 65 66
            i += 1
        if i > self.range[1]:
            return None
        return i

    def format_host(self, ipaddress, mac):
        host = mac.replace(":", "")
        return """host instance{} {{
    hardware ethernet {};
    fixed-address {};
}}
""".format(host, mac, ipaddress)

67
    def generate_ranges(self, hosts):
68 69 70 71 72 73 74 75
        result = ""
        range_start = self.next_free(self.range[0], hosts)
        while range_start:
            next_host = self.next_used(range_start + 1, hosts)
            if next_host is None:
                range_end = range_start
            else:
                range_end = next_host - 1
76 77
            result += "range {} {};\n".format(format_ip(range_start),
                                              format_ip(range_end))
78
            range_start = self.next_free(range_end + 1, hosts)
79 80 81 82
        return result

    def generate_hosts(self, hosts):
        result = ""
83 84 85 86
        for ipaddress, mac in sorted(hosts.items(), key=lambda x: socket.inet_aton(x[0])):
            result += self.format_host(ipaddress, mac)
        return result

87
    def get_hosts(self):
88
        index = 0
89
        hosts = {}
90 91

        data = self.etcd_client.read("/nova/iaas/instances", recursive=True)
Sigmund Augdal's avatar
Sigmund Augdal committed
92
        index = int(data.etcd_index)  # pylint: disable=E1101
93
        for entry in data.children:
94
            if not entry.key.endswith("/ipv4"):
95
                continue
96
            mac = entry.key.split("/")[-2]
97
            try:
98
                ipaddress = entry.value  # pylint: disable=E1101
99
                hosts[ipaddress] = mac
100 101
            except KeyError:
                pass
102 103 104 105
        return hosts, index

    def generate_all(self):
        hosts, index = self.get_hosts()
106
        total_range = self.range[1] - self.range[0] + 1
Sigmund Augdal's avatar
Sigmund Augdal committed
107 108
        dynamic_hosts = [host for host in hosts if
                         parse_ip(host) >= self.range[0] and parse_ip(host) <= self.range[1]]
109 110
        if total_range - len(dynamic_hosts) < 2:
            logging.warn("Running very low on addresses. Forcing extra cleanup")
111 112 113 114
            # We're really close to running out of addresses, check if some VMs has been deleted
            subprocess.call("/etc/cron.hourly/clean_stale")
            # fetch hopefully shorter list of hosts again, then go on
            hosts, index = self.get_hosts()
115
        with open(RANGES_FILE, "w") as f:
116 117 118 119
            ranges = self.generate_ranges(hosts)
            if ranges == "":
                logging.error("We're all out of addresses!")
            f.write(ranges)
120 121
        with open(HOSTS_FILE, "w") as f:
            f.write(self.generate_hosts(hosts))
Sigmund Augdal's avatar
Sigmund Augdal committed
122
        subprocess.call(["/etc/init.d/isc-dhcp-server", "restart"])
123 124 125
        return index

    def main(self):
126
        index = self.generate_all()
127
        while True:
Sigmund Augdal's avatar
Sigmund Augdal committed
128 129
            data = self.etcd_client.read("/nova/iaas/instances", recursive=True, wait=True,
                                         waitIndex=index+1, timeout=0)
Sigmund Augdal's avatar
Sigmund Augdal committed
130
            logging.debug("new config index %d", data.etcd_index)  # pylint: disable=E1101
131
            time.sleep(1)
132
            index = self.generate_all()
133 134


135
def daemon_main(args):
136 137 138 139 140
    try:
        generator = Generator(args.etcd_host, args.cert, args.key, args.cacert, args.logfile)
        generator.main()
    except:
        logging.exception("Unhandled exception")
141 142


143
def parse_args():
144 145 146 147 148
    parser = argparse.ArgumentParser(description=DESCRIPTION)
    parser.add_argument('-d', '--daemonize', default=False, action='store_true',
                        help="Run as daemon")
    parser.add_argument('--pidfile', type=str, default="/var/run/{}.pid".format(APP),
                        help="pidfile when run as daemon")
149
    parser.add_argument('--cert', default="client.crt", help="client certificate to use")
150 151
    parser.add_argument('--key', default="client.key",
                        help="private key to use for client certificate")
152
    parser.add_argument('--cacert', default="etcd_ca.crt", help="ca certificate to use")
153 154
    parser.add_argument('--logfile', default='/var/log/{}.log'.format(APP),
                        help="logfile to use")
155 156
    parser.add_argument('--etcd-host', dest="etcd_host", default="localhost",
                        help="Alternative etcd host to connect to")
157

158 159 160
    return parser.parse_args()

if __name__ == '__main__':
Sigmund Augdal's avatar
Sigmund Augdal committed
161 162
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s %(name)s %(levelname)s %(message)s')
163
    logging.getLogger("urllib3").setLevel(logging.WARNING)
164 165
    args = parse_args()
    if args.daemonize:
166 167
        daemon = Daemonize(app=APP, pid=args.pidfile, action=lambda: daemon_main(args))
        daemon.start()
168
    else:
169
        Generator(args.etcd_host, args.cert, args.key, args.cacert).main()