diff --git a/README.md b/README.md index 0cf1200..bb15b50 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# APU server +# Linux Box installtion tools The APU is a Linux box to be used as a control box at LIN sample environment -`servercfg/` contains the configuration files for all boxes +`cfg/` contains the configuration files for all boxes `to_system/` contains the files to be installed in the system ## config files -filename: servercfg/(hostname)_(hexdigits).cfg +filename: cfg/(hostname)_(hexdigits).cfg * (hostname) the hostname to be given on startup * (hexdigits) the last six digits of the ethernet adress of the first interface on the APU (leftmost plug) @@ -96,7 +96,7 @@ If you do not have one, you may create it from another box log in as root/FrappyLinse to an apu box ``` -apu> cd aputools +apu> cd boxtools apu> bash mkusb.sh ``` You are asked to give the device name from a list (typically sdb) @@ -150,7 +150,7 @@ which image? login with root/FrappyLinse ``` -> cd aputools +> cd boxtools > git pull > python3 install.py ... diff --git a/servercfg/dilsc_5a58e4.cfg b/cfg/dilsc_5a58e4.cfg similarity index 100% rename from servercfg/dilsc_5a58e4.cfg rename to cfg/dilsc_5a58e4.cfg diff --git a/servercfg/flame-mag_591fac.cfg b/cfg/flame-mag_591fac.cfg similarity index 100% rename from servercfg/flame-mag_591fac.cfg rename to cfg/flame-mag_591fac.cfg diff --git a/servercfg/ir-dil_60f0e8.cfg b/cfg/ir-dil_60f0e8.cfg similarity index 100% rename from servercfg/ir-dil_60f0e8.cfg rename to cfg/ir-dil_60f0e8.cfg diff --git a/cfg/linse-apu7_5fb688.cfg b/cfg/linse-apu7_5fb688.cfg new file mode 100644 index 0000000..128a88a --- /dev/null +++ b/cfg/linse-apu7_5fb688.cfg @@ -0,0 +1,11 @@ +[NETWORK] +; please refer to README.md for help +enp1s0=wan,192.168.1.0/24 +enp2s0=192.168.2.2 +enp3s0=192.168.3.3 +enp4s0=192.168.127.254 + +[ROUTER] +; please refer to README.md for help +3001=192.168.127.254:3001 +8080=192.168.127.254:80 diff --git a/servercfg/linse-box1_5b265c.cfg b/cfg/linse-box1_5b265c.cfg similarity index 100% rename from servercfg/linse-box1_5b265c.cfg rename to cfg/linse-box1_5b265c.cfg diff --git a/servercfg/linse-dil5_5a4c90.cfg b/cfg/linse-dil5_5a4c90.cfg similarity index 100% rename from servercfg/linse-dil5_5a4c90.cfg rename to cfg/linse-dil5_5a4c90.cfg diff --git a/servercfg/linse-uniax_5920a8.cfg b/cfg/linse-uniax_5920a8.cfg similarity index 100% rename from servercfg/linse-uniax_5920a8.cfg rename to cfg/linse-uniax_5920a8.cfg diff --git a/servercfg/linseapu6_5fa5cc.cfg b/cfg/linseapu6_5fa5cc.cfg similarity index 100% rename from servercfg/linseapu6_5fa5cc.cfg rename to cfg/linseapu6_5fa5cc.cfg diff --git a/display.py b/display.py index 6f4fc0f..bc4a69a 100644 --- a/display.py +++ b/display.py @@ -7,7 +7,7 @@ import threading # display tty device tty = '/dev/ttyS1' -STARTUP_TEXT = '/root/aputools/startup_display.txt' +STARTUP_TEXT = '/home/l_samenv/installtools/startup_display.txt' ESC = 0x1b ESCESC = bytes((ESC, ESC)) diff --git a/install.py b/install.py index f5e370b..028a522 100755 --- a/install.py +++ b/install.py @@ -2,7 +2,7 @@ """install.py - copy files from to_system into system directories -- set host name / network settings from aputools/servercfg file +- set host name / network settings from boxtools/cfg file """ if bytes == str: @@ -21,12 +21,17 @@ from ipaddress import IPv4Interface from configparser import ConfigParser from os.path import join, getmtime, exists, basename +if os.geteuid() != 0: + exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") + +TOOLS = '/home/l_samenv/boxtools' + more_info = False -os.chdir('/root/aputools/to_system') +os.chdir(f'{TOOLS}/to_system') DEL = '__to_delete__' -CFGPATH = '/root/aputools/servercfg/%s_%6.6x.cfg' -STARTUP_TEXT = '/root/aputools/startup_display.txt' +CFGPATH = f'{TOOLS}/cfg/%s_%6.6x.cfg' +STARTUP_TEXT = f'{TOOLS}/startup_display.txt' COMMENT = "; please refer to README.md for help" @@ -62,12 +67,12 @@ max-lease-time 7200; authoritative; """ -ROUTER_TEMPLATE = """[Unit] +ROUTER_TEMPLATE = f"""[Unit] Description = Routing to locally connected hardware After = network.target [Service] -ExecStart = /usr/bin/python3 /root/aputools/router.py +ExecStart = /usr/bin/python3 {TOOLS}/router.py [Install] WantedBy = multi-user.target @@ -79,19 +84,19 @@ After = network.target [Service] User = l_samenv -ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/secop-server %s +ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/frappy-server %s [Install] WantedBy = multi-user.target """ -DISPLAY_TEMPLATE = """[Unit] +DISPLAY_TEMPLATE = f"""[Unit] Description = status display After = network.target [Service] User = root -ExecStart = /usr/bin/python3 /root/aputools/display.py -d +ExecStart = /usr/bin/python3 {TOOLS}/display.py -d [Install] WantedBy = multi-user.target @@ -105,8 +110,8 @@ ifname_mapping = { } pip_requirements = { + 'root': {}, 'l_samenv': {}, - 'root': {} } @@ -115,7 +120,6 @@ dhcp_server_cfg = [] # configuration for dhcp main_info = { # info to be determined depending on cfg 'ifname': '', # name of interface of wan (dhcp) port 'addr': '', # addr of wan port - 'current': None, 'hostname': socket.gethostname() # effective or given host name } @@ -145,8 +149,8 @@ def router(**opts): if not opts: return None try: - with open('/root/aputools/requirements.txt') as f: - pip_requirements['root']['aputools'] = f.read() + with open(f'{TOOLS}/requirements.txt') as f: + pip_requirements['root']['tools'] = f.read() except FileNotFoundError: pass return ROUTER_TEMPLATE @@ -225,7 +229,7 @@ def write_when_new(filename, content, ignore_reduction=False): old_set = set(v for v in (old or '').split('\n') if not v.startswith('#')) content_set -= old_set if content_set: - print('missing packages: %s' % (', '.join(content_set - old_set))) + print(f"missing in {filename}: {(', '.join(content_set - old_set))}") else: return False if doit: @@ -264,24 +268,13 @@ def write_when_new(filename, content, ignore_reduction=False): def create_if(name, cfg): - result = dict( - TYPE='Ethernet', - NAME=name, - DEVICE=name, - BOOTPROTO='none', - ONBOOT='yes', - PROXY_METHOD='none', - BROWSER_ONLY='no', - IPV4_FAILURE_FATAL='yes', - IPV6INIT='no') if cfg == 'off': - result['ONBOOT'] = 'no' + result = None elif cfg.startswith('wan') or cfg == 'dhcp': if main_info.get('mainif', name) != name: raise ValueError('can not have more than one WAN/DHCP port') main_info['mainif'] = name main_info['addr'] = net_addr[name] - result['BOOTPROTO'] = 'dhcp' # default: all <= 192.0.0.0 # others have to be added explicitly dhcp_server_cfg.append(('0.0.0.0/128.0.0.0', [])) @@ -289,6 +282,7 @@ def create_if(name, cfg): for nw in cfg.split(',')[1:]: nw = IPv4Interface(nw).network dhcp_server_cfg.append((nw.with_netmask, [])) + result = f"allow-hotplug {name}\niface {name} inet dhcp" else: cfgip = IPv4Interface(cfg) network = cfgip.network @@ -300,12 +294,12 @@ def create_if(name, cfg): else: cfgip = network.network_address + 1 dhcp_server_cfg.append((network.with_netmask, [(str(otherip.ip), str(otherip.ip))])) - result['IPADDR'] = str(cfgip) + addr = str(cfgip) else: # subnet with multiple adresses -> static adresses only. dhcp range not yet implemented - result['IPADDR'] = str(cfgip.ip) - result['NETMASK'] = network.netmask - result['PREFIX'] = str(network.prefixlen) - return result + addr = str(cfgip.ip) + # result['NETMASK'] = network.netmask + result = f"allow-hotplug {name}\niface {name} inet static\n address {addr}/{network.prefixlen}" + return result + '\n' def walk(action): @@ -416,12 +410,12 @@ def handle_config(): display_available = disp.read(8)[0:4] == b'\x1b\x1b\x05\xf3' if display_available: # leftmost interface labelled 'eth0' - main['mainif'] = sorted_if[0] + # main_info['mainif'] = sorted_if[0] typ = 'controlbox' template = CONTROLBOX_TEMPLATE else: # rightmost interface - main['mainif'] = sorted_if[-1] + # main_info['mainif'] = sorted_if[-1] typ = 'bare apu' template = CONFIG_TEMPLATE print('no cfg file found for this', typ, f'with id {apuid:%6.6x} (hostname={hostname})') @@ -443,7 +437,7 @@ def handle_config(): os.system('sh sethostname.sh') else: if cfgfile: - print('replace host name %r by %r' % (hostname, newhostname)) + print('replace host name %r by %r' % (main_info['hostname'], newhostname)) show.dirty = True main_info['hostname'] = newhostname if cfgfile is None: @@ -456,8 +450,8 @@ def handle_config(): network = {ifname_mapping.get(k, k): v for k, v in parser['NETWORK'].items()} for ifname in net_addr: content = create_if(ifname, network.pop(ifname, 'off')) - content = '\n'.join('%s=%s' % kv for kv in content.items()) - todo = write_when_new('/etc/sysconfig/network-scripts/ifcfg-%s' % ifname, content) + # content = '\n'.join('%s=%s' % kv for kv in content.items()) + todo = write_when_new(f'/etc/network/interfaces.d/{ifname}', content) if todo and not more_info: print('change', ifname) show.dirty = True @@ -479,11 +473,11 @@ def handle_config(): todo = write_when_new('/etc/dhcp/dhcpd.conf', content) if todo: print('change dhcpd.conf') - to_start['dhcpd'] = 'restart' + to_start['isc-dhcp-server'] = 'restart' show.dirty = True elif doit: - unix_cmd('systemctl stop dhcpd') - unix_cmd('systemctl disable dhcpd') + unix_cmd('systemctl stop isc-dhcp-server') + unix_cmd('systemctl disable isc-dhcp-server') content = [] dirty = False for section in parser.sections(): @@ -502,7 +496,7 @@ def handle_config(): with open(cfgfile, 'w') as f: f.write('\n'.join(content)) except Exception as e: - print('ERROR: can not handle %s %s: %r' % (hostname, ifname, e)) + print('ERROR: can not handle %s %s: %r' % (main_info['hostname'], ifname, e)) raise return False @@ -546,6 +540,8 @@ def handle_config(): show.dirty = True if action == 'if_restart': unix_cmd('ifdown %s' % service) + for service, action in to_start.items(): + if action == 'if_restart': unix_cmd('ifup %s' % service) else: if action == 'restart': @@ -579,7 +575,7 @@ else: doit = True handle_config() walk(Do()) - with open('/root/aputools/current', 'w') as f: + with open(f'{TOOLS}/current', 'w') as f: f.write(result) elif 'more'.startswith(answer.lower()): more_info = True diff --git a/mkusb.sh b/mkusb.sh old mode 100644 new mode 100755 index 3a63aa4..3ebdae2 --- a/mkusb.sh +++ b/mkusb.sh @@ -1,3 +1,5 @@ +#!/bin/bash +# create an USB stick with TinyLinux on it # name of the USB stick to be created NAME=BOOT_TINY diff --git a/router.py b/router.py index 818b6fd..dcafed0 100644 --- a/router.py +++ b/router.py @@ -11,6 +11,7 @@ from serial import serial_for_url from subprocess import Popen, PIPE, check_output, call, DEVNULL from configparser import ConfigParser +FIREWALL_CONF = '/etc/nftables.conf' class Log: DEBUG = 0 @@ -187,7 +188,7 @@ class Router(IoHandler): pass except Exception as e: msg = f'error in reply: {e!r}' - service.failures[service.port] = msg + self.service.failures[self.service.port] = msg self.close() def close(self): @@ -330,7 +331,7 @@ class InfoHandler(IoHandler): class Service: """service handler - + :param: port offered port for routing :param: addr where to route (or None for InfoHandler) :param: iocls the io handler class, currently TcpHandler, SerialHandler, InfoHandler @@ -344,6 +345,7 @@ class Service: pending_connects = {} routes = {} failures = {} + firewall_ports = set() tmo = 5 def __init__(self, port, addr, iocls, maxcount=None, handler_args=()): @@ -358,6 +360,7 @@ class Service: self.handler_args = handler_args self.readers[s.fileno()] = self.accept self.port = port + self.firewall_ports.add(port) if addr: self.routes[port] = self # not for InfoHandler if maxcount is None: @@ -399,23 +402,38 @@ class Service: self.handlers[handler.fno] = handler @classmethod - def run(cls, routes, restrict=None): - if restrict is not None: - lines = BASIC % dict(accept='DROP' if restrict else 'ACCEPT') - unix_cmd('iptables -F') - for line in lines.split('\n'): - if line.strip(): - unix_cmd(line) - if restrict: - unix_cmd(FILTER % 22) + def change_firewall(cls): + pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL) + with open(FIREWALL_CONF) as f: + content = f.read() + + try: + ((prevline, prevports),) = pattern.findall(content) + except TypeError: + raise ValueError(f'{FIREWALL_CONF} does not contain expected pattern for open ports') + + # parse previous port set + prevportset = {int(p) for p in prevports[1:-1].split(',')} + # keep port numbers below 1024 + ports = {p for p in prevportset if p < 1024} | set(cls.firewall_ports) + line = prevline.replace(prevports, '{ %s }' % ', '.join((str(p) for p in sorted(ports)))) + if prevportset != ports: + if os.geteuid() == 0: + with open('f{FIREWALL_CONF}.tmp', 'w') as f: + f.write(content.replace(prevline, line)) + os.rename('f{FIREWALL_CONF}.tmp', FIREWALL_CONF) + unix_cmd('systemctl enable --now nftables') + else: + print('need sudo rights to modify firewall') + + @classmethod + def run(cls, routes): Service(1110, None, InfoHandler, 5, handler_args=(log.DEBUG,)) Service(1111, None, InfoHandler, 5, handler_args=(log.INFO,)) Service(1112, None, InfoHandler, 5, handler_args=(log.WARN,)) for port, dest in routes.items(): port = int(port) - if restrict: - unix_cmd(FILTER % port) if '/' in dest: Service(port, dest, SerialHandler) else: @@ -425,6 +443,7 @@ class Service: else: remoteport = port Service(port, (host, remoteport), TcpHandler) + cls.change_firewall() while True: try: # log.debug('select %r', list(cls.readers)) @@ -458,7 +477,7 @@ class Service: if __name__ == '__main__': parser = ConfigParser() - cfgfiles = glob('/root/aputools/servercfg/%s_*.cfg' % socket.gethostname()) + cfgfiles = glob('/home/l_samenv/boxtools/cfg/%s_*.cfg' % socket.gethostname()) if len(cfgfiles) != 1: raise ValueError('there must be one and only one single cfgfile %r' % cfgfiles) parser.read(cfgfiles[0]) diff --git a/sethostname.sh b/sethostname.sh index aaab415..589e387 100644 --- a/sethostname.sh +++ b/sethostname.sh @@ -1,7 +1,7 @@ ETHNAME=$(cat /sys/class/net/enp1s0/address) ETHNAME=${ETHNAME//:/} APUID=${ETHNAME: -6} -FOUND="$(shopt -s nullglob; echo /root/aputools/servercfg/*_$APUID.cfg)" +FOUND="$(shopt -s nullglob; echo /home/l_samenv/boxtools/cfg/*_$APUID.cfg)" if [ -z "$FOUND" ]; then HOSTNAME=apu$APUID else diff --git a/to_system/etc/default/isc-dhcp-server b/to_system/etc/default/isc-dhcp-server new file mode 100644 index 0000000..318a7af --- /dev/null +++ b/to_system/etc/default/isc-dhcp-server @@ -0,0 +1,18 @@ +# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server) + +# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf). +#DHCPDv4_CONF=/etc/dhcp/dhcpd.conf +#DHCPDv6_CONF=/etc/dhcp/dhcpd6.conf + +# Path to dhcpd's PID file (default: /var/run/dhcpd.pid). +#DHCPDv4_PID=/var/run/dhcpd.pid +#DHCPDv6_PID=/var/run/dhcpd6.pid + +# Additional options to start dhcpd with. +# Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead +#OPTIONS="" + +# On what interfaces should the DHCP server (dhcpd) serve DHCP requests? +# Separate multiple interfaces with spaces, e.g. "eth0 eth1". +INTERFACESv4="enp2s0 enp3s0 enp4s0" +INTERFACESv6="" diff --git a/to_system/etc/profile.d/welcome.sh b/to_system/etc/profile.d/welcome.sh index fe85bf8..0e35f77 100755 --- a/to_system/etc/profile.d/welcome.sh +++ b/to_system/etc/profile.d/welcome.sh @@ -7,6 +7,6 @@ function service_status () { echo ${name} $(systemctl is-active ${name}) $enabled; \ done | column -t | grep --color=always '\(disabled\|inactive\|$\)' | nl -bn } -alias current='cat /root/aputools/current' +alias current='cat /home/l_samenv/boxtools/current' service_status router frappy display echo "> current # show currently installed state" diff --git a/to_system/etc/systemd/system/sethostname.service b/to_system/etc/systemd/system/sethostname.service index 8b3a5bb..d47c96c 100644 --- a/to_system/etc/systemd/system/sethostname.service +++ b/to_system/etc/systemd/system/sethostname.service @@ -5,7 +5,7 @@ Before=network.pre-target [Service] Type=oneshot -ExecStart = /usr/bin/bash /root/aputools/sethostname.sh +ExecStart = /usr/bin/bash /home/l_samenv/boxtools/sethostname.sh RemainAfterExit=yes [Install]