linuxmuster/linuxmuster-base7

View on GitHub
sbin/linuxmuster-import-subnets

Summary

Maintainability
Test Coverage
#!/usr/bin/python3
#
# linuxmuster-import-subnets
# thomas@linuxmuster.net
# 20230801
#

import ast
import constants
import datetime
import os
import re
import subprocess
import time
import yaml

from bs4 import BeautifulSoup
from functions import firewallApi, getFwConfig, getSetupValue, getSftp
from functions import getSubnetArray, ipMatchSubnet, isValidHostIpv4
from functions import printScript, putFwConfig, putSftp, readTextfile
from functions import sshExec, writeTextfile
from IPy import IP

# read necessary values from setup.ini and other sources
serverip = getSetupValue('serverip')
domainname = getSetupValue('domainname')
gateway = getSetupValue('gateway')
firewallip = getSetupValue('firewallip')
# get boolean value
skipfw = ast.literal_eval(getSetupValue('skipfw'))
bitmask_setup = getSetupValue('bitmask')
network_setup = getSetupValue('network')
ipnet_setup = network_setup + '/' + bitmask_setup


# template variables

# lan gateway
gw_lan_descr = 'Interface LAN Gateway'
gw_lan_xml = """
        <gateway_item>
            <interface>lan</interface>
            <gateway>@@gw_ip@@</gateway>
            <name>@@gw_lan@@</name>
            <weight>1</weight>
            <ipprotocol>inet</ipprotocol>
            <interval></interval>
            <descr>@@gw_lan_descr@@</descr>
            <avg_delay_samples></avg_delay_samples>
            <avg_loss_samples></avg_loss_samples>
            <avg_loss_delay_samples></avg_loss_delay_samples>
            <monitor_disable>1</monitor_disable>
        </gateway_item>
"""
gw_lan_xml = gw_lan_xml.replace('@@gw_lan@@', constants.GW_LAN).replace(
    '@@gw_lan_descr@@', gw_lan_descr)

# outbound nat rules
nat_rule_descr = 'Outbound NAT rule for subnet'
nat_rule_xml = """
      <rule>
        <source>
          <network>@@subnet@@</network>
        </source>
        <destination>
          <any>1</any>
        </destination>
        <descr>@@nat_rule_descr@@ @@subnet@@</descr>
        <interface>wan</interface>
        <tag/>
        <tagged/>
        <poolopts/>
        <ipprotocol>inet</ipprotocol>
        <created>
          <username>root@@@serverip@@</username>
          <time>@@timestamp@@</time>
          <description>linuxmuster-import-subnet made changes</description>
        </created>
        <target/>
        <targetip_subnet>0</targetip_subnet>
        <sourceport/>
      </rule>
"""
nat_rule_xml = nat_rule_xml.replace(
    '@@nat_rule_descr@@', nat_rule_descr).replace('@@serverip@@', serverip)


# functions begin
# update static routes in netplan configuration
def updateNetplan(subnets):
    printScript('Processing netplan configuration:')
    cfgfile = constants.NETCFG
    # create backup of current configuration
    timestamp = str(datetime.datetime.now()).replace('-', '').replace(' ', '').replace(':', '').split('.')[0]
    bakfile = cfgfile + '-' + timestamp
    rc = subprocess.call('cp ' + cfgfile + ' ' + bakfile, shell=True)
    if rc != 0:
        printScript('* Failed to backup ' + cfgfile + '!')
        return False
    # read netplan config file
    with open(cfgfile) as config:
        netcfg = yaml.safe_load(config)
    iface = str(netcfg['network']['ethernets']).split('\'')[1]
    ifcfg = netcfg['network']['ethernets'][iface]
    # remove deprecated gateway4
    try:
        del ifcfg['gateway4']
        printScript('* Removed deprecated gateway4 statement.')
    except:
        None
    # first delete the old routes if there are any
    try:
        del ifcfg['routes']
        printScript('* Removed old routes.')
    except:
        None
    # set default route
    ifcfg['routes'] = []
    subroute = eval('{"to": \'default\', "via": \'' + gateway + '\'}')
    ifcfg['routes'].append(subroute)
    # add subnet routes if there are any beside server network
    if len(subnets) > 0:
        for item in subnets:
            # skip if subnet gateway is the default
            if servernet_router == gateway:
                continue
            subnet = item.split(':')[0]
            # tricky: concenate dict object for yaml using eval
            subroute = eval('{"to": \'' + subnet + '\', "via": \'' + servernet_router + '\'}')
            ifcfg['routes'].append(subroute)
        printScript('* Added new routes for all subnets.')
    # save netcfg
    with open(cfgfile, 'w') as config:
        config.write(yaml.dump(netcfg, default_flow_style=False))
    rc = subprocess.call('netplan apply', shell=True)
    if rc == 0:
        printScript('* Applied new netplan configuration.')
    else:
        printScript('* Failed to apply new netplan configuration. Rolling back to previous status.')
        subprocess.call('cp ' + bakfile + ' ' + cfgfile, shell=True)
        subprocess.call('netplan apply', shell=True)
        return False


