diff --git a/README.md b/README.md index b8ba201..0cf1200 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,23 @@ 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 +`servercfg/` contains the configuration files for all boxes -to_system/ contains the files to be installed in the system +`to_system/` contains the files to be installed in the system ## config files filename: servercfg/(hostname)_(hexdigits).cfg * (hostname) the hostname to be given on startup - * (hexdigits) the last six digits of one of the ethernet adresses of the interfaces on the APU + * (hexdigits) the last six digits of the ethernet adress of the first interface on the APU (leftmost plug) content of the config files: ``` [NETWORK] enp1s0=192.168.127.254 # leftmost socket: connect here a moxa with factory settings -enp2s0=192.168.2.2 # connected device will get this ip via DHCP +enp2s0=192.168.2.2 # connected device will get this ip via DHCP enp3s0=192.168.3.3 # or must be configured static to this IP enp4s0=dhcp # rightmost socket is the uplink @@ -30,17 +30,24 @@ enp4s0=dhcp # rightmost socket is the uplink [FRAPPY] cfg=uniax # the cfg file for the frappy server port=5000 # the port for the frappy server + +[DISPLAY] +startup_text=startup...|HOST|ADDR # startup text, 3 lines separated with | ``` ## network configuration -The example above fits the most cases. Here a detailed description of possible settings: +The example above fits the most cases. +Remark: as the control boxes (MLZ type) are labelled with 'eth0' etc., the names might be given in this form. +Internally the more modern names like 'enp1s0' are used. + +Here a detailed description of possible settings: ### fixed IP ``` enp2s0=192.168.3.5 ``` -Configure the IP address of the connected device to get the specified IP via DHCP or +Configure the IP address of the connected device to get the specified IP via DHCP or configure the device to with this static IP. The last number must be within 1..254, the APU port itself will get 1 (or 2, if the specified IP is 1). @@ -56,7 +63,7 @@ Probably needed rarely. ### uplink via DHCP ``` -enp4s0=wan # or enp4s0=dhcp +enp4s0=wan # or enp4s0=dhcp ``` Uplink configured for DHCP in any network n.x.y.z with n < 192 @@ -72,5 +79,95 @@ Must not overlap networks specified for other ports! enp3s0=off # disabled ``` +## display + +available on control boxes (MLZ type) only. must therefore not be present on bare apu boxes. +## Installation of a Fresh Control Box or bare APU + +## install image by means of the USB stick BOOT_TINY + +The stick has TinyLinux on it and some additional scripts + +If you do not have one, you may create it from another box + +### a) create a USB stick with TinyLinux (omit if you have the stick already) + +log in as root/FrappyLinse to an apu box +``` +apu> cd aputools +apu> bash mkusb.sh +``` +You are asked to give the device name from a list (typically sdb) +before it writes to the stick. + + +### b) boot with TinyLinux from USB stick + +Connect a Mac with an USB to serial adapter (and null-modem!). +``` +mac> screen /dev/tty.usbserial-130 115200 +``` + +Do not yet conenct to LAN, if the box is not yet registered to PacketFence. +Plug USB stick and start the box, wait for prompt 'tc@box:' and cd into BOOT_TINY +``` +cd /media/BOOT_TINY +``` + +### c) Determine address/name for registering in PacketFence + +``` +sh reg +``` +enter new hostname (HWaddr will be on the output) +register to PacketFence and set role to sinq-samenv +connect LAN to rightmost socket on a bare APU or the leftmost socket on a control box + + +### d) Copy Image to APU SSD + +``` +sh write_to_ssd +``` +some random art images are shown ... +``` +images from l_samenv@samenv:boxes/images: + +apumaster_2022-11-09.lz4 +apumaster_2024-01-18.lz4 + +which image? +``` + +* Enter the image you want to write (typically the last one). +* It will take around 10 mins to write the image. +* remove the stick, power off/on (or do `sudo reboot now`) + + +### e) Install Services + +login with root/FrappyLinse +``` +> cd aputools +> git pull +> python3 install.py +... +enter host name: +... + +> reboot now +``` + +DONE! + + +### f) Cloning an Image from an Existing Box + +Use (b) above to boot from the BOOT_TINY USB stick +``` +$ sh clone +``` +You are asked + diff --git a/display.py b/display.py new file mode 100644 index 0000000..6f4fc0f --- /dev/null +++ b/display.py @@ -0,0 +1,309 @@ +import sys +import os +import time +import serial +import socket +import threading + +# display tty device +tty = '/dev/ttyS1' +STARTUP_TEXT = '/root/aputools/startup_display.txt' + +ESC = 0x1b +ESCESC = bytes((ESC, ESC)) + +MODE_GRAPH = 0x20 +MODE_CONSOLE = 0x21 + +SET_POS = 0x30 +SET_FONT = 0x31 +SET_COLOR = 0x32 + +CLEAR = 0x40 +LINES = 0x41 +RECT = 0x42 +ICON = 0x43 +TEXT = 0x44 +COPYRECT = 0x45 +PLOT = 0x46 + +TOUCH = 0x50 +TOUCH_MODE = 0x51 + +SAVE_ATTRS = 0xa0 +SEL_ATTRS = 0xc0 + +SET_STARTUP = 0xf2 + +IDENT = 0xf3 + +FONT_GEO = [(6, 8), (8, 16), (20, 40), (38,64), (16, 16), (12, 24)] + +TOPGAP = 8 # upper part of display is not useable +HEIGHT = 120 +WIDTH = 480 + +MAGIC = b'\xcb\xef\x20\x18' + + +def xy(x, y): + x = min(480, int(x)) + return bytes([(min(127, y + TOPGAP) << 1) + (x >> 8), x % 256]) + + +class Display: + fg = 15 + bg = 0 + touch = 0 + bar = None + blink = False + menu = None + storage = None + + def __init__(self, dev, timeout=0.5, daemon=False): + self.event = threading.Event() + self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) + self.storage = bytearray() + if daemon: + todo_file = STARTUP_TEXT + '.todo' + try: + with open(todo_file) as f: + text = f.read() + print('new startup text:') + print(text) + except FileNotFoundError: + text = None + if text: + self.reset() + self.show(*text.split('\n')[0:3]) + self.set_startup() + os.rename(todo_file, STARTUP_TEXT) + else: + self.storage = None + else: + os.system('systemctl stop display') + self.gethost(False) + self.reset() + if daemon: + threading.Thread(target=self.gethostthread, daemon=True).start() + self.event.wait(1) # wait for thread to be started + self.net_display(None) + + def reset(self): + self.send(MODE_GRAPH) + self.font(2) + self.send(CLEAR, self.bg) + + def color(self, fg=15, bg=0): + self.fg = fg + self.bg = bg + self.send(SET_COLOR, bg, bg, fg, fg) + + def send(self, code, *args): + out = bytearray([code]) + for arg in args: + if isinstance(arg, str): + out.extend(arg.encode('latin-1')) + elif hasattr(arg, '__len__'): + out.extend(arg) + else: + out.append(arg) + cmd = bytearray([ESC,ESC,len(out)]) + out + if self.storage is not None: + self.storage.extend(cmd) + self.term.write(cmd) + + def set_startup(self): + if self.storage is None: + print('storage off') + return + data = self.storage + self.storage = None + self.send(SET_STARTUP, data) + + def version(self): + self.term.write(bytes([ESC,ESC,1,0xf3])) + reply = self.term.read(4) + assert reply[0:2] == ESCESC + return self.term.read(reply[2]-1) + + def font(self, size): + self.fontsize = size + self.send(SET_FONT, size) + self.colwid, self.rowhei = FONT_GEO[self.fontsize] + self.nrows = HEIGHT // self.rowhei + self.ncols = WIDTH // self.colwid + self.textbuffer = [" " * self.ncols] * self.nrows + self.color() + + def text(self, text, row=0, left=0, right=None, font=2): + if font != self.fontsize: + self.font(font) + if right is None: + right = self.ncols + if right < 0: + right += self.ncols + if left < 0: + left += self.ncols + for line in text.split('\n'): + padded = line + " " * (right - left - len(line)) + self.send(SET_POS, xy(left * self.colwid, TOPGAP + row * self.rowhei)) + self.send(TEXT, padded.encode('latin-1')) + + def show(self, *args): + self.send(CLEAR, self.bg) + if len(args) == 1: + self.text(args[0], 1) + else: + for row, line in enumerate(args): + if row < 3: + self.text(line, row) + + def gethost(self, truehostname): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(('8.8.8.8', 80)) # 8.8.8.8: google DNS + self.hostip = s.getsockname()[0] + except Exception as e: + self.hostip = '' + hostname = None + if self.hostip and truehostname: + try: + hostname = socket.gethostbyaddr(self.hostip)[0] + except Exception: + pass + self.hostname = hostname or socket.gethostname() + + def gethostthread(self): + self.gethost(True) + self.event.set() + time.sleep(1) + while True: + self.event.clear() + self.gethost(True) + self.event.wait(1) + + def set_touch(self, on): + self.send(TOUCH_MODE, on) + self.touch = on + + def gettouch(self): + if not self.touch: + self.set_touch(1) + while True: + ch2 = self.term.read(2) + while ch2 != ESCESC: + if len(ch2) < 2: + return + ch2 = ch2[1:2] + self.term.read(1) + length = self.term.read(1) + if not length: + return + data = self.term.read(length[0]) + if len(data) < length[0]: + return + if data[0] == TOUCH and len(data) == 3: + x = (data[1] % 2) * 256 + data[2] + return x + print('skipped', data) + + def menu_reboot(self, x): + if x is None: + return + if x < 120: + self.show('reboot ...') + os.system('reboot now') + else: + self.std_display() + + def menu_shutdown(self, x): + if x is None: + return + if x < 120: + self.show('shutdown ...') + os.system('shutdown now') + else: + self.std_display() + + def menu_main(self, x): + if x is None: + return + print(x) + if x < 160: + self.std_display() + elif x < 320: + self.menu = self.menu_reboot + self.show('reboot?', '( OK ) (cancel)') + else: + self.menu = self.menu_shutdown + self.show('shutdown?', '( OK ) (cancel)') + + def std_display(self): + self.menu = self.net_display + self.net_display(None) + + def net_display(self, x): + if x is None: + hostip = self.hostip or 'no network' + dotpos = hostip.find('.') + if dotpos < 0: + dotpos = len(hostip) + self.text(hostip.replace('.', ' ', 1), 0, 0, 15) + self.send(SET_COLOR, 0, 0, 0, 11 + self.blink) # yellow / blue + self.text('.', 0, dotpos, dotpos+1) + self.color() + self.text(self.hostname, 1) + self.event.set() + self.blink = not self.blink + else: + self.menu = self.menu_main + self.show('(cancel)(reboot)(shdown)') + + def refresh(self): + func = self.menu or self.net_display + func(self.gettouch()) + + def bootmode(self): + self.send(0xf0, 0xcb, 0xef, 0x20, 0x18) + + +def pretty_version(v): + return '%c v%d.%d%s' % (v[0], v[2], v[3], ' test' if v[1] else '') + + +daemon = False +firmware = None + +for arg in sys.argv[1:]: + if arg == '-d': + daemon = True + elif arg.endswith('.bin'): + firmware = arg + elif tty.startswith('/dev/'): + tty = arg + else: + raise ValueError(f'do not know how to tread argument: {arg}') + + +d = Display(tty, daemon=daemon and not firmware) +if firmware: + with open(firmware, 'rb') as f: + tag = f.read()[-8:] + if tag[:4] != MAGIC: + raise ValueError(f'{firmware} is not a valid firmware file') + hwversion = d.version() + if tag[4:] == hwversion: + # print('firmware is already', pretty_version(hwversion)) + print('display version:', pretty_version(hwversion)) + print('binfile version:', pretty_version(tag[4:])) + result = input('flash this (takes 1 min)? ').lower() + if result in ('y', 'yes'): + print('\ndo NOT interrupt') + d.bootmode() + d.term.close() + time.sleep(1.) + os.system(f'../stm32flash-0.7/stm32flash -R -v -b 115200 -w {firmware} {tty}') +if daemon: + while True: + d.refresh() + diff --git a/install.py b/install.py index 4f5dda4..f5e370b 100755 --- a/install.py +++ b/install.py @@ -12,31 +12,49 @@ import sys import os import filecmp import shutil +import serial +import types +import socket from subprocess import Popen, PIPE from glob import glob from ipaddress import IPv4Interface from configparser import ConfigParser from os.path import join, getmtime, exists, basename -more_info = sys.argv[1] if len(sys.argv) > 1 else 'NONE' +more_info = False os.chdir('/root/aputools/to_system') DEL = '__to_delete__' CFGPATH = '/root/aputools/servercfg/%s_%6.6x.cfg' +STARTUP_TEXT = '/root/aputools/startup_display.txt' COMMENT = "; please refer to README.md for help" -CONFIG_TEMPLATE = """[NETWORK] -%s +CONFIG_TEMPLATE = f"""[NETWORK] +{COMMENT} enp1s0=192.168.127.254 enp2s0=192.168.2.2 enp3s0=192.168.3.3 enp4s0=dhcp [ROUTER] -%s +{COMMENT} 3001=192.168.127.254:3001 -""" % (COMMENT, COMMENT) +""" + +CONTROLBOX_TEMPLATE = f"""[NETWORK] +{COMMENT} +enp1s0=dhcp +enp2s0=192.168.1.1 +enp3s0=192.168.2.2 +enp4s0=192.168.3.3 + +[DISPLAY] +{COMMENT} +line0=startup... +line1=HOST +line2=ADDR +""" DHCP_HEADER = """ default-lease-time 600; @@ -66,12 +84,51 @@ ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/secop-server %s [Install] WantedBy = multi-user.target """ + +DISPLAY_TEMPLATE = """[Unit] +Description = status display +After = network.target + +[Service] +User = root +ExecStart = /usr/bin/python3 /root/aputools/display.py -d + +[Install] +WantedBy = multi-user.target +""" + +ifname_mapping = { + 'eth0': 'enp1s0', + 'eth1': 'enp2s0', + 'eth2': 'enp3s0', + 'eth3': 'enp4s0', +} + pip_requirements = { 'l_samenv': {}, 'root': {} } +net_addr = {} # dict of +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 +} + +for netif in os.scandir('/sys/class/net'): + if netif.name != 'lo': # do not consider loopback interface + with open(os.path.join(netif.path, 'address')) as f: + addr = f.read().strip().lower() + net_addr[netif.name] = addr + +sorted_if = sorted(net_addr) +apuid = int(''.join(net_addr[sorted_if[0]].split(':')[-3:]), 16) & 0xfffffc + + def frappy(cfg=None, port=None, requirements='', **kwds): if not cfg: return None @@ -95,6 +152,24 @@ def router(**opts): return ROUTER_TEMPLATE +def display_update(cfg): + text = '\n'.join(cfg.get('startup_text', '').split('|')[:3]) + text = text.replace('HOST', main_info['hostname']) \ + .replace('ADDR', main_info['addr']) + '\n' + if write_when_new(STARTUP_TEXT, text): + print('change startup text') + if doit: + os.rename(STARTUP_TEXT, STARTUP_TEXT + '.todo') + return True + return False + + +def display(**opts): + if not opts: + return None + return DISPLAY_TEMPLATE + + def pip(): for user, requirements in pip_requirements.items(): if user == 'root': @@ -118,7 +193,7 @@ def pip(): show.dirty = True -SERVICES = dict(router=router, frappy=frappy) +SERVICES = dict(router=router, frappy=frappy, display=display) def unix_cmd(command, always=False, stdout=PIPE): @@ -159,7 +234,8 @@ def write_when_new(filename, content, ignore_reduction=False): fil.write(content) else: os.remove(filename) - elif filename.endswith(more_info): + elif more_info: + print('.' * 80) print('changes in', filename) old = [] if old is None else old.split('\n') top_lines = 0 # in case of empty loop @@ -183,10 +259,11 @@ def write_when_new(filename, content, ignore_reduction=False): print('>>>') print('\n'.join(old[-bottom_lines:-1])) print("===") - return True + print('.' * 80) + return content -def create_if(name, cfg, mac, dhcp_server_cfg): +def create_if(name, cfg): result = dict( TYPE='Ethernet', NAME=name, @@ -200,6 +277,10 @@ def create_if(name, cfg, mac, dhcp_server_cfg): if cfg == 'off': result['ONBOOT'] = 'no' 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 @@ -246,37 +327,44 @@ def walk(action): 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)) + for fname in fil: + fname = fname.strip() + if fname and exists(join(syspath, fname)): + if exists(join(dirpath, fname)): + print('ERROR: %s in %s, but also in repo -> ignored' % (fname, DEL)) else: - to_delete.append(f) + to_delete.append(fname) action.delete(to_delete) if missing: action.missing(missing) -class Show: +class Walker: + def __init__(self): + self.dirpath = None + self.syspath = None + + +class Show(Walker): dirty = False def diff(self, title, files): self.dirty = True - if more_info == 'NONE': - print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) - else: + if more_info: for f in files: - if f.endswith(MORE_INFO): + if f.endswith(more_info): print('diff %s %s' % (join(self.dirpath, f), join(self.syspath, f))) + print('.' * 80) + else: + print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) def show(self, title, dirpath, files): - if more_info == 'NONE': - print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) - else: + if more_info: for f in files: - if f.endswith(MORE_INFO): - print('cat %s' % join(dirpath, f)) + print('cat %s' % join(dirpath, f)) + print('.' * 80) + else: + print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) def newer(self, files): self.show('get from', self.dirpath, files) @@ -291,7 +379,7 @@ class Show: self.show('remove from', self.syspath, files) -class Do: +class Do(Walker): def newer(self, files): for file in files: shutil.copy(join(self.syspath, file), join(self.dirpath, file)) @@ -308,57 +396,69 @@ class Do: os.remove(join(self.syspath, file)) -IFNAMES = ['enp%ds0' % i for i in range(1,5)] - - def handle_config(): - netaddr_dict = {} - for ifname in IFNAMES: - with open('/sys/class/net/%s/address' % ifname) as f: - netaddr_dict[ifname] = f.read().strip().lower() - netaddr = netaddr_dict[IFNAMES[0]] - apuid = int(''.join(netaddr.split(':')[-3:]), 16) & 0xfffffc cfgfile = None cfgfiles = [] - for i in range(4): - # goodie: look for mac addresses of all 4 ports - cfgfiles += glob(CFGPATH % ('*', apuid + i)) - with open('/etc/hostname') as f: - hostname = f.read().strip() + dhcp_server_cfg.clear() + for file in glob(CFGPATH % ('*', apuid)): + cfgfiles.append(file) + for i in [1, 2, 3]: + bad = glob(CFGPATH % ('*', apuid+i)) + if bad: + print('cfg files found with bad apu id (use net addr of leftmost plug)') + print(bad) + return False + newhostname = main_info['hostname'] if not cfgfiles: - print('no cfg file found for %s' % hostname) - newname = input('enter host name: ') - if not newname: + # determine if display is present + disp = serial.Serial('/dev/ttyS1', baudrate=115200, timeout=1) + disp.write(b'\x1b\x1b\x01\xf3') + display_available = disp.read(8)[0:4] == b'\x1b\x1b\x05\xf3' + if display_available: + # leftmost interface labelled 'eth0' + main['mainif'] = sorted_if[0] + typ = 'controlbox' + template = CONTROLBOX_TEMPLATE + else: + # rightmost interface + main['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})') + newhostname = input('enter host name: ') + if not newhostname: print('no hostname given') return False - cfgfile = CFGPATH % (newname, apuid) + cfgfile = CFGPATH % (newhostname, apuid) with open(cfgfile, 'w') as f: - f.write(CONFIG_TEMPLATE) + f.write(template) elif len(cfgfiles) > 1: print('ERROR: ambiguous cfg files: %s' % ', '.join(cfgfiles)) else: cfgfile = cfgfiles[0] - if cfgfile != CFGPATH % (hostname, apuid): + if cfgfile != CFGPATH % (newhostname, apuid): + if cfgfile: + newhostname = basename(cfgfile).rpartition('_')[0] if doit: - os.system('sh ../sethostname.sh') + os.system('sh sethostname.sh') else: if cfgfile: - print('replace host name %r by %r' % (hostname, basename(cfgfile).rpartition('_')[0])) + print('replace host name %r by %r' % (hostname, newhostname)) show.dirty = True + main_info['hostname'] = newhostname if cfgfile is None: return False to_start = {} ifname = '' parser = ConfigParser() - dhcp_server_cfg = [] try: parser.read(cfgfile) - network = dict(parser['NETWORK']) - for ifname in IFNAMES: - content = create_if(ifname, network.pop(ifname, 'off'), netaddr_dict[ifname], dhcp_server_cfg) + 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) - if todo and more_info == 'NONE': + if todo and not more_info: print('change', ifname) show.dirty = True to_start[ifname] = 'if_restart' @@ -380,7 +480,7 @@ def handle_config(): if todo: print('change dhcpd.conf') to_start['dhcpd'] = 'restart' - show.dirty = True + show.dirty = True elif doit: unix_cmd('systemctl stop dhcpd') unix_cmd('systemctl disable dhcpd') @@ -391,7 +491,11 @@ def handle_config(): dirty = True content.append('[%s]' % section) content.append(COMMENT) - for key, value in parser[section].items(): + section_dict = dict(parser[section].items()) + if section == 'DISPLAY': + if display_update(section_dict): + to_start['display'] = 'restart' + for key, value in section_dict.items(): content.append('%s=%s' % (key, value)) content.append('') if dirty: @@ -403,12 +507,12 @@ def handle_config(): return False reload_systemd = False - for service, template_func in SERVICES.items(): + for service, service_func in SERVICES.items(): section = service.upper() if parser.has_section(section): - template = template_func(**dict(parser[section])) + servicecfg = service_func(**dict(parser[section])) else: - template = None + servicecfg = None result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) active = False enabled = False @@ -417,7 +521,7 @@ def handle_config(): enabled = True elif line.strip() == 'ActiveState=active': active = True - if template is None: + if servicecfg is None: if active: unix_cmd('systemctl stop %s' % service) show.dirty = True @@ -429,10 +533,10 @@ def handle_config(): to_start[service] = 'enable' elif not active: to_start[service] = 'restart' - if write_when_new('/etc/systemd/system/%s.service' % service, template): + if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): show.dirty = True reload_systemd = True - if template and to_start.get('service') is None: + if servicecfg and to_start.get('service') is None: to_start[service] = 'restart' pip() @@ -447,23 +551,39 @@ def handle_config(): if action == 'restart': unix_cmd('systemctl restart %s' % service) unix_cmd('systemctl enable %s' % service) - return True + result = [f'config file:\n {cfgfile}'] + for section in parser.sections(): + result.append(section) + for key, value in parser.items(section): + result.append(f' {key} = {value}') + result.append('') + return '\n'.join(result) +more_info = False doit = False -print('---') +print(' ') show = Show() walk(show) result = handle_config() if not result: print('fix first above errors') -elif show.dirty and more_info == 'NONE': - print('---') - answer = input('do above? ') - doit = True - if answer.lower().startswith('y'): - handle_config() - walk(Do()) else: - print('nothing to do') + if show.dirty: + print('---') + answer = input('enter "y(es)" to do above or "m(ore)" for more info: ') + if not answer: + print('cancel') + elif 'yes'.startswith(answer.lower()): + doit = True + handle_config() + walk(Do()) + with open('/root/aputools/current', 'w') as f: + f.write(result) + elif 'more'.startswith(answer.lower()): + more_info = True + walk(show) + result = handle_config() + else: + print('nothing to do') diff --git a/mkusb.sh b/mkusb.sh new file mode 100644 index 0000000..3a63aa4 --- /dev/null +++ b/mkusb.sh @@ -0,0 +1,59 @@ +# name of the USB stick to be created +NAME=BOOT_TINY + +# list removeable disks +DISKS=() +while read LINE +do + ARR=($LINE) + if [[ ${ARR[@]:0:2} == "1 disk" ]]; then + DISKS+=(${ARR[@]:2:1}) + printf "%-7s %7s " ${ARR[@]:2:2} + echo ${ARR[@]:4} + elif [[ ${ARR[@]:0:2} == "1 part" ]]; then + printf " %7s " ${ARR[@]:3:1} + echo ${ARR[@]:4} + fi +done < <(lsblk -l --output RM,TYPE,NAME,SIZE,LABEL,VENDOR,MODEL) + +echo "which device? should be one of: ${DISKS[@]}" +read DEVICE + +if [ -z $DEVICE ] ; then + exit +fi +if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then + echo "create TinyLinux" + + if [ ! -d boot_tiny ] ; then + git clone --depth 1 git@gitlab.psi.ch:samenv/boot_tiny.git + fi + + if [[ $(cat /sys/block/$DEVICE/removable) != "1" ]]; then + echo "/dev/$DEVICE is not a removable disk" + exit + fi + + DEVICE=/dev/$DEVICE + + umount ${DEVICE}1 + dd if=/dev/zero of=${DEVICE} count=1 conv=notrunc + echo -e "o\nn\np\n1\n\n\nw" | fdisk ${DEVICE} + mkfs.vfat -n $NAME -I ${DEVICE}1 + + syslinux -i ${DEVICE}1 + dd conv=notrunc bs=440 count=1 if=boot_tiny/mbr.bin of=${DEVICE} + parted ${DEVICE} set 1 boot on + + mkdir -p /mnt/apusb + mount ${DEVICE}1 /mnt/apusb + echo "copy files ..." + cp -r boot_tiny/* /mnt/apusb/ + umount /mnt/apusb + + rm -rf boot_tiny + echo "done." +else + echo "/dev/$DEVICE is not a removeable disk" +fi + diff --git a/router.py b/router.py index 26fa7cd..41c6af1 100644 --- a/router.py +++ b/router.py @@ -18,6 +18,7 @@ iptables -A INPUT -i lo -j ACCEPT sim = False + def unix_cmd(command): if sim: print('> %r' % command) @@ -29,10 +30,15 @@ def unix_cmd(command): class IoHandler: client = None handler = None + port = None def __init__(self, client, handler): self.handler = handler self.client = client + self.sentchunks = 0 + self.sentbytes = 0 + self.rcvdchunks = 0 + self.rcvdbytes = 0 def request(self): try: @@ -40,6 +46,8 @@ class IoHandler: if data: # print('<', data) self.write(data) + self.sentbytes += len(data) + self.sentchunks += 1 return except Exception as e: print('ERROR in request: %s' % e) @@ -51,6 +59,8 @@ class IoHandler: data = self.read() # print('>', data) self.client.sendall(data) + self.rcvdbytes += len(data) + self.rcvdchunks += 1 return except ConnectionResetError: pass @@ -99,6 +109,44 @@ class SerialHandler(IoHandler): self.serial.close() +class InfoHandler(IoHandler): + clients = {} + + def __init__(self, client, handler): + super().__init__(client, handler) + info = [f'{k} -> {v}' for k, v in AcceptHandler.routes.items()] + if AcceptHandler.handlers: + info.append('\nactive routings, statistics bytes/chunks') + info.append('fno port sent received') + for fno, h in AcceptHandler.handlers.items(): + info.append(f'{fno} {h.port} {h.sentbytes:d}/{h.sentchunks:d} {h.rcvdbytes:d}/{h.rcvdchunks:d}') + info.append('') + self.client.sendall('\n'.join(info).encode('utf-8')) + self.clients[client.fileno()] = self + self.fno = None + + def read(self): + return b'' + + def write(self, data): + pass + + def close(self): + self.clients.pop(self.client.fileno()) + + @classmethod + def log(cls, line): + if cls.clients: + for c in cls.clients.values(): + try: + c.client.sendall(line.encode('utf-8')) + c.client.sendall(b'\n') + except TimeoutError: + pass + else: + print(line) + + class AcceptHandler: """handler for routing @@ -112,6 +160,8 @@ class AcceptHandler: reused: in this case maxcount has to be increased ... """ readers = {} + handlers = {} + routes = {} def __init__(self, port, addr, iocls, maxcount=1): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -125,16 +175,18 @@ class AcceptHandler: 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) + client = iohandler.client + fno = client.fileno() try: - client = iohandler.client - self.readers.pop(client.fileno()) + self.readers.pop(fno) client.close() + InfoHandler.log(f'closed connection from port {self.port} fno {fno}') except Exception as e: - print('ERROR in close_client: %s' % e) + InfoHandler.log(f'{e!r} in close_client') + self.handlers.pop(fno, None) iohandler.client = None iohandler.fno = None self.available += 1 @@ -148,18 +200,23 @@ class AcceptHandler: return try: client, addr = self.socket.accept() - print('accepted', addr, 'on', self.port) + InfoHandler.log(f'accepted {addr} on {self.port} fno {client.fileno()}') handler = self.iocls(client, self) except Exception as e: - print('ERROR creating %s(%r)' % (self.iocls.__name__, self.addr)) + InfoHandler.log(f'{e!r} creating {self.iocls.__name__}({self.addr})') client.close() return self.readers[client.fileno()] = handler.request - self.readers[handler.fno] = handler.reply + if handler.fno is not None: + self.readers[handler.fno] = handler.reply + # statistics: number of chunks sent / received + handler.port = self.port + self.handlers[client.fileno()] = handler self.available -= 1 @classmethod def run(cls, routes, restrict=None): + cls.routes = dict(routes) if restrict is not None: lines = BASIC % dict(accept='DROP' if restrict else 'ACCEPT') unix_cmd('iptables -F') @@ -169,6 +226,7 @@ class AcceptHandler: if restrict: unix_cmd(FILTER % 22) + AcceptHandler(1111, None, InfoHandler, 5) for port, dest in routes.items(): port=int(port) if restrict: @@ -196,7 +254,6 @@ class AcceptHandler: cls.readers[fno]() - if __name__ == '__main__': parser = ConfigParser() cfgfiles = glob('/root/aputools/servercfg/%s_*.cfg' % socket.gethostname()) diff --git a/servercfg/linse-box1_5b265c.cfg b/servercfg/linse-box1_5b265c.cfg new file mode 100644 index 0000000..f808e75 --- /dev/null +++ b/servercfg/linse-box1_5b265c.cfg @@ -0,0 +1,10 @@ +[NETWORK] +; please refer to README.md for help +eth0=dhcp +eth1=192.168.1.1 +eth2=192.168.2.2 +eth3=192.168.3.3 + +[DISPLAY] +; please refer to README.md for help +startup_text=startup...|HOST|ADDR diff --git a/servercfg/linseapu6_5fa5cc.cfg b/servercfg/linseapu6_5fa5cc.cfg new file mode 100644 index 0000000..2a55d7f --- /dev/null +++ b/servercfg/linseapu6_5fa5cc.cfg @@ -0,0 +1,11 @@ +[NETWORK] +; please refer to README.md for help +enp1s0=192.168.127.254 +enp2s0=192.168.12.2 +enp3s0=192.168.13.3 +enp4s0=wan,192.168.1.0/24 + +[ROUTER] +; please refer to README.md for help +3001=192.168.127.254:3001 +8080=192.168.127.254:80 diff --git a/to_system/etc/profile.d/welcome.sh b/to_system/etc/profile.d/welcome.sh index 421c75c..b4b10e3 100755 --- a/to_system/etc/profile.d/welcome.sh +++ b/to_system/etc/profile.d/welcome.sh @@ -1,9 +1,11 @@ echo "Welcome to $HOSTNAME $(hostname -I)" -echo "$(cat /sys/class/net/enp4s0/address)" +echo "$(cat /sys/class/net/enp1s0/address) .. $(cat /sys/class/net/enp4s0/address)" export EDITOR=nano function service_status () { for name in $@; do \ - echo ${name} $(systemctl is-active ${name}) $(systemctl is-enabled ${name}); \ + echo ${name} $(systemctl is-active ${name}) $(systemctl is-enabled ${name} 2> /dev/null); \ done | column -t | grep --color=always '\(disabled\|inactive\|$\)' } -service_status router frappy +alias current='cat /root/aputools/current' +service_status router frappy display +echo "> current # show currently installed state"