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