sbin/linuxmuster-import-subnets
#!/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.')