summaryrefslogtreecommitdiff
path: root/arper.py
blob: d74b659b857beb74c12057a30e07ea57955a2b7a (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
#!/usr/bin/env python3

import datetime
import logging
import os
from typing import Any, Optional

import argparse_utils
from collect.bidict import BiDict
import config
import exec_utils
import persistent
import string_utils

logger = logging.getLogger(__name__)

cfg = config.add_commandline_args(
    f'MAC <--> IP Address mapping table cache ({__file__})',
    'Commandline args related to MAC <--> IP Address mapping',
)
cfg.add_argument(
    '--arper_cache_location',
    default=f'{os.environ["HOME"]}/cache/.arp_table_cache',
    metavar='FILENAME',
    help='Where to cache the kernel ARP table',
)
cfg.add_argument(
    '--arper_cache_max_staleness',
    type=argparse_utils.valid_duration,
    default=datetime.timedelta(seconds=60 * 60),
    metavar='DURATION',
    help='Max acceptable age of the kernel arp table cache'
)


@persistent.persistent_autoloaded_singleton()
class Arper(persistent.Persistent):
    def __init__(
            self, cached_state: Optional[BiDict[str, str]] = None
    ) -> None:
        self.state = BiDict()
        if cached_state is not None:
            logger.debug('Loading Arper map from cached state.')
            self.state = cached_state
        else:
            logger.debug('No usable cached state; calling /usr/sbin/arp')
            output = exec_utils.cmd(
                '/usr/sbin/arp -a',
                timeout_seconds=5.0
            )
            for line in output.split('\n'):
                ip = string_utils.extract_ip_v4(line)
                mac = string_utils.extract_mac_address(line)
                if ip is not None and mac is not None:
                    mac = mac.lower()
                    logger.debug(f'    {mac} => {ip}')
                    self.state[mac] = ip

    def get_ip_by_mac(self, mac: str) -> Optional[str]:
        mac = mac.lower()
        return self.state.get(mac, None)

    def get_mac_by_ip(self, ip: str) -> Optional[str]:
        return self.state.inverse.get(ip, None)

    def save(self) -> bool:
        logger.debug(
            f'Persisting state to {config.config["arper_cache_location"]}'
        )
        with open(config.config['arper_cache_location'], 'w') as wf:
            for (mac, ip) in self.state.items():
                mac = mac.lower()
                print(f'{mac}, {ip}', file=wf)

    @classmethod
    def load(cls) -> Any:
        if persistent.was_file_written_within_n_seconds(
                config.config['arper_cache_location'],
                config.config['arper_cache_max_staleness'].total_seconds(),
        ):
            logger.debug(
                f'Loading state from {config.config["arper_cache_location"]}'
            )
            cached_state = BiDict()
            with open(config.config['arper_cache_location'], 'r') as rf:
                contents = rf.readlines()
                for line in contents:
                    logger.debug(f'ARPER> {line}')
                    (mac, ip) = line.split(',')
                    mac = mac.strip()
                    mac = mac.lower()
                    ip = ip.strip()
                    cached_state[mac] = ip
            return cls(cached_state)
        logger.debug('No usable saved state found')
        return None