summaryrefslogtreecommitdiffstats
path: root/at/dhcp.py
blob: 2874e2708d3b24832587560f73c8922b1a4fc04d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import threading
import traceback
import os

import logging

from time import sleep, time, mktime
from datetime import datetime

logger = logging.getLogger(__name__)

def strfts(ts, format='%d/%m/%Y %H:%M'):
    return datetime.fromtimestamp(ts).strftime(format)

class Updater(threading.Thread):
    def __init__(self,  timeout, lease_offset=0, logger=logger, *a, **kw):
        self.timeout = timeout
        self.lock = threading.Lock()
        self.lease_offset = lease_offset
        self.logger = logger
        self.active = {}
        threading.Thread.__init__(self, *a, **kw)
        self.daemon = True

    def purge_stale(self):
        now = time()
        for addr, (atime, ip, name) in list(self.active.items()):
            if now - atime > self.timeout:
                del self.active[addr]

    def get_active_devices(self):
        with self.lock:
            self.purge_stale()
            r = dict(self.active)
        return r

    def get_device(self, ip):
        active_devices = iter(self.get_active_devices().items())
        for hwaddr, (atime, dip, name) in active_devices:
            if ip == dip:
                return hwaddr, name
        return None, None

    def update(self, hwaddr, atime=None, ip=None, name=None):
        if atime:
            atime -= self.lease_offset
        else:
            atime = time()
        with self.lock:
            if hwaddr not in self.active or self.active[hwaddr][0] < atime:
                self.active[hwaddr] = (atime, ip, name)
                self.logger.info('updated %s with atime %s and ip %s',
                    hwaddr, strfts(atime), ip)


class CapUpdater(Updater):
    def __init__(self, cap_file, *a, **kw):
        self.cap_file = cap_file
        Updater.__init__(self, *a, **kw)

    def run(self):
        while True:
            try:
                with open(self.cap_file, 'r', buffering=0) as f:
                    self.logger.info('Updater ready on cap file %s',
                                    self.cap_file)
                    lines = [l.strip() for l in f.read().split('\n')]
                    for hwaddr in lines:
                        if hwaddr:
                            self.update(hwaddr)
                self.logger.warning('Cap file %s closed, reopening',
                                   self.cap_file)
            except Exception as e:
                self.logger.error('Updater got an exception:\n' +
                                 traceback.format_exc(e))
                sleep(10.0)


class MtimeUpdater(Updater):
    def __init__(self, lease_file, *a, **kw):
        self.lease_file = lease_file
        self.position = 0
        self.last_modified = 0
        Updater.__init__(self, *a, **kw)

    def file_changed(self, f):
        """Callback on changed lease file

        Args:
            f: Lease file. File offset can be used to skip already parsed lines.

        Returns: New byte offset pointing after last parsed byte.
        """
        return f.tell()

    def _trigger_update(self):
        self.logger.info('Lease file changed, updating')
        with open(self.lease_file, 'r') as f:
            f.seek(self.position)
            self.position = self.file_changed(f)

    def run(self):
        """Periodicaly check if file has changed

        From ISC DHCPD manual:

            New leases are appended to the end of the dhcpd.leases file.  In
            order to prevent the file from becoming arbitrarily large, from
            time to time dhcpd creates a new dhcpd.leases file from its in-core
            lease database. Once this file has been written to disk, the old
            file is renamed dhcpd.leases~, and the new file is renamed
            dhcpd.leases.
        """
        while True:
            try:
                stat = os.stat(self.lease_file)
                mtime = stat.st_mtime
                size = stat.st_size
                if size < self.position:
                    self.logger.info('leases file changed - reseting pointer')
                    self.position = 0
                try:
                    # checking if DHCPD performed cleanup
                    # cleanup during operation seems to be currently broken
                    # on customs so this could never execute
                    purge_time = os.stat(self.lease_file + '~').st_mtime
                    if purge_time > self.last_modified:
                        self.logger.info('leases file purged - reseting pointer')
                        self.position = 0
                except FileNotFoundError:
                    pass
                if mtime > self.last_modified:
                    self._trigger_update()
                self.last_modified = mtime
                sleep(5.0)
            except Exception as e:
                self.logger.exception('Exception in updater')
                sleep(10.0)


class DnsmasqUpdater(MtimeUpdater):
    def file_changed(self, f):
        raise NotImplementedError(
            "This was not tested after adding differential update")
        for line in f:
            ts, hwaddr, ip, name, client_id = line.split(' ')
            self.update(hwaddr, int(ts), ip, name)
        return f.tell()


class DhcpdUpdater(MtimeUpdater):
    def file_changed(self, f):
        lease = False
        # for use by next-line logic
        ip = None
        hwaddr = None
        atime = None
        offset = f.tell()
        while True:
            # using readline because iter(file) blocks file.tell usage
            line = f.readline()
            if not line:
                return offset
            line = line.split('#')[0]
            cmd = line.strip().split()
            if not cmd:
                continue
            if lease:
                field = cmd[0]
                if(field == 'starts'):
                    dt = datetime.strptime(' '.join(cmd[2:]),
                                           '%Y/%m/%d %H:%M:%S;')
                    atime = mktime(dt.utctimetuple())
                if(field == 'client-hostname'):
                    name = cmd[1][1:-2]
                if(field == 'hardware'):
                    hwaddr = cmd[2][:-1]
                if(field.startswith('}')):
                    offset = f.tell()
                    lease = False
                    if hwaddr is not None and atime is not None:
                        self.update(hwaddr, atime, ip, name)
                    hwaddr, atime = None, None
            elif cmd[0] == 'lease':
                ip = cmd[1]
                name, hwaddr, atime = [None] * 3
                lease = True