From fedd9448364d766d80ef4724b0b38784ab69c2ae Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 24 Mar 2021 11:59:48 +0100 Subject: [PATCH] inital commit --- README.md | 7 + install.py | 80 ++++++++ router.py | 191 ++++++++++++++++++ servercfg/apumaster.cfg | 11 + servercfg/apuslave1.cfg | 12 ++ sethostname.sh | 10 + to_system/etc/systemd/system/router.service | 10 + .../etc/systemd/system/sethostname.service | 12 ++ 8 files changed, 333 insertions(+) create mode 100644 README.md create mode 100644 install.py create mode 100644 router.py create mode 100644 servercfg/apumaster.cfg create mode 100644 servercfg/apuslave1.cfg create mode 100644 sethostname.sh create mode 100644 to_system/etc/systemd/system/router.service create mode 100644 to_system/etc/systemd/system/sethostname.service diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2460d1 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# APU server + +The APU is a Linux box to be used as a control box at LIN sample environment + +servercfg/.cfg contain the configuration for all boxes + +to_system contains the files to be installed in the system diff --git a/install.py b/install.py new file mode 100644 index 0000000..3a4457b --- /dev/null +++ b/install.py @@ -0,0 +1,80 @@ +"""install.py: + +copy files from to_system into system directories +""" + +import os +import filecmp +import shutil +from os.path import join, getmtime, exists + +os.chdir('to_system') + +DEL = '__to_delete__' + +def walk(action): + for dirpath, _, files in os.walk('.'): + syspath = dirpath[1:] # remove leading '.' + action.dirpath = dirpath + action.syspath = syspath + if files: + match, mismatch, missing = filecmp.cmpfiles(dirpath, syspath, files) + if mismatch: + newer = [f for f in mismatch if getmtime(join(syspath, f)) > getmtime(join(dirpath, f))] + if newer: + action.newer(newer) + if len(newer) < len(mismatch): + newer = set(newer) + action.older([f for f in mismatch if f not in newer]) + if missing: + if DEL in missing: + missing.remove(DEL) + with open(join(dirpath, DEL)) as fil: + to_delete = [] + for f in fil: + f = f.strip() + if f and exists(join(syspath, f)): + if exists(join(dirpath, f)): + print('ERROR: %s in %s, but also in repo -> ignored' % (f, DEL)) + else: + to_delete.append(f) + action.delete(to_delete) + if missing: + action.missing(missing) + +class Show: + def show(self, title, files): + print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) + + def newer(self, files): + self.show('get from', files) + + def older(self, files): + self.show('replace in', files) + + def missing(self, files): + self.show('install in', files) + + def delete(self, files): + self.show('remove from', files) + +class Do: + def newer(self, files): + for file in files: + shutil.copy(join(self.syspath, file), join(self.dirpath, file)) + + def older(self, files): + for file in files: + shutil.copy(join(self.dirpath, file), join(self.syspath, file)) + + def missing(self, files): + self.older(files) + + def delete(self, files): + for file in files: + os.remove(join(self.syspath, file)) + +walk(Show()) +answer = input('do above?') +if answer.lower().startswith('y'): + walk(Do()) diff --git a/router.py b/router.py new file mode 100644 index 0000000..e552632 --- /dev/null +++ b/router.py @@ -0,0 +1,191 @@ +import socket +from select import select +from serial import serial_for_url +from subprocess import Popen, PIPE, check_output, call +from configparser import ConfigParser + +FILTER = "iptables -i enp4s0 -p tcp -m tcp --dport %d -j ACCEPT" + +BASIC = """ +iptables -P INPUT %(accept)s +iptables -P FORWARD %(accept)s +iptables -P OUTPUT ACCEPT +iptables -A INPUT -i lo -j ACCEPT +""" + +sim = False + +def unix_cmd(command): + if sim: + print('> %r' % command) + else: + print('$ %r' % command) + return Popen(command.split(), stdout=PIPE).communicate()[0].decode() + + +class IoHandler: + client = None + handler = None + + def __init__(self, client, handler): + self.handler = handler + self.client = client + + def request(self): + try: + data = self.client.recv(1024) + if data: + self.write(data) + return + except Exception as e: + print('ERROR in request: %s' % e) + self.close() + self.handler.close_client(self) + + def reply(self): + try: + self.client.sendall(self.read()) + return + except ConnectionResetError: + pass + except Exception as e: + print('ERROR in reply: %s' % e) + self.close() + self.handler.close_client(self) + + +class TcpHandler(IoHandler): + def __init__(self, client, handler): + self.socket = socket.create_connection(handler.addr) + self.fno = self.socket.fileno() + super().__init__(client, handler) + + def read(self): + data = self.socket.recv(1024) + if not data: + raise ConnectionResetError('disconnected') + return data + + def write(self, data): + self.socket.sendall(data) + + def close(self): + try: + self.socket.close() + except Exception as e: + print('ERROR in close: %s' % e) + + +class SerialHandler(IoHandler): + def __init__(self, client, handler): + self.serial = serial_for_url(handler.addr, timeout=10) + self.serial.timeout = None + self.fno = self.serial.fileno() + super().__init__(client, handler) + + def read(self): + return self.serial.read(self.serial.in_waiting) + + def write(self, data): + self.serial.write(data) + + def close(self): + self.serial.close() + + +class AcceptHandler: + """handler for routing + + :param: port offered port for routing + :param: addr where to route + :param: iocls the io ahndler class, currently TcpHandler or SerialHandler + :param: maxcount the maximal number of concurrent connections. defauls to 1 + as a side effect, if the destination is a web server, the traffic + are serialized (only one connection at a time), which helps for + some moxa device servers. might be a problem, if connections are + reused: in this case maxcount has to be increased ... + """ + readers = {} + + def __init__(self, port, addr, iocls, maxcount=1): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', port)) + s.listen() + self.socket = s + self.addr = addr + self.iocls = iocls + self.readers[s.fileno()] = self.accept + self.port = port + self.available = maxcount + self.pending = 0 + print('listening at port %d for %s(%r)' % (port, iocls.__name__, addr) ) + + def close_client(self, iohandler): + self.readers.pop(iohandler.fno, None) + try: + client = iohandler.client + self.readers.pop(client.fileno()) + client.close() + except Exception as e: + print('ERROR in close_client: %s' % e) + iohandler.client = None + iohandler.fno = None + self.available += 1 + if self.pending: + self.pending -= 1 + self.accept() + + def accept(self): + if not self.available: + self.pending += 1 + return + client, addr = self.socket.accept() + handler = self.iocls(client, self) + self.readers[client.fileno()] = handler.request + self.readers[handler.fno] = handler.reply + self.available -= 1 + + @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) + + for port, dest in routes.items(): + port=int(port) + if restrict: + unix_cmd(FILTER % port) + if '/' in dest: + AcceptHandler(port, dest, SerialHandler) + else: + host, _, remoteport = dest.partition(':') + if remoteport: + remoteport = int(remoteport) + else: + remoteport = port + AcceptHandler(port, (host, remoteport), TcpHandler) + while True: + try: + ready, _, _ = select(cls.readers, [], []) + except Exception as e: + for r in cls.readers: + try: + select([r], [], [], 0.1) + except Exception as e: + print(r, e) + raise + for fno in ready: + cls.readers[fno]() + + + +if __name__ == '__main__': + parser = ConfigParser() + parser.read('/root/aputools/servercfg/%s.cfg' % socket.gethostname()) + AcceptHandler.run(parser['ROUTER']) diff --git a/servercfg/apumaster.cfg b/servercfg/apumaster.cfg new file mode 100644 index 0000000..703529a --- /dev/null +++ b/servercfg/apumaster.cfg @@ -0,0 +1,11 @@ +[NETWORK] +address=00:0d:b9:59:1f:ac +enp2s0=192.168.127.2/24 +enp3s0=192.168.2.3/24 +enp4s0=192.168.2.4/24 + +[ROUTER] +3000=/dev/ttyUSB0 +5900=192.168.2.33 +8080=192.168.127.254:80 +3001=192.168.127.254:3001 diff --git a/servercfg/apuslave1.cfg b/servercfg/apuslave1.cfg new file mode 100644 index 0000000..03edce8 --- /dev/null +++ b/servercfg/apuslave1.cfg @@ -0,0 +1,12 @@ +[HOSTNAME] +apuslave1=00:0d:b9:59:20:a8 + +[NETWORK] +enp2s0=192.168.127.2/24 +enp3s0=192.168.2.3/24 +enp4s0=192.168.2.4/24 + +[ROUTER] +3000=/dev/ttyUSB0 +5900=192.168.2.33 +8080=192.168.127.254:80 diff --git a/sethostname.sh b/sethostname.sh new file mode 100644 index 0000000..edd8843 --- /dev/null +++ b/sethostname.sh @@ -0,0 +1,10 @@ +ETHNAME=$(cat /sys/class/net/enp1s0/address) +FOUND=$(grep -H address=$ETHNAME /root/aputools/servercfg/*.cfg) +if [ -z "$FOUND" ]; then + ETHNAME=${ETHNAME//:/} + HOSTNAME=apu${ETHNAME:6:6} +else + FOUND=$(basename $FOUND) # remove directory part + HOSTNAME=${FOUND%%.*} # remove extension and all the rest +fi +echo $HOSTNAME > /etc/hostname diff --git a/to_system/etc/systemd/system/router.service b/to_system/etc/systemd/system/router.service new file mode 100644 index 0000000..32036ca --- /dev/null +++ b/to_system/etc/systemd/system/router.service @@ -0,0 +1,10 @@ +[Unit] +Description = Routing to locally connected hardware +After = network.target + +[Service] +ExecStart = /usr/bin/python3 /root/aputools/router.py + +[Install] +WantedBy = multi-user.target + diff --git a/to_system/etc/systemd/system/sethostname.service b/to_system/etc/systemd/system/sethostname.service new file mode 100644 index 0000000..8b3a5bb --- /dev/null +++ b/to_system/etc/systemd/system/sethostname.service @@ -0,0 +1,12 @@ +[Unit] +Description = Create host name from ethernet address +Wants=network-pre.target +Before=network.pre-target + +[Service] +Type=oneshot +ExecStart = /usr/bin/bash /root/aputools/sethostname.sh +RemainAfterExit=yes + +[Install] +WantedBy = network.target