# update vlan gateway on firewall
def updateFwGw(servernet_router, content):
    soup = BeautifulSoup(content, 'lxml')
    # get all gateways
    gateways = soup.findAll('gateways')[0]
    soup = BeautifulSoup(str(gateways), 'lxml')
    # remove old lan gateway from gateways
    gw_array = []
    for gw_item in soup.findAll('gateway_item'):
        if gw_lan_descr not in str(gw_item):
            gw_array.append(gw_item)
    # append new lan gateway
    gw_array.append(gw_lan_xml.replace('@@gw_ip@@', servernet_router))
    # create gateways xml code
    gateways_xml = '<gateways>'
    for gw_item in gw_array:
        gateways_xml = gateways_xml + str(gw_item)
    gateways_xml = gateways_xml + '\n' + '</gateways>'
    content = re.sub(r'<gateways>.*?</gateways>',
                     gateways_xml, content, flags=re.S)
    return True, content


# update subnet nat rules on firewall
def updateFwNat(subnets, ipnet_setup, serverip, content):
    # create array with all nat rules
    soup = BeautifulSoup(content, 'lxml')
    out_nat = soup.findAll('outbound')[0]
    soup = BeautifulSoup(str(out_nat), 'lxml')
    # remove old subnet rules from array
    nat_rules = []
    for item in soup.findAll('rule'):
        if nat_rule_descr not in str(item):
            nat_rules.append(item)
    # add new subnet rules to array
    for item in subnets:
        subnet = item.split(':')[0]
        # skip servernet
        if subnet == ipnet_setup:
            continue
        timestamp = str(datetime.datetime.now(datetime.timezone.utc).timestamp())
        nat_rule = nat_rule_xml.replace('@@subnet@@', subnet)
        nat_rule = nat_rule.replace('@@timestamp@@', timestamp)
        nat_rules.append(nat_rule)
    # create nat rules xml code
    nat_xml = '\n<outbound>\n<mode>hybrid</mode>\n'
    for nat_rule in nat_rules:
        nat_xml = nat_xml + str(nat_rule)
    nat_xml = nat_xml + '\n</outbound>'
    # replace code in config content
    content = re.sub(r'<outbound>.*?</outbound>', nat_xml, content, flags=re.S)
    return True, content


# download, modify and upload firewall config
def updateFw(subnets, firewallip, ipnet_setup, serverip, servernet_router, gw_lan_xml):
    # first get config.xml
    if not getFwConfig(firewallip):
        return False
    # load configfile
    rc, content = readTextfile(constants.FWCONFLOCAL)
    if not rc:
        return rc
    changed = False
    # add vlan gateway to firewall
    rc, content = updateFwGw(servernet_router, content)
    if rc:
        changed = rc
    # add subnet nat rules to firewall
    rc, content = updateFwNat(subnets, ipnet_setup, serverip, content)
    if rc:
        changed = rc
    if changed:
        # write changed config
        if writeTextfile(constants.FWCONFLOCAL, content, 'w'):
            printScript('* Saved changed config.')
        else:
            printScript('* Unable to save configfile!')
            return False
        if not putFwConfig(firewallip):
            return False
    return changed


# add single route
def addFwRoute(subnet):
    try:
        payload = '{"route": {"network": "' + subnet + '", "gateway": "' + \
            constants.GW_LAN + '", "descr": "Route for subnet ' + \
            subnet + '", "disabled": "0"}}'
        res = firewallApi('post', '/routes/routes/addroute', payload)
        printScript('* Added route for subnet ' + subnet + '.')
        return True
    except:
        printScript('* Unable to add route for subnet ' + subnet + '!')
        return False


# delete route on firewall by uuid
def delFwRoute(uuid, subnet):
    try:
        rc = firewallApi('post', '/routes/routes/delroute/' + uuid)
        printScript('* Route ' + uuid + ' - ' + subnet + ' deleted.')
        return True
    except:
        printScript('* Unable to delete route ' + uuid + ' - ' + subnet + '!')
        return False


