Mini Shell
#!/opt/imh-python/bin/python3
"""Scans /etc/ips for IPs with incorrect or missing subnet masks/broadcasts.
Also searches for IPs in the file which are not from the correct network"""
from argparse import ArgumentParser
from configparser import ConfigParser
import logging
import os
import sys
import json
from collections import Counter
import subprocess
from netaddr import IPNetwork, IPAddress
from netaddr.core import AddrFormatError
import rads
def iter_v4_prefixes():
"""Yield subnet (IPNetwork) and zone (str) for each net in imh/whh"""
with open('/opt/maint/etc/netbox.json', encoding='utf-8') as file:
data: dict[str, dict] = json.load(file)['ipv4']['prefixes']
for tenant, site_ips in data.items():
if tenant not in ('imh', 'whh'):
continue
for site, nets in site_ips.items():
for net in nets:
try:
ipnet = IPNetwork(net)
except AddrFormatError:
logging.error('Could not parse subnet: %r', net)
continue
yield ipnet, site
def get_zone() -> str:
"""Return the value of salt[imh.zone]() which is saved to maint.ini"""
conf = ConfigParser(strict=True)
if not conf.read('/opt/maint/etc/maint.ini'):
raise FileNotFoundError('/opt/maint/etc/maint.ini')
return conf.get('net', 'zone')
def find(v4_prefixes: dict[IPNetwork, str], addr: str) -> tuple[IPNetwork, str]:
"""Find the subnet and zone an IP is from"""
try:
ipaddr = IPAddress(addr)
except AddrFormatError as exc:
raise ValueError(f'Could not parse IP: {addr!r}') from exc
matches = {}
for net, site in v4_prefixes.items():
if ipaddr in net:
matches[net] = site
if not matches:
raise KeyError(f'Could not find subnet for {addr!r}')
smallest_net = sorted(matches, key=len)[0]
return smallest_net, matches[smallest_net]
def send_str(bad_ips):
"""Send an STR ticket for an IP assigned from the wrong block"""
logging.info('Sending STR for bad IPs: %r', bad_ips)
body = """
The following IP(s) on this server appear to be assigned from the wrong datacenter's IP blocks.
For each,
* clear the IP from IMHSC
* check if it has a customer on it and fix / contact as necessary
* remove the IP from the server
IP(s):
""".strip()
rads.send_email(
to_addr='str@imhadmin.net',
subject='Bad IP(s) in /etc/ips',
body='{}\n\n{}'.format(body, '\n'.join(bad_ips)),
)
def parse_args():
"""Parse sys.argv"""
parser = ArgumentParser(description=__doc__)
parser.add_argument('--noop', action='store_true')
return parser.parse_args()
def main():
"""Main program logic: Find errors in /etc/ips"""
noop: bool = parse_args().noop
# cron config appends stdout/err to /var/log/maint/fix_etc_ips.log
rads.setup_logging(path=None, loglevel=logging.INFO, print_out='stdout')
v4_prefixes = dict(iter_v4_prefixes())
local_zone = get_zone()
with open('/etc/ips', encoding='ascii') as handle:
etc_ips = handle.read().split()
fixed = False # whether we changed /etc/ips
wrong_zone = [] # any IPs we found asigned from the wrong block
for index, ip_row in enumerate(etc_ips.copy()):
if ip_row.startswith('169.'):
continue
addr = ip_row.split(':', 1)[0]
try:
net, site = find(v4_prefixes, addr)
except (KeyError, ValueError) as exc:
logging.error(exc)
continue
if site != local_zone:
# this IP was assigned from the wrong block
logging.warning(
'%s was assigned from the wrong zone (%s)', addr, site
)
wrong_zone.append(addr)
# Don't proceed to editing its mask - it's already likely causing
# routing problems for this server. Don't risk making it worse.
continue
correct = f"{addr}:{net.netmask}:{net.broadcast}"
if correct != ip_row:
# either the netmask or broadcast was wrong
logging.info('Fixing %r -> %r', ip_row, correct)
fixed = True
etc_ips[index] = correct
if dupes := [k for k, v in Counter(etc_ips).items() if v > 1]:
logging.info('Removing duplicates: %r', dupes)
etc_ips = list(set(etc_ips))
fixed = True
if noop:
return
if fixed: # we fixed at least one line in /etc/ips
write_etc_ips(etc_ips)
restart_ipaliases()
if wrong_zone: # at least one IP was assigned from the wrong zone in IMHSC
send_str(wrong_zone)
def write_etc_ips(etc_ips):
"""Safely write to /etc/ips"""
with open('/etc/ips.fixing', 'w', encoding='ascii') as handle:
handle.write('\n'.join(etc_ips))
handle.write('\n')
os.rename('/etc/ips.fixing', '/etc/ips')
def restart_ipaliases():
"""Restart ipaliases to load subinterfaces from the IPs in /etc/ips"""
cmd = ['/usr/local/cpanel/scripts/restartsrv_ipaliases']
try:
subprocess.check_call(cmd)
except (subprocess.CalledProcessError, OSError):
logging.critical('error restarting ipaliases')
sys.exit(2)
if __name__ == '__main__':
main()
Zerion Mini Shell 1.0