From 68f0d30a546b14f9e6337a464c73ac2346ecdcf1 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 7 Aug 2023 08:46:35 +0200 Subject: [PATCH 01/23] fix MORE_INFO -> more_info --- install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.py b/install.py index 4f5dda4..250f7f5 100755 --- a/install.py +++ b/install.py @@ -267,7 +267,7 @@ class Show: print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) else: 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))) def show(self, title, dirpath, files): @@ -275,7 +275,7 @@ class Show: print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) else: for f in files: - if f.endswith(MORE_INFO): + if f.endswith(more_info): print('cat %s' % join(dirpath, f)) def newer(self, files): From 9a782cb28358a208b3a01a5f78eaef868c1e7408 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Mon, 7 Aug 2023 11:27:19 +0200 Subject: [PATCH 02/23] add info/log connection to router --- requirements.txt | 1 + router.py | 73 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2804b73..8fb62df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pyserial ipaddress +dhcp-leases 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()) From b19d1368c20ac510230e18ad85ccdb002082dd2e Mon Sep 17 00:00:00 2001 From: LIN SE Date: Mon, 7 Aug 2023 13:28:26 +0200 Subject: [PATCH 03/23] remove superfluous dhcp-leases from requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8fb62df..2804b73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ pyserial ipaddress -dhcp-leases From fdef0b69cfb66ec18adf70bfee172b11b5804542 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Tue, 8 Aug 2023 11:11:13 +0200 Subject: [PATCH 04/23] add 'current' alias showing installed state --- install.py | 12 ++++++++++-- servercfg/linseapu6_5fa5cc.cfg | 11 +++++++++++ to_system/etc/profile.d/welcome.sh | 2 ++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 servercfg/linseapu6_5fa5cc.cfg diff --git a/install.py b/install.py index 250f7f5..3ef02b1 100755 --- a/install.py +++ b/install.py @@ -183,7 +183,7 @@ def write_when_new(filename, content, ignore_reduction=False): print('>>>') print('\n'.join(old[-bottom_lines:-1])) print("===") - return True + return content def create_if(name, cfg, mac, dhcp_server_cfg): @@ -447,7 +447,13 @@ 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) doit = False @@ -465,5 +471,7 @@ elif show.dirty and more_info == 'NONE': if answer.lower().startswith('y'): handle_config() walk(Do()) + with open('/root/aputools/current', 'w') as f: + f.write(result) else: print('nothing to do') 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..6d5eaf3 100755 --- a/to_system/etc/profile.d/welcome.sh +++ b/to_system/etc/profile.d/welcome.sh @@ -6,4 +6,6 @@ function service_status () { echo ${name} $(systemctl is-active ${name}) $(systemctl is-enabled ${name}); \ done | column -t | grep --color=always '\(disabled\|inactive\|$\)' } +alias current='cat /root/aputools/current' service_status router frappy +echo "> current # show currently installed state" From 7f7993ef9642f5bad2ff29946451d5fa9fe46c68 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 10 Jan 2024 15:14:40 +0100 Subject: [PATCH 05/23] added first control box - added python display daemon - on control boxes, the uplink is the leftmost plug by default --- display.py | 234 +++++++++++++++++++++++++++++ install.py | 83 +++++++--- servercfg/linse-box1_5b265c.cfg | 9 ++ to_system/etc/profile.d/welcome.sh | 6 +- 4 files changed, 309 insertions(+), 23 deletions(-) create mode 100644 display.py create mode 100644 servercfg/linse-box1_5b265c.cfg diff --git a/display.py b/display.py new file mode 100644 index 0000000..7aa4352 --- /dev/null +++ b/display.py @@ -0,0 +1,234 @@ +import sys +import os +import time +import serial +import socket +import threading + +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 + +IDENT = 0xf3 + +FONT_GEO = [(6, 8), (8, 16), (20, 40)] + +TOPGAP = 8 # upper part of display is not useable +HEIGHT = 120 +WIDTH = 480 + + +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 + thread = None + bar = None + blinkchar = ' ' + menu = None + + def __init__(self, dev='/dev/ttyS1', timeout=0.5): + self.event = threading.Event() + self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) + self.gethost(False) + self.reset() + + def reset(self): + self.send(MODE_GRAPH) + self.font(2) + self.send(CLEAR, self.bg) + + def start(self): + if self.thread is None or not self.thread.is_alive(): + self.thread = threading.Thread(target=self.gethostthread) + self.thread.daemon = True + self.thread.start() + self.event.wait(1) # wait for thread to be started + print('refresh') + self.refresh() + + def color(self, fg=15, bg=0): + self.fg = fg + self.bg = bg + + def send(self, code, *args): + out = [code] + for arg in args: + if isinstance(arg, bytes): + out.extend(list(arg)) + elif isinstance(arg, str): + out.append(arg.encode('latin-1')) + else: + out.append(arg) + self.term.write(bytes([ESC,ESC,len(out)] + out)) + + 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.send(SET_COLOR, self.bg, self.bg, self.fg, self.fg) + + 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 = '' + if self.hostip and truehostname: + self.hostname = socket.gethostbyaddr(self.hostip)[0] + else: + self.hostname = 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] + print(x) + 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.' + self.text(hostip.replace('.', self.blinkchar, 1), 0, 0, 15) + self.text(self.hostname, 1) + self.event.set() + self.blinkchar = ' ' if self.blinkchar == '.' else '.' + 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) + + +d = Display() + +if len(sys.argv) == 1: + d.start() + while True: + d.refresh() + diff --git a/install.py b/install.py index 3ef02b1..99c056a 100755 --- a/install.py +++ b/install.py @@ -12,6 +12,7 @@ import sys import os import filecmp import shutil +import serial from subprocess import Popen, PIPE from glob import glob from ipaddress import IPv4Interface @@ -26,17 +27,28 @@ CFGPATH = '/root/aputools/servercfg/%s_%6.6x.cfg' 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} +""" DHCP_HEADER = """ default-lease-time 600; @@ -66,6 +78,19 @@ 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 + +[Install] +WantedBy = multi-user.target +""" + pip_requirements = { 'l_samenv': {}, 'root': {} @@ -95,6 +120,10 @@ def router(**opts): return ROUTER_TEMPLATE +def display(**opts): + return DISPLAY_TEMPLATE + + def pip(): for user, requirements in pip_requirements.items(): if user == 'root': @@ -118,7 +147,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): @@ -308,36 +337,50 @@ class Do: os.remove(join(self.syspath, file)) -IFNAMES = ['enp%ds0' % i for i in range(1,5)] - +netaddr_dict = {} +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: + netaddr_dict[netif.name] = netaddr = f.read().strip().lower() 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)) + shortids = {} + for netif, addr in netaddr_dict.items(): + shortid = int(''.join(addr.split(':')[-3:]), 16) & 0xffffff + cfgfiles += glob(CFGPATH % ('*', shortid)) + shortids[netif] = shortid with open('/etc/hostname') as f: hostname = f.read().strip() if not cfgfiles: - print('no cfg file found for %s' % hostname) + 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' + ids = sorted(shortids) + if display_available: + # leftmost interface labelled 'eth0' + apuid = shortids[ids[0]] + typ = 'controlbox' + template = CONTROLBOX_TEMPLATE + else: + # rightmost interface + apuid = shortid[idx[-1]] + typ = 'bare apu' + template = CONFIG_TEMPLATE + print('no cfg file found for this', typ, f'with id {apuid:%6.6x} (hostname={hostname})') newname = input('enter host name: ') if not newname: print('no hostname given') return False cfgfile = CFGPATH % (newname, 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] + apuid = int(cfgfile.split('_')[-1].split('.')[0], 16) if cfgfile != CFGPATH % (hostname, apuid): if doit: os.system('sh ../sethostname.sh') @@ -354,8 +397,8 @@ def handle_config(): 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) + for ifname, addr in netaddr_dict.items(): + content = create_if(ifname, network.pop(ifname, 'off'), addr, dhcp_server_cfg) 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': diff --git a/servercfg/linse-box1_5b265c.cfg b/servercfg/linse-box1_5b265c.cfg new file mode 100644 index 0000000..6e62f83 --- /dev/null +++ b/servercfg/linse-box1_5b265c.cfg @@ -0,0 +1,9 @@ +[NETWORK] +; please refer to README.md for help +enp1s0=dhcp +enp2s0=192.168.1.1 +enp3s0=192.168.2.2 +enp4s0=192.168.3.3 + +[DISPLAY] +; please refer to README.md for help diff --git a/to_system/etc/profile.d/welcome.sh b/to_system/etc/profile.d/welcome.sh index 6d5eaf3..b4b10e3 100755 --- a/to_system/etc/profile.d/welcome.sh +++ b/to_system/etc/profile.d/welcome.sh @@ -1,11 +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\|$\)' } alias current='cat /root/aputools/current' -service_status router frappy +service_status router frappy display echo "> current # show currently installed state" From c88afce79123f3359746a726b27cb911f48b94b3 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Thu, 11 Jan 2024 08:44:52 +0100 Subject: [PATCH 06/23] support setting startup - in interactive mode, support setting startup - standard startup shows ethernet address - colored blinking dot - need argument for running as daemon: python3 display.py -d - interactive mode: python3 -i display.py --- display.py | 91 +++++++++++++++++++++++++++++++++++++----------------- install.py | 2 +- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/display.py b/display.py index 7aa4352..4dd7a60 100644 --- a/display.py +++ b/display.py @@ -29,6 +29,8 @@ TOUCH_MODE = 0x51 SAVE_ATTRS = 0xa0 SEL_ATTRS = 0xc0 +SET_STARTUP = 0xf2 + IDENT = 0xf3 FONT_GEO = [(6, 8), (8, 16), (20, 40)] @@ -47,45 +49,62 @@ class Display: fg = 15 bg = 0 touch = 0 - thread = None bar = None - blinkchar = ' ' + blink = False menu = None + storage = None - def __init__(self, dev='/dev/ttyS1', timeout=0.5): + def __init__(self, dev, timeout=0.5, daemon=False): self.event = threading.Event() self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) + if not daemon: + self.storage = bytearray() 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.refresh() def reset(self): self.send(MODE_GRAPH) self.font(2) self.send(CLEAR, self.bg) - def start(self): - if self.thread is None or not self.thread.is_alive(): - self.thread = threading.Thread(target=self.gethostthread) - self.thread.daemon = True - self.thread.start() - self.event.wait(1) # wait for thread to be started - print('refresh') - self.refresh() - 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 = [code] + out = bytearray([code]) for arg in args: - if isinstance(arg, bytes): - out.extend(list(arg)) - elif isinstance(arg, str): - out.append(arg.encode('latin-1')) + if isinstance(arg, str): + out.extend(arg.encode('latin-1')) + elif hasattr(arg, '__len__'): + out.extend(arg) else: out.append(arg) - self.term.write(bytes([ESC,ESC,len(out)] + out)) + 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 standard_startup(self): + self.storage = bytearray() + self.reset() + with open('/sys/class/net/enp1s0/address') as f: + netaddr = f.read().strip().lower() + self.show('startup ...', netaddr) + self.set_startup() def version(self): self.term.write(bytes([ESC,ESC,1,0xf3])) @@ -100,7 +119,7 @@ class Display: self.nrows = HEIGHT // self.rowhei self.ncols = WIDTH // self.colwid self.textbuffer = [" " * self.ncols] * self.nrows - self.send(SET_COLOR, self.bg, self.bg, self.fg, self.fg) + self.color() def text(self, text, row=0, left=0, right=None, font=2): if font != self.fontsize: @@ -132,10 +151,13 @@ class Display: self.hostip = s.getsockname()[0] except Exception as e: self.hostip = '' + hostname = None if self.hostip and truehostname: - self.hostname = socket.gethostbyaddr(self.hostip)[0] - else: - self.hostname = socket.gethostname() + try: + hostname = socket.gethostbyaddr(self.hostip)[0] + except Exception: + pass + self.hostname = hostname or socket.gethostname() def gethostthread(self): self.gethost(True) @@ -208,11 +230,17 @@ class Display: def net_display(self, x): if x is None: - hostip = self.hostip or 'no network.' - self.text(hostip.replace('.', self.blinkchar, 1), 0, 0, 15) + 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.blinkchar = ' ' if self.blinkchar == '.' else '.' + self.blink = not self.blink else: self.menu = self.menu_main self.show('(cancel)(reboot)(shdown)') @@ -225,10 +253,17 @@ class Display: self.send(0xf0, 0xcb, 0xef, 0x20, 0x18) -d = Display() +tty = '/dev/ttyS1' +daemon = False -if len(sys.argv) == 1: - d.start() +for arg in sys.argv[1:]: + if arg == '-d': + daemon = True + else: + tty = arg + +d = Display(tty, daemon=daemon) +if daemon: while True: d.refresh() diff --git a/install.py b/install.py index 99c056a..7e06ab6 100755 --- a/install.py +++ b/install.py @@ -85,7 +85,7 @@ After = network.target [Service] User = root -ExecStart = /usr/bin/python3 /root/aputools/display.py +ExecStart = /usr/bin/python3 /root/aputools/display.py -d [Install] WantedBy = multi-user.target From 73b1a9a97d07943b1a5e4c110b7bddbff16ce5d8 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Thu, 11 Jan 2024 13:08:36 +0100 Subject: [PATCH 07/23] wip temporary push --- display.py | 31 ++++++---- install.py | 105 ++++++++++++++++++++++++-------- servercfg/linse-box1_5b265c.cfg | 3 + 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/display.py b/display.py index 4dd7a60..e4cf402 100644 --- a/display.py +++ b/display.py @@ -5,6 +5,10 @@ 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)) @@ -57,8 +61,22 @@ class Display: def __init__(self, dev, timeout=0.5, daemon=False): self.event = threading.Event() self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) - if not daemon: - self.storage = bytearray() + self.storage = bytearray() + if daemon: + text = None + try: + with open(STARTUP_TEXT) as f: + text = f.read() + print('new startup text:') + print(text) + except FileNotFoundError: + pass + if text: + self.reset() + self.show(*todo_text.split('\n')[0:3]) + self.set_startup() + else: + self.storage = None self.gethost(False) self.reset() if daemon: @@ -98,14 +116,6 @@ class Display: self.storage = None self.send(SET_STARTUP, data) - def standard_startup(self): - self.storage = bytearray() - self.reset() - with open('/sys/class/net/enp1s0/address') as f: - netaddr = f.read().strip().lower() - self.show('startup ...', netaddr) - self.set_startup() - def version(self): self.term.write(bytes([ESC,ESC,1,0xf3])) reply = self.term.read(4) @@ -253,7 +263,6 @@ class Display: self.send(0xf0, 0xcb, 0xef, 0x20, 0x18) -tty = '/dev/ttyS1' daemon = False for arg in sys.argv[1:]: diff --git a/install.py b/install.py index 7e06ab6..e74e0f9 100755 --- a/install.py +++ b/install.py @@ -13,6 +13,7 @@ import os import filecmp import shutil import serial +import types from subprocess import Popen, PIPE from glob import glob from ipaddress import IPv4Interface @@ -24,6 +25,7 @@ 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" @@ -48,6 +50,9 @@ enp4s0=192.168.3.3 [DISPLAY] {COMMENT} +line0=startup... +line1=HOST +line2=ADDR """ DHCP_HEADER = """ @@ -97,6 +102,11 @@ pip_requirements = { } +net_addr = {} # dict of +main_info = {} # info about WAN interface +dhcp_server_cfg = [] # configuration for dhcp + + def frappy(cfg=None, port=None, requirements='', **kwds): if not cfg: return None @@ -120,7 +130,37 @@ def router(**opts): return ROUTER_TEMPLATE +def display_update(cfg): + dirty = False + lines = [] + for key in ['line0', 'line1', 'line2']: + value = cfg.get(key, '') + if value.startswith('HOST'): + line = main_info.get('hostname', '') + value = 'HOST: ' + line + elif value.startswith('ADDR'): + line = main_info.get('addr', '') + value = 'ADDR: ' + line + else: + line = value + lines.append(line) + if cfg.get(key) != value: + if doit: + cfg[key] = value + dirty = True + if dirty: + text = '\n'.join(lines) + if doit: + with open(STARTUP_TEXT, 'w') as f: + f.write(text) + else: + print('new startup text:') + print('\n'.join(lines)) + + def display(**opts): + if not opts: + return None return DISPLAY_TEMPLATE @@ -215,7 +255,7 @@ def write_when_new(filename, content, ignore_reduction=False): return content -def create_if(name, cfg, mac, dhcp_server_cfg): +def create_if(name, cfg): result = dict( TYPE='Ethernet', NAME=name, @@ -229,6 +269,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 @@ -337,68 +381,72 @@ class Do: os.remove(join(self.syspath, file)) -netaddr_dict = {} 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: - netaddr_dict[netif.name] = netaddr = f.read().strip().lower() + 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) & 0xffffff + def handle_config(): cfgfile = None cfgfiles = [] - shortids = {} - for netif, addr in netaddr_dict.items(): - shortid = int(''.join(addr.split(':')[-3:]), 16) & 0xffffff - cfgfiles += glob(CFGPATH % ('*', shortid)) - shortids[netif] = shortid + for file in glob(CFGPATH % ('*', apuid)): + cfgfiles.append(file) with open('/etc/hostname') as f: hostname = f.read().strip() + newhostname = hostname if not cfgfiles: 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' - ids = sorted(shortids) if display_available: # leftmost interface labelled 'eth0' - apuid = shortids[ids[0]] + wan_if = sorted_if[0] typ = 'controlbox' template = CONTROLBOX_TEMPLATE else: # rightmost interface - apuid = shortid[idx[-1]] + wan_id = 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})') - newname = input('enter host name: ') - if not newname: + 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(template) elif len(cfgfiles) > 1: print('ERROR: ambiguous cfg files: %s' % ', '.join(cfgfiles)) else: cfgfile = cfgfiles[0] - apuid = int(cfgfile.split('_')[-1].split('.')[0], 16) + wan_if = None if cfgfile != CFGPATH % (hostname, 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, addr in netaddr_dict.items(): - content = create_if(ifname, network.pop(ifname, 'off'), addr, dhcp_server_cfg) + 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': @@ -434,7 +482,10 @@ 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': + display_update(section_dict) + for key, value in section_dict.items(): content.append('%s=%s' % (key, value)) content.append('') if dirty: @@ -446,12 +497,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 @@ -460,7 +511,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 @@ -472,10 +523,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() diff --git a/servercfg/linse-box1_5b265c.cfg b/servercfg/linse-box1_5b265c.cfg index 6e62f83..3a8dcbf 100644 --- a/servercfg/linse-box1_5b265c.cfg +++ b/servercfg/linse-box1_5b265c.cfg @@ -7,3 +7,6 @@ enp4s0=192.168.3.3 [DISPLAY] ; please refer to README.md for help +line0=startup... +line1=HOST: linse-box1 +line2=ADDR: 00:0d:b9:5b:26:5c From ef95e4e383127b30e2d5480ebc574d64169c9b1b Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 11 Jan 2024 16:59:58 +0100 Subject: [PATCH 08/23] install.py: use class instead of global variables --- install.py | 855 ++++++++++++++++++++++++++--------------------------- 1 file changed, 423 insertions(+), 432 deletions(-) diff --git a/install.py b/install.py index e74e0f9..eed8d8d 100755 --- a/install.py +++ b/install.py @@ -61,277 +61,24 @@ max-lease-time 7200; authoritative; """ -ROUTER_TEMPLATE = """[Unit] -Description = Routing to locally connected hardware -After = network.target - -[Service] -ExecStart = /usr/bin/python3 /root/aputools/router.py - -[Install] -WantedBy = multi-user.target -""" - -FRAPPY_TEMPLATE = """[Unit] -Description = Running frappy server -After = network.target - -[Service] -User = l_samenv -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 -""" - -pip_requirements = { - 'l_samenv': {}, - 'root': {} -} - - -net_addr = {} # dict of -main_info = {} # info about WAN interface -dhcp_server_cfg = [] # configuration for dhcp - - -def frappy(cfg=None, port=None, requirements='', **kwds): - if not cfg: - return None - req = pip_requirements['l_samenv'] - req[cfg] = '\n'.join(requirements.split(',')) - if port: - cfg = '-p %s %s' % (port, cfg) - with open('/home/l_samenv/frappy/requirements.txt') as f: - req['frappy main'] = f.read() - return FRAPPY_TEMPLATE % cfg - - -def router(**opts): - if not opts: - return None - try: - with open('/root/aputools/requirements.txt') as f: - pip_requirements['root']['aputools'] = f.read() - except FileNotFoundError: - pass - return ROUTER_TEMPLATE - - -def display_update(cfg): - dirty = False - lines = [] - for key in ['line0', 'line1', 'line2']: - value = cfg.get(key, '') - if value.startswith('HOST'): - line = main_info.get('hostname', '') - value = 'HOST: ' + line - elif value.startswith('ADDR'): - line = main_info.get('addr', '') - value = 'ADDR: ' + line - else: - line = value - lines.append(line) - if cfg.get(key) != value: - if doit: - cfg[key] = value - dirty = True - if dirty: - text = '\n'.join(lines) - if doit: - with open(STARTUP_TEXT, 'w') as f: - f.write(text) - else: - print('new startup text:') - print('\n'.join(lines)) - - -def display(**opts): - if not opts: - return None - return DISPLAY_TEMPLATE - - -def pip(): - for user, requirements in pip_requirements.items(): - if user == 'root': - tmpname = join('/root', 'pip_requirements.tmp') - pipcmd = 'pip3 install -r %s' % tmpname - else: - tmpname = join('/home', user, 'pip_requirements.tmp') - pipcmd = 'sudo --user %s pip3 install --user -r %s' % (user, tmpname) - filename = tmpname.replace('.tmp', '.txt') - content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items()) - if write_when_new(filename, content, True): - if doit: - os.rename(filename, tmpname) - if os.system(pipcmd) == 0: - os.rename(tmpname, filename) - else: - os.remove(tmpname) - else: - print(pipcmd) - # unix_cmd(pipcmd, stdout=None) - show.dirty = True - - SERVICES = dict(router=router, frappy=frappy, display=display) -def unix_cmd(command, always=False, stdout=PIPE): - if doit or always: - if not always: - print('$ %s' % command) - result = Popen(command.split(), stdout=stdout).communicate()[0] - return (result or b'').decode() - else: - print('> %s' % command) +net_addr = {} # dict of +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 -def write_when_new(filename, content, ignore_reduction=False): - if content is None: - lines = [] - else: - if not content.endswith('\n'): - content += '\n' - lines = content.split('\n') - try: - with open(filename) as fil: - old = fil.read() - except FileNotFoundError: - old = None - if old == content: - return False - if ignore_reduction: - content_set = set(v for v in lines if not v.startswith('#')) - 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))) - else: - return False - if doit: - if lines: - with open(filename, 'w') as fil: - fil.write(content) - else: - os.remove(filename) - elif filename.endswith(more_info): - print('changes in', filename) - old = [] if old is None else old.split('\n') - top_lines = 0 # in case of empty loop - for top_lines, (ol, ll) in enumerate(zip(old, lines)): - if ol != ll: - break - bottom_lines = 0 - for bottom_lines, (ol, ll) in enumerate(zip(reversed(old[top_lines:]), reversed(lines[top_lines:]))): - if ol != ll: - break - if bottom_lines == 0: - old.append('') - lines.append('') - bottom_lines += 1 - print("===") - print('\n'.join(old[:top_lines])) - print('<<<') - print('\n'.join(old[top_lines:-bottom_lines])) - print('---') - print('\n'.join(lines[top_lines:-bottom_lines])) - print('>>>') - print('\n'.join(old[-bottom_lines:-1])) - print("===") - return content +class Walker: + def __init__(self): + self.dirpath = None + self.syspath = None -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' - 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', [])) - dhcp_server_cfg.append(('128.0.0.0/192.0.0.0', [])) - for nw in cfg.split(',')[1:]: - nw = IPv4Interface(nw).network - dhcp_server_cfg.append((nw.with_netmask, [])) - else: - cfgip = IPv4Interface(cfg) - network = cfgip.network - if network.prefixlen == 32: # or no prefix specified - otherip = IPv4Interface('%s/24' % cfgip.ip) - network = otherip.network - if str(cfgip.ip).endswith('.1'): - cfgip = network.network_address + 2 - else: - cfgip = network.network_address + 1 - dhcp_server_cfg.append((network.with_netmask, [(str(otherip.ip), str(otherip.ip))])) - result['IPADDR'] = 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 - - -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: +class Show(Walker): dirty = False def diff(self, title, files): @@ -364,7 +111,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)) @@ -381,191 +128,435 @@ class Do: os.remove(join(self.syspath, file)) -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 +class Install: + ROUTER_TEMPLATE = """[Unit] + Description = Routing to locally connected hardware + After = network.target -sorted_if = sorted(net_addr) + [Service] + ExecStart = /usr/bin/python3 /root/aputools/router.py -apuid = int(''.join(net_addr[sorted_if[0]].split(':')[-3:]), 16) & 0xffffff + [Install] + WantedBy = multi-user.target + """ + FRAPPY_TEMPLATE = """[Unit] + Description = Running frappy server + After = network.target -def handle_config(): - cfgfile = None - cfgfiles = [] - for file in glob(CFGPATH % ('*', apuid)): - cfgfiles.append(file) - with open('/etc/hostname') as f: - hostname = f.read().strip() - newhostname = hostname - if not cfgfiles: - 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' - wan_if = sorted_if[0] - typ = 'controlbox' - template = CONTROLBOX_TEMPLATE - else: - # rightmost interface - wan_id = 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 % (newhostname, apuid) - with open(cfgfile, 'w') as f: - f.write(template) - elif len(cfgfiles) > 1: - print('ERROR: ambiguous cfg files: %s' % ', '.join(cfgfiles)) - else: - cfgfile = cfgfiles[0] - wan_if = None - if cfgfile != CFGPATH % (hostname, apuid): - if cfgfile: - newhostname = basename(cfgfile).rpartition('_')[0] - if doit: - os.system('sh sethostname.sh') - else: - if cfgfile: - 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() - try: - parser.read(cfgfile) - network = dict(parser['NETWORK']) - 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': - print('change', ifname) - show.dirty = True - to_start[ifname] = 'if_restart' - if dhcp_server_cfg: - content = [DHCP_HEADER] - for subnet, rangelist in dhcp_server_cfg: - try: - adr, mask = subnet.split('/') - except Exception as e: - print(subnet, repr(e)) - continue - content.append('subnet %s netmask %s {\n' % (adr, mask)) - #content.append(' option netmask %s;\n' % mask) - for rng in rangelist: - content.append(' range %s %s;\n' % rng) - content.append('}\n') - content = ''.join(content) - todo = write_when_new('/etc/dhcp/dhcpd.conf', content) - if todo: - print('change dhcpd.conf') - to_start['dhcpd'] = 'restart' - show.dirty = True - elif doit: - unix_cmd('systemctl stop dhcpd') - unix_cmd('systemctl disable dhcpd') - content = [] - dirty = False - for section in parser.sections(): - if COMMENT not in parser[section]: - dirty = True - content.append('[%s]' % section) - content.append(COMMENT) - section_dict = dict(parser[section].items()) - if section == 'DISPLAY': - display_update(section_dict) - for key, value in section_dict.items(): - content.append('%s=%s' % (key, value)) - content.append('') - if dirty: + [Service] + User = l_samenv + 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 + """ + + main_if = '' + main_addr = '' + + def __init__(self, doit): + if not doit: + self.to_system = Show() + walk(self.to_system) + self.doit = doit + self.current = None + self.sorted_if = sorted(net_addr) + self.apuid = int(''.join(net_addr[sorted_if[0]].split(':')[-3:]), 16) & 0xfffffc + self.dhcp_server_cfg = [] # configuration for dhcp + self.pip_requirements = { + 'l_samenv': {}, + 'root': {} + } + cfgfile = None + cfgfiles = [] + for file in glob(CFGPATH % ('*', apuid)): + cfgfiles.append(file) + with open('/etc/hostname') as f: + hostname = f.read().strip() + self.hostname = hostname + if not cfgfiles: + 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' + typ = 'controlbox' + template = CONTROLBOX_TEMPLATE + else: + # rightmost interface + typ = 'bare apu' + template = CONFIG_TEMPLATE + print('no cfg file found for this', typ, f'with id {apuid:%6.6x} (hostname={hostname})') + self.hostname = input('enter host name: ') + if not self.hostname: + print('no hostname given') + return + cfgfile = CFGPATH % (self.hostname, apuid) 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)) - raise - return False - - reload_systemd = False - for service, service_func in SERVICES.items(): - section = service.upper() - if parser.has_section(section): - servicecfg = service_func(**dict(parser[section])) + f.write(template) + elif len(cfgfiles) > 1: + print('ERROR: ambiguous cfg files: %s' % ', '.join(cfgfiles)) else: - servicecfg = None - result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) - active = False - enabled = False - for line in result.split('\n'): - if line.startswith('WantedBy=m'): - enabled = True - elif line.strip() == 'ActiveState=active': - active = True - if servicecfg is None: - if active: - unix_cmd('systemctl stop %s' % service) - show.dirty = True - if enabled: - unix_cmd('systemctl disable %s' % service) - show.dirty = True + cfgfile = cfgfiles[0] + if cfgfile != CFGPATH % (hostname, apuid): + if cfgfile: + self.hostname = basename(cfgfile).rpartition('_')[0] + if doit: + os.system('sh sethostname.sh') + else: + if cfgfile: + print('replace host name %r by %r' % (hostname, self.hostname)) + self.to_system.dirty = True + if cfgfile is None: + raise ValueError('no config file') + to_start = {} + ifname = '' + parser = ConfigParser() + try: + parser.read(cfgfile) + network = dict(parser['NETWORK']) + 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': + print('change', ifname) + self.to_system.dirty = True + to_start[ifname] = 'if_restart' + if self.dhcp_server_cfg: + content = [DHCP_HEADER] + for subnet, rangelist in self.dhcp_server_cfg: + try: + adr, mask = subnet.split('/') + except Exception as e: + print(subnet, repr(e)) + continue + content.append('subnet %s netmask %s {\n' % (adr, mask)) + # content.append(' option netmask %s;\n' % mask) + for rng in rangelist: + content.append(' range %s %s;\n' % rng) + content.append('}\n') + content = ''.join(content) + todo = write_when_new('/etc/dhcp/dhcpd.conf', content) + if todo: + print('change dhcpd.conf') + to_start['dhcpd'] = 'restart' + self.to_system.dirty = True + elif doit: + unix_cmd('systemctl stop dhcpd') + unix_cmd('systemctl disable dhcpd') + content = [] + dirty = False + for section in parser.sections(): + if COMMENT not in parser[section]: + dirty = True + content.append('[%s]' % section) + content.append(COMMENT) + section_dict = dict(parser[section].items()) + if section == 'DISPLAY': + display_update(section_dict) + for key, value in section_dict.items(): + content.append('%s=%s' % (key, value)) + content.append('') + if dirty: + 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)) + raise + + reload_systemd = False + for service, service_func in SERVICES.items(): + section = service.upper() + if parser.has_section(section): + servicecfg = service_func(**dict(parser[section])) + else: + servicecfg = None + result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) + active = False + enabled = False + for line in result.split('\n'): + if line.startswith('WantedBy=m'): + enabled = True + elif line.strip() == 'ActiveState=active': + active = True + if servicecfg is None: + if active: + unix_cmd('systemctl stop %s' % service) + self.to_system.dirty = True + if enabled: + unix_cmd('systemctl disable %s' % service) + self.to_system.dirty = True + else: + if not enabled: + to_start[service] = 'enable' + elif not active: + to_start[service] = 'restart' + if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): + self.to_system.dirty = True + reload_systemd = True + if servicecfg and to_start.get('service') is None: + to_start[service] = 'restart' + + pip() + if reload_systemd: + unix_cmd('systemctl daemon-reload') + for service, action in to_start.items(): + self.to_system.dirty = True + if action == 'if_restart': + unix_cmd('ifdown %s' % service) + unix_cmd('ifup %s' % service) + else: + if action == 'restart': + unix_cmd('systemctl restart %s' % service) + unix_cmd('systemctl enable %s' % service) + 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('') + self.current = '\n'.join(result) + + def unix_cmd(self, command, always=False, stdout=PIPE): + if self.doit or always: + if not always: + print('$ %s' % command) + result = Popen(command.split(), stdout=stdout).communicate()[0] + return (result or b'').decode() else: - if not enabled: - to_start[service] = 'enable' - elif not active: - to_start[service] = 'restart' - if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): - show.dirty = True - reload_systemd = True - if servicecfg and to_start.get('service') is None: - to_start[service] = 'restart' + print('> %s' % command) - pip() - if reload_systemd: - unix_cmd('systemctl daemon-reload') - for service, action in to_start.items(): - show.dirty = True - if action == 'if_restart': - unix_cmd('ifdown %s' % service) - unix_cmd('ifup %s' % service) + def write_when_new(self, filename, content, ignore_reduction=False): + if content is None: + lines = [] else: - if action == 'restart': - unix_cmd('systemctl restart %s' % service) - unix_cmd('systemctl enable %s' % service) - 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) + if not content.endswith('\n'): + content += '\n' + lines = content.split('\n') + try: + with open(filename) as f: + old = f.read() + except FileNotFoundError: + old = None + if old == content: + return False + if ignore_reduction: + content_set = set(v for v in lines if not v.startswith('#')) + 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))) + else: + return False + if self.doit: + if lines: + with open(filename, 'w') as f: + f.write(content) + else: + os.remove(filename) + elif filename.endswith(more_info): + print('changes in', filename) + old = [] if old is None else old.split('\n') + top_lines = 0 # in case of empty loop + for top_lines, (ol, ll) in enumerate(zip(old, lines)): + if ol != ll: + break + bottom_lines = 0 + for bottom_lines, (ol, ll) in enumerate(zip(reversed(old[top_lines:]), reversed(lines[top_lines:]))): + if ol != ll: + break + if bottom_lines == 0: + old.append('') + lines.append('') + bottom_lines += 1 + print("===") + print('\n'.join(old[:top_lines])) + print('<<<') + print('\n'.join(old[top_lines:-bottom_lines])) + print('---') + print('\n'.join(lines[top_lines:-bottom_lines])) + print('>>>') + print('\n'.join(old[-bottom_lines:-1])) + print("===") + return content + + @staticmethod + def frappy(cfg=None, port=None, requirements='', **kwds): + if not cfg: + return None + req = pip_requirements['l_samenv'] + req[cfg] = '\n'.join(requirements.split(',')) + if port: + cfg = '-p %s %s' % (port, cfg) + with open('/home/l_samenv/frappy/requirements.txt') as f: + req['frappy main'] = f.read() + return FRAPPY_TEMPLATE % cfg + + def router(self, **opts): + if not opts: + return None + try: + with open('/root/aputools/requirements.txt') as f: + pip_requirements['root']['aputools'] = f.read() + except FileNotFoundError: + pass + return self.ROUTER_TEMPLATE + + def display_update(self, cfg): + dirty = False + lines = [] + for key in ['line0', 'line1', 'line2']: + value = cfg.get(key, '') + if value.startswith('HOST'): + line = self.hostname + value = 'HOST: ' + line + elif value.startswith('ADDR'): + line = self.main_addr + value = 'ADDR: ' + line + else: + line = value + lines.append(line) + if cfg.get(key) != value: + if self.doit: + cfg[key] = value + dirty = True + if dirty: + text = '\n'.join(lines) + if self.doit: + with open(STARTUP_TEXT, 'w') as f: + f.write(text) + else: + print('new startup text:') + print('\n'.join(lines)) + + def display(self, **opts): + if not opts: + return None + return self.DISPLAY_TEMPLATE + + def pip(self): + for user, requirements in pip_requirements.items(): + if user == 'root': + tmpname = join('/root', 'pip_requirements.tmp') + pipcmd = 'pip3 install -r %s' % tmpname + else: + tmpname = join('/home', user, 'pip_requirements.tmp') + pipcmd = 'sudo --user %s pip3 install --user -r %s' % (user, tmpname) + filename = tmpname.replace('.tmp', '.txt') + content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items()) + if write_when_new(filename, content, True): + if self.doit: + os.rename(filename, tmpname) + if os.system(pipcmd) == 0: + os.rename(tmpname, filename) + else: + os.remove(tmpname) + else: + print(pipcmd) + # unix_cmd(pipcmd, stdout=None) + self.to_system.dirty = True + + def create_if(self, 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' + elif cfg.startswith('wan') or cfg == 'dhcp': + if name != self.main_if and self.main_if + raise ValueError('can not have more than one WAN/DHCP port') + self.main_if = name + self.main_addr = net_addr[name] + result['BOOTPROTO'] = 'dhcp' + # default: all <= 192.0.0.0 + # others have to be added explicitly + self.dhcp_server_cfg.append(('0.0.0.0/128.0.0.0', [])) + self.dhcp_server_cfg.append(('128.0.0.0/192.0.0.0', [])) + for nw in cfg.split(',')[1:]: + nw = IPv4Interface(nw).network + self.dhcp_server_cfg.append((nw.with_netmask, [])) + else: + cfgip = IPv4Interface(cfg) + network = cfgip.network + if network.prefixlen == 32: # or no prefix specified + otherip = IPv4Interface('%s/24' % cfgip.ip) + network = otherip.network + if str(cfgip.ip).endswith('.1'): + cfgip = network.network_address + 2 + else: + cfgip = network.network_address + 1 + self.dhcp_server_cfg.append((network.with_netmask, [(str(otherip.ip), str(otherip.ip))])) + result['IPADDR'] = 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 + + def walk(self, 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 f: + to_delete = [] + for line in f: + line = line.strip() + if fline and exists(join(syspath, line)): + if exists(join(dirpath, line)): + print('ERROR: %s in %s, but also in repo -> ignored' % (line, DEL)) + else: + to_delete.append(f) + action.delete(to_delete) + if missing: + action.missing(missing) -doit = False print('---') -show = Show() -walk(show) -result = handle_config() +to_install = Install(False) if not result: print('fix first above errors') -elif show.dirty and more_info == 'NONE': +elif to_system.dirty and more_info == 'NONE': print('---') answer = input('do above? ') - doit = True if answer.lower().startswith('y'): - handle_config() + Install(True) walk(Do()) - with open('/root/aputools/current', 'w') as f: - f.write(result) + with open('/root/aputools/current', 'w') as fil: + fil.write(to_install.current) else: print('nothing to do') From fe1303efdc7be364b278578c5c284c2f555beb90 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 15 Jan 2024 09:58:03 +0100 Subject: [PATCH 09/23] revert prev. commit + wip fix --- display.py | 9 +- install.py | 872 +++++++++++++++++++++++++++-------------------------- 2 files changed, 450 insertions(+), 431 deletions(-) diff --git a/display.py b/display.py index e4cf402..c141dd8 100644 --- a/display.py +++ b/display.py @@ -63,18 +63,19 @@ class Display: self.term = serial.Serial(dev, baudrate=115200, timeout=timeout) self.storage = bytearray() if daemon: - text = None + todo_file = STARTUP_TEXT + '.todo' try: - with open(STARTUP_TEXT) as f: + with open(todo_file) as f: text = f.read() print('new startup text:') print(text) except FileNotFoundError: - pass + text = None if text: self.reset() - self.show(*todo_text.split('\n')[0:3]) + self.show(*text.split('\n')[0:3]) self.set_startup() + os.rename(todo_file, STARTUP_TEXT) else: self.storage = None self.gethost(False) diff --git a/install.py b/install.py index eed8d8d..ebbef16 100755 --- a/install.py +++ b/install.py @@ -61,16 +61,277 @@ max-lease-time 7200; authoritative; """ -SERVICES = dict(router=router, frappy=frappy, display=display) +ROUTER_TEMPLATE = """[Unit] +Description = Routing to locally connected hardware +After = network.target + +[Service] +ExecStart = /usr/bin/python3 /root/aputools/router.py + +[Install] +WantedBy = multi-user.target +""" + +FRAPPY_TEMPLATE = """[Unit] +Description = Running frappy server +After = network.target + +[Service] +User = l_samenv +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 +""" + +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': 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 + req = pip_requirements['l_samenv'] + req[cfg] = '\n'.join(requirements.split(',')) + if port: + cfg = '-p %s %s' % (port, cfg) + with open('/home/l_samenv/frappy/requirements.txt') as f: + req['frappy main'] = f.read() + return FRAPPY_TEMPLATE % cfg + + +def router(**opts): + if not opts: + return None + try: + with open('/root/aputools/requirements.txt') as f: + pip_requirements['root']['aputools'] = f.read() + except FileNotFoundError: + pass + return ROUTER_TEMPLATE + + +def display_update(cfg): + text = '\n'.join(cfg.get(key, '') for key in ('line0', 'line1', 'line2')) + text = text.replace('HOST', main_info['hostname']) \ + .replace('ADDR', main_info['addr']) + '\n' + try: + with open(STARTUP_TEXT) as f: + if f.read() == text: + return + except FileNotFoundError: + pass + print('new startup text:') + print(text) + with open(STARTUP_TEXT + '.todo', 'w') as f: + f.write(text) + + +def display(**opts): + if not opts: + return None + return DISPLAY_TEMPLATE + + +def pip(): + for user, requirements in pip_requirements.items(): + if user == 'root': + tmpname = join('/root', 'pip_requirements.tmp') + pipcmd = 'pip3 install -r %s' % tmpname + else: + tmpname = join('/home', user, 'pip_requirements.tmp') + pipcmd = 'sudo --user %s pip3 install --user -r %s' % (user, tmpname) + filename = tmpname.replace('.tmp', '.txt') + content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items()) + if write_when_new(filename, content, True): + if doit: + os.rename(filename, tmpname) + if os.system(pipcmd) == 0: + os.rename(tmpname, filename) + else: + os.remove(tmpname) + else: + print(pipcmd) + # unix_cmd(pipcmd, stdout=None) + show.dirty = True + + +SERVICES = dict(router=router, frappy=frappy, display=display) + + +def unix_cmd(command, always=False, stdout=PIPE): + if doit or always: + if not always: + print('$ %s' % command) + result = Popen(command.split(), stdout=stdout).communicate()[0] + return (result or b'').decode() + else: + print('> %s' % command) + + +def write_when_new(filename, content, ignore_reduction=False): + if content is None: + lines = [] + else: + if not content.endswith('\n'): + content += '\n' + lines = content.split('\n') + try: + with open(filename) as fil: + old = fil.read() + except FileNotFoundError: + old = None + if old == content: + return False + if ignore_reduction: + content_set = set(v for v in lines if not v.startswith('#')) + 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))) + else: + return False + if doit: + if lines: + with open(filename, 'w') as fil: + fil.write(content) + else: + os.remove(filename) + elif filename.endswith(more_info): + print('changes in', filename) + old = [] if old is None else old.split('\n') + top_lines = 0 # in case of empty loop + for top_lines, (ol, ll) in enumerate(zip(old, lines)): + if ol != ll: + break + bottom_lines = 0 + for bottom_lines, (ol, ll) in enumerate(zip(reversed(old[top_lines:]), reversed(lines[top_lines:]))): + if ol != ll: + break + if bottom_lines == 0: + old.append('') + lines.append('') + bottom_lines += 1 + print("===") + print('\n'.join(old[:top_lines])) + print('<<<') + print('\n'.join(old[top_lines:-bottom_lines])) + print('---') + print('\n'.join(lines[top_lines:-bottom_lines])) + print('>>>') + print('\n'.join(old[-bottom_lines:-1])) + print("===") + return content + + +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' + 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', [])) + dhcp_server_cfg.append(('128.0.0.0/192.0.0.0', [])) + for nw in cfg.split(',')[1:]: + nw = IPv4Interface(nw).network + dhcp_server_cfg.append((nw.with_netmask, [])) + else: + cfgip = IPv4Interface(cfg) + network = cfgip.network + if network.prefixlen == 32: # or no prefix specified + otherip = IPv4Interface('%s/24' % cfgip.ip) + network = otherip.network + if str(cfgip.ip).endswith('.1'): + cfgip = network.network_address + 2 + else: + cfgip = network.network_address + 1 + dhcp_server_cfg.append((network.with_netmask, [(str(otherip.ip), str(otherip.ip))])) + result['IPADDR'] = 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 + + +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 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(fname) + action.delete(to_delete) + if missing: + action.missing(missing) + class Walker: def __init__(self): @@ -128,435 +389,192 @@ class Do(Walker): os.remove(join(self.syspath, file)) -class Install: - ROUTER_TEMPLATE = """[Unit] - Description = Routing to locally connected hardware - After = network.target - - [Service] - ExecStart = /usr/bin/python3 /root/aputools/router.py - - [Install] - WantedBy = multi-user.target - """ - - FRAPPY_TEMPLATE = """[Unit] - Description = Running frappy server - After = network.target - - [Service] - User = l_samenv - 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 - """ - - main_if = '' - main_addr = '' - - def __init__(self, doit): - if not doit: - self.to_system = Show() - walk(self.to_system) - self.doit = doit - self.current = None - self.sorted_if = sorted(net_addr) - self.apuid = int(''.join(net_addr[sorted_if[0]].split(':')[-3:]), 16) & 0xfffffc - self.dhcp_server_cfg = [] # configuration for dhcp - self.pip_requirements = { - 'l_samenv': {}, - 'root': {} - } - cfgfile = None - cfgfiles = [] - for file in glob(CFGPATH % ('*', apuid)): - cfgfiles.append(file) - with open('/etc/hostname') as f: - hostname = f.read().strip() - self.hostname = hostname - if not cfgfiles: - 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' - typ = 'controlbox' - template = CONTROLBOX_TEMPLATE - else: - # rightmost interface - typ = 'bare apu' - template = CONFIG_TEMPLATE - print('no cfg file found for this', typ, f'with id {apuid:%6.6x} (hostname={hostname})') - self.hostname = input('enter host name: ') - if not self.hostname: - print('no hostname given') - return - cfgfile = CFGPATH % (self.hostname, apuid) - with open(cfgfile, 'w') as f: - 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: - self.hostname = basename(cfgfile).rpartition('_')[0] - if doit: - os.system('sh sethostname.sh') - else: - if cfgfile: - print('replace host name %r by %r' % (hostname, self.hostname)) - self.to_system.dirty = True - if cfgfile is None: - raise ValueError('no config file') - to_start = {} - ifname = '' - parser = ConfigParser() - try: - parser.read(cfgfile) - network = dict(parser['NETWORK']) - 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': - print('change', ifname) - self.to_system.dirty = True - to_start[ifname] = 'if_restart' - if self.dhcp_server_cfg: - content = [DHCP_HEADER] - for subnet, rangelist in self.dhcp_server_cfg: - try: - adr, mask = subnet.split('/') - except Exception as e: - print(subnet, repr(e)) - continue - content.append('subnet %s netmask %s {\n' % (adr, mask)) - # content.append(' option netmask %s;\n' % mask) - for rng in rangelist: - content.append(' range %s %s;\n' % rng) - content.append('}\n') - content = ''.join(content) - todo = write_when_new('/etc/dhcp/dhcpd.conf', content) - if todo: - print('change dhcpd.conf') - to_start['dhcpd'] = 'restart' - self.to_system.dirty = True - elif doit: - unix_cmd('systemctl stop dhcpd') - unix_cmd('systemctl disable dhcpd') - content = [] - dirty = False - for section in parser.sections(): - if COMMENT not in parser[section]: - dirty = True - content.append('[%s]' % section) - content.append(COMMENT) - section_dict = dict(parser[section].items()) - if section == 'DISPLAY': - display_update(section_dict) - for key, value in section_dict.items(): - content.append('%s=%s' % (key, value)) - content.append('') - if dirty: - 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)) - raise - - reload_systemd = False - for service, service_func in SERVICES.items(): - section = service.upper() - if parser.has_section(section): - servicecfg = service_func(**dict(parser[section])) - else: - servicecfg = None - result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) - active = False - enabled = False - for line in result.split('\n'): - if line.startswith('WantedBy=m'): - enabled = True - elif line.strip() == 'ActiveState=active': - active = True - if servicecfg is None: - if active: - unix_cmd('systemctl stop %s' % service) - self.to_system.dirty = True - if enabled: - unix_cmd('systemctl disable %s' % service) - self.to_system.dirty = True - else: - if not enabled: - to_start[service] = 'enable' - elif not active: - to_start[service] = 'restart' - if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): - self.to_system.dirty = True - reload_systemd = True - if servicecfg and to_start.get('service') is None: - to_start[service] = 'restart' - - pip() - if reload_systemd: - unix_cmd('systemctl daemon-reload') - for service, action in to_start.items(): - self.to_system.dirty = True - if action == 'if_restart': - unix_cmd('ifdown %s' % service) - unix_cmd('ifup %s' % service) - else: - if action == 'restart': - unix_cmd('systemctl restart %s' % service) - unix_cmd('systemctl enable %s' % service) - 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('') - self.current = '\n'.join(result) - - def unix_cmd(self, command, always=False, stdout=PIPE): - if self.doit or always: - if not always: - print('$ %s' % command) - result = Popen(command.split(), stdout=stdout).communicate()[0] - return (result or b'').decode() - else: - print('> %s' % command) - - def write_when_new(self, filename, content, ignore_reduction=False): - if content is None: - lines = [] - else: - if not content.endswith('\n'): - content += '\n' - lines = content.split('\n') - try: - with open(filename) as f: - old = f.read() - except FileNotFoundError: - old = None - if old == content: +def handle_config(): + cfgfile = None + cfgfiles = [] + 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 - if ignore_reduction: - content_set = set(v for v in lines if not v.startswith('#')) - 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))) - else: - return False - if self.doit: - if lines: - with open(filename, 'w') as f: - f.write(content) - else: - os.remove(filename) - elif filename.endswith(more_info): - print('changes in', filename) - old = [] if old is None else old.split('\n') - top_lines = 0 # in case of empty loop - for top_lines, (ol, ll) in enumerate(zip(old, lines)): - if ol != ll: - break - bottom_lines = 0 - for bottom_lines, (ol, ll) in enumerate(zip(reversed(old[top_lines:]), reversed(lines[top_lines:]))): - if ol != ll: - break - if bottom_lines == 0: - old.append('') - lines.append('') - bottom_lines += 1 - print("===") - print('\n'.join(old[:top_lines])) - print('<<<') - print('\n'.join(old[top_lines:-bottom_lines])) - print('---') - print('\n'.join(lines[top_lines:-bottom_lines])) - print('>>>') - print('\n'.join(old[-bottom_lines:-1])) - print("===") - return content - - @staticmethod - def frappy(cfg=None, port=None, requirements='', **kwds): - if not cfg: - return None - req = pip_requirements['l_samenv'] - req[cfg] = '\n'.join(requirements.split(',')) - if port: - cfg = '-p %s %s' % (port, cfg) - with open('/home/l_samenv/frappy/requirements.txt') as f: - req['frappy main'] = f.read() - return FRAPPY_TEMPLATE % cfg - - def router(self, **opts): - if not opts: - return None - try: - with open('/root/aputools/requirements.txt') as f: - pip_requirements['root']['aputools'] = f.read() - except FileNotFoundError: - pass - return self.ROUTER_TEMPLATE - - def display_update(self, cfg): - dirty = False - lines = [] - for key in ['line0', 'line1', 'line2']: - value = cfg.get(key, '') - if value.startswith('HOST'): - line = self.hostname - value = 'HOST: ' + line - elif value.startswith('ADDR'): - line = self.main_addr - value = 'ADDR: ' + line - else: - line = value - lines.append(line) - if cfg.get(key) != value: - if self.doit: - cfg[key] = value - dirty = True - if dirty: - text = '\n'.join(lines) - if self.doit: - with open(STARTUP_TEXT, 'w') as f: - f.write(text) - else: - print('new startup text:') - print('\n'.join(lines)) - - def display(self, **opts): - if not opts: - return None - return self.DISPLAY_TEMPLATE - - def pip(self): - for user, requirements in pip_requirements.items(): - if user == 'root': - tmpname = join('/root', 'pip_requirements.tmp') - pipcmd = 'pip3 install -r %s' % tmpname - else: - tmpname = join('/home', user, 'pip_requirements.tmp') - pipcmd = 'sudo --user %s pip3 install --user -r %s' % (user, tmpname) - filename = tmpname.replace('.tmp', '.txt') - content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items()) - if write_when_new(filename, content, True): - if self.doit: - os.rename(filename, tmpname) - if os.system(pipcmd) == 0: - os.rename(tmpname, filename) - else: - os.remove(tmpname) - else: - print(pipcmd) - # unix_cmd(pipcmd, stdout=None) - self.to_system.dirty = True - - def create_if(self, 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' - elif cfg.startswith('wan') or cfg == 'dhcp': - if name != self.main_if and self.main_if - raise ValueError('can not have more than one WAN/DHCP port') - self.main_if = name - self.main_addr = net_addr[name] - result['BOOTPROTO'] = 'dhcp' - # default: all <= 192.0.0.0 - # others have to be added explicitly - self.dhcp_server_cfg.append(('0.0.0.0/128.0.0.0', [])) - self.dhcp_server_cfg.append(('128.0.0.0/192.0.0.0', [])) - for nw in cfg.split(',')[1:]: - nw = IPv4Interface(nw).network - self.dhcp_server_cfg.append((nw.with_netmask, [])) + newhostname = main['hostname'] + if not cfgfiles: + # 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: - cfgip = IPv4Interface(cfg) - network = cfgip.network - if network.prefixlen == 32: # or no prefix specified - otherip = IPv4Interface('%s/24' % cfgip.ip) - network = otherip.network - if str(cfgip.ip).endswith('.1'): - cfgip = network.network_address + 2 - else: - cfgip = network.network_address + 1 - self.dhcp_server_cfg.append((network.with_netmask, [(str(otherip.ip), str(otherip.ip))])) - result['IPADDR'] = 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 + # 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 % (newhostname, apuid) + with open(cfgfile, 'w') as f: + 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: + newhostname = basename(cfgfile).rpartition('_')[0] + if doit: + os.system('sh sethostname.sh') + else: + if cfgfile: + 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() + try: + parser.read(cfgfile) + network = dict(parser['NETWORK']) + 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': + print('change', ifname) + show.dirty = True + to_start[ifname] = 'if_restart' + if dhcp_server_cfg: + content = [DHCP_HEADER] + for subnet, rangelist in dhcp_server_cfg: + try: + adr, mask = subnet.split('/') + except Exception as e: + print(subnet, repr(e)) + continue + content.append('subnet %s netmask %s {\n' % (adr, mask)) + #content.append(' option netmask %s;\n' % mask) + for rng in rangelist: + content.append(' range %s %s;\n' % rng) + content.append('}\n') + content = ''.join(content) + todo = write_when_new('/etc/dhcp/dhcpd.conf', content) + if todo: + print('change dhcpd.conf') + to_start['dhcpd'] = 'restart' + show.dirty = True + elif doit: + unix_cmd('systemctl stop dhcpd') + unix_cmd('systemctl disable dhcpd') + content = [] + dirty = False + for section in parser.sections(): + if COMMENT not in parser[section]: + dirty = True + content.append('[%s]' % section) + content.append(COMMENT) + section_dict = dict(parser[section].items()) + if section == 'DISPLAY': + display_update(section_dict) + for key, value in section_dict.items(): + content.append('%s=%s' % (key, value)) + content.append('') + if dirty: + 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)) + raise + return False - def walk(self, 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 f: - to_delete = [] - for line in f: - line = line.strip() - if fline and exists(join(syspath, line)): - if exists(join(dirpath, line)): - print('ERROR: %s in %s, but also in repo -> ignored' % (line, DEL)) - else: - to_delete.append(f) - action.delete(to_delete) - if missing: - action.missing(missing) + reload_systemd = False + for service, service_func in SERVICES.items(): + section = service.upper() + if parser.has_section(section): + servicecfg = service_func(**dict(parser[section])) + else: + servicecfg = None + result = unix_cmd('systemctl show -p WantedBy -p ActiveState %s' % service, True) + active = False + enabled = False + for line in result.split('\n'): + if line.startswith('WantedBy=m'): + enabled = True + elif line.strip() == 'ActiveState=active': + active = True + if servicecfg is None: + if active: + unix_cmd('systemctl stop %s' % service) + show.dirty = True + if enabled: + unix_cmd('systemctl disable %s' % service) + show.dirty = True + else: + if not enabled: + to_start[service] = 'enable' + elif not active: + to_start[service] = 'restart' + if write_when_new('/etc/systemd/system/%s.service' % service, servicecfg): + show.dirty = True + reload_systemd = True + if servicecfg and to_start.get('service') is None: + to_start[service] = 'restart' + + pip() + if reload_systemd: + unix_cmd('systemctl daemon-reload') + for service, action in to_start.items(): + show.dirty = True + if action == 'if_restart': + unix_cmd('ifdown %s' % service) + unix_cmd('ifup %s' % service) + else: + if action == 'restart': + unix_cmd('systemctl restart %s' % service) + unix_cmd('systemctl enable %s' % service) + 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) -print('---') -to_install = Install(False) - -if not result: - print('fix first above errors') -elif to_system.dirty and more_info == 'NONE': +more_info = 'NONE' +while True: + doit = False print('---') - answer = input('do above? ') - if answer.lower().startswith('y'): - Install(True) - walk(Do()) - with open('/root/aputools/current', 'w') as fil: - fil.write(to_install.current) -else: - print('nothing to do') + show = Show() + walk(show) + result = handle_config() + + if not result: + print('fix first above errors') + break + if show.dirty and more_info == 'NONE': + print('---') + answer = input('enter y(es) to do above or a filename to check changes? ') + doit = True + if answer.lower() in {'y', 'yes'}: + handle_config() + walk(Do()) + with open('/root/aputools/current', 'w') as f: + f.write(result) + break + if not answer: + print('cancel') + break + else: + print('nothing to do') + break From ae6ba7a0310bfa430c15de333f3bf8686b02efb0 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Mon, 15 Jan 2024 11:34:03 +0100 Subject: [PATCH 10/23] improve display - display config has only one line for startup text - faster display daeomon startup - added newer fonts (e.g. medium) to display.py --- display.py | 7 ++- install.py | 103 +++++++++++++++++--------------- servercfg/linse-box1_5b265c.cfg | 12 ++-- 3 files changed, 65 insertions(+), 57 deletions(-) diff --git a/display.py b/display.py index c141dd8..1aa70d2 100644 --- a/display.py +++ b/display.py @@ -37,12 +37,14 @@ SET_STARTUP = 0xf2 IDENT = 0xf3 -FONT_GEO = [(6, 8), (8, 16), (20, 40)] +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)) @@ -83,7 +85,7 @@ class Display: if daemon: threading.Thread(target=self.gethostthread, daemon=True).start() self.event.wait(1) # wait for thread to be started - self.refresh() + self.net_display(None) def reset(self): self.send(MODE_GRAPH) @@ -200,7 +202,6 @@ class Display: return if data[0] == TOUCH and len(data) == 3: x = (data[1] % 2) * 256 + data[2] - print(x) return x print('skipped', data) diff --git a/install.py b/install.py index ebbef16..f5e370b 100755 --- a/install.py +++ b/install.py @@ -14,13 +14,14 @@ 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__' @@ -96,6 +97,13 @@ ExecStart = /usr/bin/python3 /root/aputools/display.py -d WantedBy = multi-user.target """ +ifname_mapping = { + 'eth0': 'enp1s0', + 'eth1': 'enp2s0', + 'eth2': 'enp3s0', + 'eth3': 'enp4s0', +} + pip_requirements = { 'l_samenv': {}, 'root': {} @@ -108,7 +116,7 @@ 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': gethostname() # effective or given host name + 'hostname': socket.gethostname() # effective or given host name } for netif in os.scandir('/sys/class/net'): @@ -145,19 +153,15 @@ def router(**opts): def display_update(cfg): - text = '\n'.join(cfg.get(key, '') for key in ('line0', 'line1', 'line2')) + text = '\n'.join(cfg.get('startup_text', '').split('|')[:3]) text = text.replace('HOST', main_info['hostname']) \ .replace('ADDR', main_info['addr']) + '\n' - try: - with open(STARTUP_TEXT) as f: - if f.read() == text: - return - except FileNotFoundError: - pass - print('new startup text:') - print(text) - with open(STARTUP_TEXT + '.todo', 'w') as f: - f.write(text) + 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): @@ -230,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 @@ -254,6 +259,7 @@ def write_when_new(filename, content, ignore_reduction=False): print('>>>') print('\n'.join(old[-bottom_lines:-1])) print("===") + print('.' * 80) return content @@ -344,20 +350,21 @@ class Show(Walker): 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): 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) @@ -392,6 +399,7 @@ class Do(Walker): def handle_config(): cfgfile = None cfgfiles = [] + dhcp_server_cfg.clear() for file in glob(CFGPATH % ('*', apuid)): cfgfiles.append(file) for i in [1, 2, 3]: @@ -400,7 +408,7 @@ def handle_config(): print('cfg files found with bad apu id (use net addr of leftmost plug)') print(bad) return False - newhostname = main['hostname'] + newhostname = main_info['hostname'] if not cfgfiles: # determine if display is present disp = serial.Serial('/dev/ttyS1', baudrate=115200, timeout=1) @@ -428,7 +436,7 @@ def handle_config(): 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: @@ -445,12 +453,12 @@ def handle_config(): parser = ConfigParser() try: parser.read(cfgfile) - network = dict(parser['NETWORK']) + 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' @@ -472,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') @@ -485,7 +493,8 @@ def handle_config(): content.append(COMMENT) section_dict = dict(parser[section].items()) if section == 'DISPLAY': - display_update(section_dict) + if display_update(section_dict): + to_start['display'] = 'restart' for key, value in section_dict.items(): content.append('%s=%s' % (key, value)) content.append('') @@ -551,30 +560,30 @@ def handle_config(): return '\n'.join(result) -more_info = 'NONE' -while True: - doit = False - print('---') - show = Show() - walk(show) - result = handle_config() +more_info = False +doit = False +print(' ') +show = Show() +walk(show) +result = handle_config() - if not result: - print('fix first above errors') - break - if show.dirty and more_info == 'NONE': +if not result: + print('fix first above errors') +else: + if show.dirty: print('---') - answer = input('enter y(es) to do above or a filename to check changes? ') - doit = True - if answer.lower() in {'y', 'yes'}: + 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) - break - if not answer: - print('cancel') - break + elif 'more'.startswith(answer.lower()): + more_info = True + walk(show) + result = handle_config() else: print('nothing to do') - break diff --git a/servercfg/linse-box1_5b265c.cfg b/servercfg/linse-box1_5b265c.cfg index 3a8dcbf..f808e75 100644 --- a/servercfg/linse-box1_5b265c.cfg +++ b/servercfg/linse-box1_5b265c.cfg @@ -1,12 +1,10 @@ [NETWORK] ; please refer to README.md for help -enp1s0=dhcp -enp2s0=192.168.1.1 -enp3s0=192.168.2.2 -enp4s0=192.168.3.3 +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 -line0=startup... -line1=HOST: linse-box1 -line2=ADDR: 00:0d:b9:5b:26:5c +startup_text=startup...|HOST|ADDR From ee6e91aadcfd57b602eb9a1c999291ace0c6b6fb Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 15 Jan 2024 11:43:21 +0100 Subject: [PATCH 11/23] update REAME.txt (for new boxes MLZ type) --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8ba201..209e061 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ to_system/ contains the files to be installed in the system 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: @@ -30,11 +30,19 @@ 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 form munich 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 ``` @@ -72,5 +80,9 @@ 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. + From ca6c4f25cb161caae3ebb309f6fe63a5523caa54 Mon Sep 17 00:00:00 2001 From: zolliker Date: Mon, 15 Jan 2024 12:47:05 +0100 Subject: [PATCH 12/23] fix typo in README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 209e061..7c438a2 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,8 @@ startup_text=startup...|HOST|ADDR # startup text, 3 lines separated with | ## network configuration The example above fits the most cases. -Remark: as the control boxes form munich are labelled with 'eth0' etc., -the names might be given in this form. internally the more modern names like 'enp1s0' -are used. +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: From 80edb3a58138498ae08d040b49e1016d35fb1991 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 16 Jan 2024 12:57:06 +0100 Subject: [PATCH 13/23] add docu for installing APU to README.md --- README.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7c438a2..a9216d5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 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 @@ -47,7 +47,7 @@ Here a detailed description of possible settings: ``` 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). @@ -63,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 @@ -84,4 +84,114 @@ enp3s0=off # disabled 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 + +### Tools / Resources needed + +#### progress bar + +On mac use ``brew install pv`` to install. If you do not want it, you may leve it out, +it is a matter of convenience. In this case omit ``| pv `` in the commands + + + +## install image by means of an USB stick + +The stick has TinyLinux and on it and some additional scripts + +If you do not have one, you may create it logging to an other box as root + +### a) create a USB stick with TinyLinux (omit if you have the stick already) + +to verify that /dev/sdb is your stick, use the following command before and after plugging the stick +``` +apu> ls /dev/sdb +``` + +copy the stick image to the stick (32 GB should be enough) + +``` +mac> dd if=apuinstall.lz4 bs=512k | pv | ssh root@ "lz4 -d | dd of=/dev/sdb bs=512k" +``` + +### b) mount with TinyLinux from USB stick + +Connect a Mac with an USB to serial adapter (and null-modem!). +If no such adapter is available, you might try to connect LAN and search for computer name box, +do the registration in PacketFence and reboot, login with root/root). +``` +mac> screen /dev/tty.usbserial-130 115200 +``` + +Do not yet connect LAN, plug USB stick and start the box, wait for prompt 'tc@box:' +``` +apu> cd /media/CENTOS7_APU (or /media/CENTOS_APU) +apu> sh reg +``` + +enter new hostname (HWaddr will be on the output) +register to PacketFence (if not yet done) and set role to sinq-samenv +connect LAN to rightmost socket on a bare APU or the leftmost socket an a control box +``` +apu> sh tiny +... +some random art images are shown ... +... +inet addr:129.168.1.43 Bcast:192.168.1.255 Mask:255.255.255.0 +inet addr:127.0.0.1 Mask:255.0.0.0 +``` + +Use the displayed ip address for the following step. + + +### c) Copy Image to APU SSD +``` +mac> cd ~/switchdrive/apu/images +mac> dd if=apumaster.lz4 bs=512k | pv | ssh root@192.168.1.43 "lz4 -d | dd of=/dev/sda bs=512k" +``` + +(you are asked for root password, which is root, ``| pv `` may be omitted) + +remove the stick, power off/on + +login with root/FrappyLinse +``` +> cd aputools +> git pull +> python3 install.py +... +enter host name: +... + +> reboot now +``` + +DONE! + + +### d) Cloning an Image from an Existing Box + +Use (b) above to boot from TinyLinux (replace the IP address by the one from the box!) +First make sure that a copy of the last apumaster.lz4 is saved with its date. +``` +mac> ssh root@192.168.1.43 "dd if=/dev/sda bs=512k | lz4" | pv | dd of=apumaster.lz4 bs=512k +``` + + +### e) Appendix + +Source of scripts on TinyLinux: +``` +apu> cat tini.sh +tce-load -wi nano +tce-load -wi lz4 +tce-load -wi openssh +sudo /usr/local/etc/init.d/openssh start +sudo chpasswd << EOF +tc:tc +root:root +EOF +ifconfig | grep 'inet addr' +``` + From c354a0076b865f1f11225b62f0cd1f3a4b0eeab6 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Tue, 16 Jan 2024 17:52:04 +0100 Subject: [PATCH 14/23] add scripts to create an bootable USB stick with TinyLinux --- mktiny.sh | 34 ++++++++++++++++++++++++++++++++++ mkusb.sh | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 mktiny.sh create mode 100644 mkusb.sh diff --git a/mktiny.sh b/mktiny.sh new file mode 100644 index 0000000..2459f33 --- /dev/null +++ b/mktiny.sh @@ -0,0 +1,34 @@ +# create a bootable USB stick for APU +# this part of the script needs root priv +if [ $# -ne 2 ] || [ $EUID != 0 ] +then + echo "Usage: sudo bash mktiny.sh " + exit +fi + +if [[ $(cat /sys/block/$1/removable) != "1" ]]; then + echo "/dev/$1 is not a removable disk" + exit +fi + +DEVICE=/dev/$1 +NAME=$2 + +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=mbr.bin of=${DEVICE} +parted ${DEVICE} set 1 boot on + +mkdir -p /mnt/apusb +mount ${DEVICE}1 /mnt/apusb +echo "copy files ..." +cp files/* /mnt/apusb/ + +umount /mnt/apusb + +rm -rf usbboot +echo "done." diff --git a/mkusb.sh b/mkusb.sh new file mode 100644 index 0000000..668dc13 --- /dev/null +++ b/mkusb.sh @@ -0,0 +1,50 @@ +# name of the USB stick to be created +NAME=BOOT_TINY + +echo "enter source for tinylinux files, e.g. zolliker@mpc1704:switchdrive/apu/usbboot" +read SRC + +scp -r $SRC ./ + +cd usbboot + +# create (overwrite) syslinux.cfg +cat > files/syslinux.cfg << "EOF" +SERIAL 0 115200 +CONSOLE 0 +default centos +ALLOWOPTIONS 0 +TIMEOUT 600 +label centos +kernel vmlinuz +append initrd=core.gz inst.stage2=hd:LABEL=USBNAME quiet console=ttyS0,115200n8 +EOF +sed -i -- "s/USBNAME/$NAME/g" files/syslinux.cfg + +ls files + +# 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 (${DISKS[@]})?" +read DEVICE + +if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then + echo "create TinyLinux" + sudo bash ../mktiny.sh $DEVICE $NAME +else + echo "/dev/$DEVICE is not a removeable disk" +fi + From 370896170ada506a3ee72c632500a3055e095d5e Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 16 Jan 2024 18:17:27 +0100 Subject: [PATCH 15/23] simplify mkusb.sh --- README.md | 17 +++++++++-------- mktiny.sh | 34 ---------------------------------- mkusb.sh | 36 +++++++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 49 deletions(-) delete mode 100644 mktiny.sh diff --git a/README.md b/README.md index a9216d5..835e245 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ it is a matter of convenience. In this case omit ``| pv `` in the commands -## install image by means of an USB stick +## install image by means of the USB stick BOOT_TINY -The stick has TinyLinux and on it and some additional scripts +The stick has TinyLinux on it and some additional scripts If you do not have one, you may create it logging to an other box as root @@ -105,13 +105,11 @@ If you do not have one, you may create it logging to an other box as root to verify that /dev/sdb is your stick, use the following command before and after plugging the stick ``` -apu> ls /dev/sdb -``` +apu> cd aputools +apu> bash mkusb.sh -copy the stick image to the stick (32 GB should be enough) +... you are asked to give the device name from a list (typically sdb) ... -``` -mac> dd if=apuinstall.lz4 bs=512k | pv | ssh root@ "lz4 -d | dd of=/dev/sdb bs=512k" ``` ### b) mount with TinyLinux from USB stick @@ -125,7 +123,7 @@ mac> screen /dev/tty.usbserial-130 115200 Do not yet connect LAN, plug USB stick and start the box, wait for prompt 'tc@box:' ``` -apu> cd /media/CENTOS7_APU (or /media/CENTOS_APU) +apu> cd /media/BOOT_TINY (or /media/CENTOS_APU) apu> sh reg ``` @@ -145,6 +143,9 @@ Use the displayed ip address for the following step. ### c) Copy Image to APU SSD + +Note that the images are not part of the git repo. + ``` mac> cd ~/switchdrive/apu/images mac> dd if=apumaster.lz4 bs=512k | pv | ssh root@192.168.1.43 "lz4 -d | dd of=/dev/sda bs=512k" diff --git a/mktiny.sh b/mktiny.sh deleted file mode 100644 index 2459f33..0000000 --- a/mktiny.sh +++ /dev/null @@ -1,34 +0,0 @@ -# create a bootable USB stick for APU -# this part of the script needs root priv -if [ $# -ne 2 ] || [ $EUID != 0 ] -then - echo "Usage: sudo bash mktiny.sh " - exit -fi - -if [[ $(cat /sys/block/$1/removable) != "1" ]]; then - echo "/dev/$1 is not a removable disk" - exit -fi - -DEVICE=/dev/$1 -NAME=$2 - -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=mbr.bin of=${DEVICE} -parted ${DEVICE} set 1 boot on - -mkdir -p /mnt/apusb -mount ${DEVICE}1 /mnt/apusb -echo "copy files ..." -cp files/* /mnt/apusb/ - -umount /mnt/apusb - -rm -rf usbboot -echo "done." diff --git a/mkusb.sh b/mkusb.sh index 668dc13..7200e08 100644 --- a/mkusb.sh +++ b/mkusb.sh @@ -1,15 +1,13 @@ # name of the USB stick to be created NAME=BOOT_TINY -echo "enter source for tinylinux files, e.g. zolliker@mpc1704:switchdrive/apu/usbboot" +echo "enter source for tinylinux files, e.g. zolliker@mpc1704:switchdrive/apu/boot_tiny" read SRC scp -r $SRC ./ -cd usbboot - # create (overwrite) syslinux.cfg -cat > files/syslinux.cfg << "EOF" +cat > boot_tiny/syslinux.cfg << "EOF" SERIAL 0 115200 CONSOLE 0 default centos @@ -19,9 +17,9 @@ label centos kernel vmlinuz append initrd=core.gz inst.stage2=hd:LABEL=USBNAME quiet console=ttyS0,115200n8 EOF -sed -i -- "s/USBNAME/$NAME/g" files/syslinux.cfg +sed -i -- "s/USBNAME/$NAME/g" boot_tiny/syslinux.cfg -ls files +ls boot_tiny # list removeable disks DISKS=() @@ -43,7 +41,31 @@ read DEVICE if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then echo "create TinyLinux" - sudo bash ../mktiny.sh $DEVICE $NAME + + 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=mbr.bin of=${DEVICE} + parted ${DEVICE} set 1 boot on + + mkdir -p /mnt/apusb + mount ${DEVICE}1 /mnt/apusb + echo "copy files ..." + cp boot_tiny/* /mnt/apusb/ + umount /mnt/apusb + + rm -rf boot_tiny + echo "done." else echo "/dev/$DEVICE is not a removeable disk" fi From 23d9299b566180f442203ee26f067f0c1baad74b Mon Sep 17 00:00:00 2001 From: LIN SE Date: Tue, 16 Jan 2024 18:21:16 +0100 Subject: [PATCH 16/23] added mbr.bin --- mbr.bin | Bin 0 -> 440 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mbr.bin diff --git a/mbr.bin b/mbr.bin new file mode 100644 index 0000000000000000000000000000000000000000..646a6848dc54986513bb0731e84d30b3db684ab5 GIT binary patch literal 440 zcmXp$@T>1e--SI4HJ#7c!ut;V{e-s4X04g_|GR2l8(_=J5StP(YgDfwY+~(imQtGWuo~ z7iZ?BE94iX7A2N|SjCmaC8@c3yj*DsX@-|}Gt{I7r3J7BXEA08bUsV_ZuX`6z!SEP zhaE>JeKY$}-EoZLq2mrlrY^Bx)n|o!Qv}iy55G9_f8V~^{rlK=DlmLrdN?hi^XYCN zhlRoLKyctU_J$H>ht7M=hXo#e_9%(2w2X}Ia%ZvOFJV)uw_z<+O6zv#Nb3$0Nn^Z_ zrg*88Ij!^1i#7j?m|txBpVsXr^J2k&kk&IGd&(GmOLIyx3vyBw5|c|Z%Tg5z5{pVQ zOEUBGia{RecHu~4c9Ti#bm4em@V`hstvinyXx{AN` z)4x5xs-xoBfBk>*LXN>7EjSc%@)J`OQj3c6i+~2lt=YoYWtYUVnf Date: Wed, 17 Jan 2024 08:46:48 +0100 Subject: [PATCH 17/23] changed ssh path for boot_tiny directory - include mbr.bin there --- mbr.bin | Bin 440 -> 0 bytes mkusb.sh | 23 ++++------------------- 2 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 mbr.bin diff --git a/mbr.bin b/mbr.bin deleted file mode 100644 index 646a6848dc54986513bb0731e84d30b3db684ab5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440 zcmXp$@T>1e--SI4HJ#7c!ut;V{e-s4X04g_|GR2l8(_=J5StP(YgDfwY+~(imQtGWuo~ z7iZ?BE94iX7A2N|SjCmaC8@c3yj*DsX@-|}Gt{I7r3J7BXEA08bUsV_ZuX`6z!SEP zhaE>JeKY$}-EoZLq2mrlrY^Bx)n|o!Qv}iy55G9_f8V~^{rlK=DlmLrdN?hi^XYCN zhlRoLKyctU_J$H>ht7M=hXo#e_9%(2w2X}Ia%ZvOFJV)uw_z<+O6zv#Nb3$0Nn^Z_ zrg*88Ij!^1i#7j?m|txBpVsXr^J2k&kk&IGd&(GmOLIyx3vyBw5|c|Z%Tg5z5{pVQ zOEUBGia{RecHu~4c9Ti#bm4em@V`hstvinyXx{AN` z)4x5xs-xoBfBk>*LXN>7EjSc%@)J`OQj3c6i+~2lt=YoYWtYUVnf boot_tiny/syslinux.cfg << "EOF" -SERIAL 0 115200 -CONSOLE 0 -default centos -ALLOWOPTIONS 0 -TIMEOUT 600 -label centos -kernel vmlinuz -append initrd=core.gz inst.stage2=hd:LABEL=USBNAME quiet console=ttyS0,115200n8 -EOF -sed -i -- "s/USBNAME/$NAME/g" boot_tiny/syslinux.cfg - -ls boot_tiny +scp -r $SRC/boot_tiny ./ # list removeable disks DISKS=() @@ -36,7 +21,7 @@ do fi done < <(lsblk -l --output RM,TYPE,NAME,SIZE,LABEL,VENDOR,MODEL) -echo "which device (${DISKS[@]})?" +echo "which device? should be one of: ${DISKS[@]}" read DEVICE if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then @@ -55,7 +40,7 @@ if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then mkfs.vfat -n $NAME -I ${DEVICE}1 syslinux -i ${DEVICE}1 - dd conv=notrunc bs=440 count=1 if=mbr.bin of=${DEVICE} + dd conv=notrunc bs=440 count=1 if=boot_tiny/mbr.bin of=${DEVICE} parted ${DEVICE} set 1 boot on mkdir -p /mnt/apusb From 77a06a0107b5f75682ffd1c75ea1a2258afbe88c Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 22 Jan 2024 15:05:01 +0100 Subject: [PATCH 18/23] remove remark about installing without console as this does not work --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 835e245..6c60181 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,6 @@ apu> bash mkusb.sh ### b) mount with TinyLinux from USB stick Connect a Mac with an USB to serial adapter (and null-modem!). -If no such adapter is available, you might try to connect LAN and search for computer name box, -do the registration in PacketFence and reboot, login with root/root). ``` mac> screen /dev/tty.usbserial-130 115200 ``` From 926680e52b24b9d02fd9dd0ab05752a906a3eadd Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 24 Jan 2024 10:21:46 +0100 Subject: [PATCH 19/23] mkusb.sh: copy subdirectories of boot_tiny --- mkusb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkusb.sh b/mkusb.sh index c7b63ea..338000b 100644 --- a/mkusb.sh +++ b/mkusb.sh @@ -46,7 +46,7 @@ if [[ " ${DISKS[@]} " =~ " $DEVICE " ]]; then mkdir -p /mnt/apusb mount ${DEVICE}1 /mnt/apusb echo "copy files ..." - cp boot_tiny/* /mnt/apusb/ + cp -r boot_tiny/* /mnt/apusb/ umount /mnt/apusb rm -rf boot_tiny From ee560155803e388b4d38145e814226d10e88c864 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 24 Jan 2024 11:26:27 +0100 Subject: [PATCH 20/23] update README.md --- README.md | 103 +++++++++++++++++++++--------------------------------- 1 file changed, 40 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 6c60181..0cf1200 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ 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 @@ -86,72 +86,67 @@ available on control boxes (MLZ type) only. must therefore not be present on bar ## Installation of a Fresh Control Box or bare APU -### Tools / Resources needed - -#### progress bar - -On mac use ``brew install pv`` to install. If you do not want it, you may leve it out, -it is a matter of convenience. In this case omit ``| pv `` in the commands - - - ## 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 logging to an other box as root +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) -to verify that /dev/sdb is your stick, use the following command before and after plugging the stick +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) ... - ``` +You are asked to give the device name from a list (typically sdb) +before it writes to the stick. -### b) mount with TinyLinux from USB 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 connect LAN, plug USB stick and start the box, wait for prompt 'tc@box:' +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 ``` -apu> cd /media/BOOT_TINY (or /media/CENTOS_APU) -apu> sh reg +cd /media/BOOT_TINY ``` -enter new hostname (HWaddr will be on the output) -register to PacketFence (if not yet done) and set role to sinq-samenv -connect LAN to rightmost socket on a bare APU or the leftmost socket an a control box +### 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 ``` -apu> sh tiny -... some random art images are shown ... -... -inet addr:129.168.1.43 Bcast:192.168.1.255 Mask:255.255.255.0 -inet addr:127.0.0.1 Mask:255.0.0.0 +``` +images from l_samenv@samenv:boxes/images: + +apumaster_2022-11-09.lz4 +apumaster_2024-01-18.lz4 + +which image? ``` -Use the displayed ip address for the following step. +* 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`) -### c) Copy Image to APU SSD - -Note that the images are not part of the git repo. - -``` -mac> cd ~/switchdrive/apu/images -mac> dd if=apumaster.lz4 bs=512k | pv | ssh root@192.168.1.43 "lz4 -d | dd of=/dev/sda bs=512k" -``` - -(you are asked for root password, which is root, ``| pv `` may be omitted) - -remove the stick, power off/on +### e) Install Services login with root/FrappyLinse ``` @@ -168,29 +163,11 @@ enter host name: DONE! -### d) Cloning an Image from an Existing Box +### f) Cloning an Image from an Existing Box -Use (b) above to boot from TinyLinux (replace the IP address by the one from the box!) -First make sure that a copy of the last apumaster.lz4 is saved with its date. +Use (b) above to boot from the BOOT_TINY USB stick ``` -mac> ssh root@192.168.1.43 "dd if=/dev/sda bs=512k | lz4" | pv | dd of=apumaster.lz4 bs=512k +$ sh clone ``` - - -### e) Appendix - -Source of scripts on TinyLinux: -``` -apu> cat tini.sh -tce-load -wi nano -tce-load -wi lz4 -tce-load -wi openssh -sudo /usr/local/etc/init.d/openssh start -sudo chpasswd << EOF -tc:tc -root:root -EOF -ifconfig | grep 'inet addr' -``` - +You are asked From 9d0edff2ce0f31b44516884c550b1279ccd4a8d3 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 24 Jan 2024 15:56:19 +0100 Subject: [PATCH 21/23] take boot_tiny fiels from gitlab + wip display firmware --- display.py | 29 +++++++++++++++++++++++++++-- mkusb.sh | 8 +++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/display.py b/display.py index 1aa70d2..2370e41 100644 --- a/display.py +++ b/display.py @@ -80,6 +80,8 @@ class Display: os.rename(todo_file, STARTUP_TEXT) else: self.storage = None + else: + os.system('systemctl stop display') self.gethost(False) self.reset() if daemon: @@ -265,15 +267,38 @@ class Display: 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 - else: + 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) + +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)) + else: + print('firmware file:', pretty_version(tag[4:])) + print('display version:', pretty_version(hwversion)) + #d.bootmode() + #os.system(f'../stm32flash-0.7/stm32flash -R -v -b 115200 -w /tmp/fw.bin {dev}') if daemon: while True: d.refresh() diff --git a/mkusb.sh b/mkusb.sh index 338000b..b6b190d 100644 --- a/mkusb.sh +++ b/mkusb.sh @@ -1,11 +1,9 @@ # name of the USB stick to be created NAME=BOOT_TINY -echo "enter ssh path of parent directory for boot_tiny, e.g. l_samenv@samenv:boxes" -read SRC - -scp -r $SRC/boot_tiny ./ - +if [ ! -d boot_tiny ] ; then + git clone --depth 1 git@gitlab.psi.ch:samenv/boot_tiny.git +fi # list removeable disks DISKS=() while read LINE From 847685bcdc6378ce00c087c9f00893dae617170a Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 24 Jan 2024 16:35:01 +0100 Subject: [PATCH 22/23] display.py: flashing firmware --- display.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/display.py b/display.py index 2370e41..6f4fc0f 100644 --- a/display.py +++ b/display.py @@ -293,12 +293,16 @@ if firmware: raise ValueError(f'{firmware} is not a valid firmware file') hwversion = d.version() if tag[4:] == hwversion: - print('firmware is already', pretty_version(hwversion)) - else: - print('firmware file:', pretty_version(tag[4:])) + # print('firmware is already', pretty_version(hwversion)) print('display version:', pretty_version(hwversion)) - #d.bootmode() - #os.system(f'../stm32flash-0.7/stm32flash -R -v -b 115200 -w /tmp/fw.bin {dev}') + 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() From 4b052f9320884226cc2fc00488665f5641581965 Mon Sep 17 00:00:00 2001 From: LIN SE Date: Wed, 24 Jan 2024 16:52:17 +0100 Subject: [PATCH 23/23] improve mkusb.sh --- mkusb.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mkusb.sh b/mkusb.sh index b6b190d..3a63aa4 100644 --- a/mkusb.sh +++ b/mkusb.sh @@ -1,9 +1,6 @@ # name of the USB stick to be created NAME=BOOT_TINY -if [ ! -d boot_tiny ] ; then - git clone --depth 1 git@gitlab.psi.ch:samenv/boot_tiny.git -fi # list removeable disks DISKS=() while read LINE @@ -22,9 +19,16 @@ 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