# update firewall routes
def updateFwRoutes(subnets, ipnet_setup, servernet_router):
    printScript('Updating subnet routing on firewall:')
    try:
        routes = firewallApi('get', '/routes/routes/searchroute')
        staticroutes_nr = len(routes['rows'])
        printScript('* Got ' + str(staticroutes_nr) + ' routes.')
    except:
        printScript('* Unable to get routes.')
        return False
    # iterate through firewall routes and delete them if necessary
    changed = False
    gateway_orig = constants.GW_LAN + ' - ' + servernet_router
    if staticroutes_nr > 0:
        count = 0
        while (count < staticroutes_nr):
            uuid = routes['rows'][count]['uuid']
            subnet = routes['rows'][count]['network']
            gateway = routes['rows'][count]['gateway']
            # delete not compliant routes
            if (subnet not in str(subnets) and gateway == gateway_orig) or (subnet in str(subnets) and gateway != gateway_orig):
                delFwRoute(uuid, subnet)
                printScript('* Route ' + subnet + ' deleted.')
                changed = True
            count += 1
    # get changed routes
    if changed:
        routes = firewallApi('get', '/routes/routes/searchroute')
    # find and collect routes to be added
    for subnet in subnets:
        # extract subnet from string
        s = subnet.split(':')[0]
        # skip server network
        if s == ipnet_setup:
            continue
        if s not in str(routes):
            rc = addFwRoute(s)
            if rc:
                changed = rc
    return changed
# functions end


# iterate over subnets
printScript('linuxmuster-import-subnets')
printScript('', 'begin')
printScript('Reading setup data:')
printScript('* Server address: ' + serverip)
printScript('* Server network: ' + ipnet_setup)
printScript('Processing dhcp subnets:')
servernet_router = firewallip
subnets = []
# collect subnet data and write dhcpd's subnet.conf
subnetconf = open(constants.DHCPSUBCONF, 'w')
for row in getSubnetArray():
    try:
        ipnet = row[0]
        router = row[1]
        range1 = row[2]
        range2 = row[3]
        nameserver = row[4]
    except:
        continue
    try:
        nextserver = row[5]
    except:
        nextserver = ''
    if ipnet[:1] == '#' or ipnet[:1] == ';' or not isValidHostIpv4(router):
        continue
    if not isValidHostIpv4(range1) or not isValidHostIpv4(range2):
        range1 = ''
        range2 = ''
    if not isValidHostIpv4(nameserver):
        nameserver = ''
    if not isValidHostIpv4(nextserver):
        nextserver = ''

    # compute network data
    try:
        n = IP(ipnet, make_net=True)
        network = IP(n).strNormal(0)
        netmask = IP(n).strNormal(2).split('/')[1]
        broadcast = IP(n).strNormal(3).split('-')[1]
    except:
        continue
    # save servernet router address for later use
    if ipnet == ipnet_setup:
        servernet_router = router
        supp_info = 'server network'
    else:
        supp_info = ''
    subnets.append(ipnet + ':' + router)
    # write subnets.conf
    printScript('* ' + ipnet)
    subnetconf.write('# Subnet ' + ipnet + ' ' + supp_info + '\n')
    subnetconf.write('subnet ' + network + ' netmask ' + netmask + ' {\n')
    subnetconf.write('  option routers ' + router + ';\n')
    subnetconf.write('  option subnet-mask ' + netmask + ';\n')
    subnetconf.write('  option broadcast-address ' + broadcast + ';\n')
    if nameserver != '':
        subnetconf.write('  option domain-name-servers ' + nameserver + ';\n')
        nameserver = ''
    else:
        subnetconf.write('  option netbios-name-servers ' + serverip + ';\n')
    if nextserver != '':
        subnetconf.write('  next-server ' + nextserver + ';\n')
    if range1 != '':
        subnetconf.write('  range ' + range1 + ' ' + range2 + ';\n')
    subnetconf.write('  option host-name pxeclient;\n')
    subnetconf.write('}\n')

subnetconf.close()

# restart dhcp service
service = 'isc-dhcp-server'
msg = 'Restarting ' + service + ' '
printScript(msg, '', False, False, True)
os.system('service ' + service + ' stop')
os.system('service ' + service + ' start')
# wait one second before service check
time.sleep(1)
rc = os.system('systemctl is-active --quiet ' + service)
if rc == 0:
    printScript(' OK!', '', True, True, False, len(msg))
else:
    printScript(' Failed!', '', True, True, False, len(msg))

os.system('systemctl restart isc-dhcp-server.service')

# update netplan config with new routes for server (localhost)
changed = updateNetplan(subnets)

# update firewall
if not skipfw:
    changed = updateFw(subnets, firewallip, ipnet_setup,
                       serverip, servernet_router, gw_lan_xml)
    if changed:
        changed = firewallApi('post', '/routes/routes/reconfigure')
        if changed:
            printScript('Applied new gateway.')
    changed = updateFwRoutes(subnets, ipnet_setup, servernet_router)
    if changed:
        changed = firewallApi('post', '/routes/routes/reconfigure')
        if changed:
            printScript('Applied new routes.')