make install work also for rpi boxes

- in addition to to_system, we have now to_<box> with every
  type of box
- install is not needed to run under sudo anymore
This commit is contained in:
2025-04-04 15:36:03 +02:00
parent 2945855b39
commit e344108e51
11 changed files with 343 additions and 90 deletions

View File

@ -1,13 +1,19 @@
# Installation tools for APUs and Raspberry Pi boxes # Installation tools for APUs and Raspberry Pi boxes
The APU is a Linux box to be used as a control box at LIN sample environment Supports teh following boxes used at LIN sample environment:
- bare-apu: Linux box
- controlbox: from FRM2
- ionopimax: PLC from Sfera Labs with compute module 3
- ionopi: PLC from Sfrea labs with Raspberry Pi 3
- dual-eth-rpi: dual ethernet Raspberry Pi with compute module 4
`cfg/` contains the configuration files for all boxes `cfg/` contains the configuration files for all boxes
`to_system/` contains the files to be installed in the system `to_system/` contains the files to be installed in the system
We have also the Raspberry based PLC Iono Pi Max `to_<box>/` system files specific for a box (box names as in above list)
and other Raspberry Pi based boxes.
## config files ## config files
@ -16,9 +22,12 @@ filename: cfg/(hostname)_(hexdigits).cfg
* (hostname) the hostname to be given on startup * (hostname) the hostname to be given on startup
* (hexdigits) the last six digits of the ethernet adress of the first interface on the APU (leftmost plug) * (hexdigits) the last six digits of the ethernet adress of the first interface on the APU (leftmost plug)
content of the config files: ### example content of a config files for a control box:
``` ```
[BOX]
type=controlbox
[NETWORK] [NETWORK]
eth0=192.168.127.254 # leftmost socket: connect here a moxa with factory settings eth0=192.168.127.254 # leftmost socket: connect here a moxa with factory settings
eth1=192.168.2.2 # connected device will get this ip via DHCP eth1=192.168.2.2 # connected device will get this ip via DHCP
@ -38,6 +47,19 @@ port=5000 # the port for the frappy server
startup_text=startup...|HOST|ADDR # startup text, 3 lines separated with | startup_text=startup...|HOST|ADDR # startup text, 3 lines separated with |
``` ```
the [BOX] section is optional for controlbox and bare-apu
### example content of a config files for a control box:
```
[BOX]
type=ionopi
[NETWORK]
eth0=wan
```
## network configuration ## network configuration
The example above fits the most cases. The example above fits the most cases.

View File

@ -12,37 +12,36 @@ if bytes == str:
import sys import sys
import os import os
import filecmp import filecmp
import shutil
import re import re
import types import types
import socket import socket
from pathlib import Path
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from ipaddress import IPv4Interface from ipaddress import IPv4Interface
from os.path import join, getmtime, exists, basename from os.path import getmtime
if os.geteuid() != 0:
exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.")
def exit(): def exit():
print('please restart sudo ./install.py again') print('please restart sudo ./install.py again')
sys.exit() sys.exit()
try: try:
import serial import serial
except ImportError: except ImportError:
if 'yes'.startswith(input('install pyserial? [y]')): if 'yes'.startswith(input('install pyserial? [y]')):
os.system('pip3 install --break-system-packages pyserial') os.system('sudo pip3 install --break-system-packages pyserial')
serial = None serial = None
try: try:
from utils import BoxInfo, check_service, unix_cmd, change_firewall from utils import BoxInfo, check_service, unix_cmd, change_firewall, UndefinedConfigFile
except ImportError: except ImportError:
if 'yes'.startswith(input('install netifaces? [y]')): if 'yes'.startswith(input('install netifaces? [y]')):
os.system('pip3 install --break-system-packages netifaces') os.system('sudo pip3 install --break-system-packages netifaces')
serial = None serial = None
if serial is None: if serial is None:
print('please restart sudo ./install.py again') print('please restart ./install.py again')
exit() exit()
@ -51,36 +50,62 @@ TOOLS = BoxInfo.TOOLS
more_info = False more_info = False
DEL = '__to_delete__' DEL = '__to_delete__'
STARTUP_TEXT = f'{TOOLS}/startup_display.txt' STARTUP_TEXT = TOOLS / 'startup_display.txt'
COMMENT = "; please refer to README.md for help" COMMENT = "; please refer to README.md for help"
CONFIG_TEMPLATE = f"""[NETWORK] TEMPLATES = {}
{COMMENT} TEMPLATES['bare-apu'] = f"""{COMMENT}
[BOX]
type=bare-apu
[NETWORK]
eth0=wan eth0=wan
eth1=192.168.2.2 eth1=192.168.2.2
eth2=192.168.3.3 eth2=192.168.3.3
eth3=192.168.127.254 eth3=192.168.127.254
[ROUTER] [ROUTER]
{COMMENT}
3001=192.168.127.254:3001 3001=192.168.127.254:3001
""" """
CONTROLBOX_TEMPLATE = f"""[NETWORK] TEMPLATES['controlbox'] = f"""{COMMENT}
{COMMENT}
eth0=dhcp [BOX]
type=controlbox
[NETWORK]
eth0=wan
eth1=192.168.1.1 eth1=192.168.1.1
eth2=192.168.2.2 eth2=192.168.2.2
eth3=192.168.3.3 eth3=192.168.3.3
[DISPLAY] [DISPLAY]
{COMMENT}
line0=startup... line0=startup...
line1=HOST line1=HOST
line2=ADDR line2=ADDR
""" """
TEMPLATES['dual-eth-rpi'] = f"""{COMMENT}
[BOX]
type=dual-eth-rpi
[NETWORK]
eth0=wan
eth1=192.168.1.1
"""
GENERIC_TEMPLATE = f"""{COMMENT}
[BOX]
type=%s
[NETWORK]
eth0=wan
"""
DHCP_HEADER = """ DHCP_HEADER = """
default-lease-time 600; default-lease-time 600;
max-lease-time 7200; max-lease-time 7200;
@ -133,14 +158,22 @@ box = BoxInfo()
box.hostname_changed = False box.hostname_changed = False
dhcp_server_cfg = [] dhcp_server_cfg = []
TO_SYSTEM = [f'{TOOLS}/to_system', f'{TOOLS}/{box.typ}_system'] TO_SYSTEM = [TOOLS / 'to_system', TOOLS / f'{box.typ}_system']
def do_cmd(command): def do_cmd(*command):
unix_cmd(command, doit) unix_cmd(*command, execute=doit) # with sudo
show.dirty = True show.dirty = True
def copyfiles(srcdir, dstdir, files):
unix_cmd('cp', *(str(srcdir / f) for f in files), str(dstdir))
def deletefiles(srcdir, files):
unix_cmd('rm', '-f', *(str(srcdir / f) for f in files))
def frappy(cfg=None, port=None, requirements='', **kwds): def frappy(cfg=None, port=None, requirements='', **kwds):
if not cfg: if not cfg:
return None return None
@ -173,8 +206,8 @@ def router(firewall=False, **opts):
if not opts: if not opts:
return None return None
try: try:
os.remove(join(TO_SYSTEM[0], 'etc/nftables.conf')) os.remove(TO_SYSTEM[0] / 'etc/nftables.conf')
with open(f'{TOOLS}/requirements.txt') as f: with open(TOOLS / 'requirements.txt') as f:
pip_requirements['root']['tools'] = f.read() pip_requirements['root']['tools'] = f.read()
except FileNotFoundError: except FileNotFoundError:
pass pass
@ -202,18 +235,21 @@ def display(**opts):
def pip(): def pip():
for user, requirements in pip_requirements.items(): for user, requirements in pip_requirements.items():
if user == 'root': if user == 'root':
tmpname = join('/root', 'pip_requirements.tmp') tmpname = '/root/pip_requirements.tmp'
pipcmd = 'pip3 install -r %s' % tmpname pipcmd = ['sudo pip3 install -r', tmpname]
else: else:
tmpname = join('/home', user, 'pip_requirements.tmp') tmpname = f'/home/{user}/pip_requirements.tmp'
pipcmd = 'sudo --user %s pip3 install --user --break-system-packages -r %s' % (user, tmpname) pipcmd = ['pip3 install --user --break-system-packages -r', tmpname]
filename = tmpname.replace('.tmp', '.txt') filename = tmpname.replace('.tmp', '.txt')
content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items()) content = ''.join('# --- for %s ---\n%s\n' % kv for kv in requirements.items())
if write_when_new(filename, content, True): if content:
if doit: if write_when_new(filename, content, True):
os.rename(filename, tmpname) if doit:
if os.system(pipcmd) == 0: os.rename(filename, tmpname)
os.rename(tmpname, filename) if os.system(' '.join(pipcmd)) == 0:
os.rename(tmpname, filename)
else:
os.remove(tmpname)
else: else:
os.remove(tmpname) os.remove(tmpname)
else: else:
@ -225,7 +261,7 @@ def pip():
SERVICES = dict(router=router, frappy=frappy, display=display) SERVICES = dict(router=router, frappy=frappy, display=display)
def write_when_new(filename, content, ignore_reduction=False): def write_when_new(filename, content, as_root=False, ignore_reduction=False):
if content is None: if content is None:
lines = [] lines = []
else: else:
@ -237,6 +273,9 @@ def write_when_new(filename, content, ignore_reduction=False):
old = fil.read() old = fil.read()
except FileNotFoundError: except FileNotFoundError:
old = None old = None
#except Exception as e:
# print('can not read', filename, '-> do not check')
# return False
if old == content: if old == content:
return False return False
if ignore_reduction: if ignore_reduction:
@ -249,10 +288,12 @@ def write_when_new(filename, content, ignore_reduction=False):
return False return False
if doit: if doit:
if lines: if lines:
with open(filename, 'w') as fil: with tempfile.NamedTemporaryFile('w') as fil:
fil.write(content) fil.write(content)
fil.flush()
unix_cmd('cp', fil.name, filename, sudo=as_root)
else: else:
os.remove(filename) unix_cmd('rm', '-f', filename, sudo=as_root)
elif more_info: elif more_info:
print('.' * 80) print('.' * 80)
print('changes in', filename) print('changes in', filename)
@ -314,7 +355,7 @@ def replace_in_file(filename, pat, repl):
if not success: if not success:
raise ValueError(f'{pat} not in {filename}') raise ValueError(f'{pat} not in {filename}')
write_when_new(filename, newcontent) write_when_new(filename, newcontent, as_root=True)
return success[0] return success[0]
@ -356,17 +397,17 @@ def create_if(name, cfg):
def walk(action): def walk(action):
for rootpath in TO_SYSTEM: for rootpath in TO_SYSTEM:
if not exists(rootpath): if not rootpath.exists():
continue continue
os.chdir(rootpath) os.chdir(rootpath)
for dirpath, _, files in os.walk('.'): for dirpath, _, files in os.walk('.'):
syspath = dirpath[1:] # remove leading '.' syspath = Path(dirpath[1:]) # remove leading '.' -> absolute path
action.dirpath = dirpath action.dirpath = Path(dirpath)
action.syspath = syspath action.syspath = syspath
if files: if files:
match, mismatch, missing = filecmp.cmpfiles(dirpath, syspath, files) match, mismatch, missing = filecmp.cmpfiles(dirpath, syspath, files)
if mismatch: if mismatch:
newer = [f for f in mismatch if getmtime(join(syspath, f)) > getmtime(join(dirpath, f))] newer = [f for f in mismatch if getmtime(syspath / f) > getmtime(dirpath / f)]
if newer: if newer:
action.newer(newer) action.newer(newer)
if len(newer) < len(mismatch): if len(newer) < len(mismatch):
@ -375,12 +416,12 @@ def walk(action):
if missing: if missing:
if DEL in missing: if DEL in missing:
missing.remove(DEL) missing.remove(DEL)
with open(join(dirpath, DEL)) as fil: with open(dirpath / DEL) as fil:
to_delete = [] to_delete = []
for fname in fil: for fname in fil:
fname = fname.strip() fname = fname.strip()
if fname and exists(join(syspath, fname)): if fname and exists(syspath / fname):
if exists(join(dirpath, fname)): if (dirpath / fname).exists():
print('ERROR: %s in %s, but also in repo -> ignored' % (fname, DEL)) print('ERROR: %s in %s, but also in repo -> ignored' % (fname, DEL))
else: else:
to_delete.append(fname) to_delete.append(fname)
@ -390,9 +431,8 @@ def walk(action):
class Walker: class Walker:
def __init__(self): dirpath = None
self.dirpath = None syspath = None
self.syspath = None
class Show(Walker): class Show(Walker):
@ -403,7 +443,7 @@ class Show(Walker):
if more_info: if more_info:
for f in files: 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('diff %s %s' % (self.dirpath / f, self.syspath / f))
print('.' * 80) print('.' * 80)
else: else:
print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) print('%s %s:\n %s' % (title, self.syspath, ' '.join(files)))
@ -412,7 +452,7 @@ class Show(Walker):
self.dirty = True self.dirty = True
if more_info: if more_info:
for f in files: for f in files:
print('cat %s' % join(dirpath, f)) print('cat %s' % (dirpath / f))
print('.' * 80) print('.' * 80)
else: else:
print('%s %s:\n %s' % (title, self.syspath, ' '.join(files))) print('%s %s:\n %s' % (title, self.syspath, ' '.join(files)))
@ -432,41 +472,65 @@ class Show(Walker):
class Do(Walker): class Do(Walker):
def newer(self, files): def newer(self, files):
for file in files: copyfiles(self.syspath, self.dirpath, files)
shutil.copy(join(self.syspath, file), join(self.dirpath, file))
def older(self, files): older = newer
for file in files:
shutil.copy(join(self.dirpath, file), join(self.syspath, file))
def missing(self, files): missing = newer
self.older(files)
def delete(self, files): def delete(self, files):
for file in files: deletefiles(self.syspath, files)
os.remove(join(self.syspath, file))
def handle_config(): def handle_config():
dhcp_server_cfg.clear() dhcp_server_cfg.clear()
config = box.read_config() try:
config = box.read_config()
except UndefinedConfigFile as e:
print(e)
cfgfile = box.cfgfile cfgfile = box.cfgfile
newhostname = box.hostname newhostname = box.hostname
if not cfgfile: if cfgfile:
template = CONFIG_TEMPLATE if box.hwtype != 'apu':
try:
typ = config.get('BOX', {}).get('type')
if typ:
box.typ = typ
except ValueError:
pass
else:
if box.typ == 'cm4':
print('WARNING: missing type in BOX section')
print('This is a cm4 so guess its a dual-eth-rpi')
box.typ = 'dual-eth-rpi'
elif box.typ == 'cm3':
print('WARNING: missing type in BOX section')
print('This is a cm3 so guess its a ionopimax')
box.typ = 'ionopimax'
else:
raise ValueError(f'unknown type={typ} defined in BOX section')
else:
if box.typ == 'apu': if box.typ == 'apu':
# determine if display is present # determine if display is present
disp = serial.Serial('/dev/ttyS1', baudrate=115200, timeout=1) disp = serial.Serial('/dev/ttyS1', baudrate=115200, timeout=1)
disp.write(b'\x1b\x1b\x01\xf3') disp.write(b'\x1b\x1b\x01\xf3')
display_available = disp.read(8)[0:4] == b'\x1b\x1b\x05\xf3' display_available = disp.read(8)[0:4] == b'\x1b\x1b\x05\xf3'
if display_available: if display_available:
typ = 'controlbox' box.typ = 'controlbox'
template = CONTROLBOX_TEMPLATE
else: else:
typ = 'bare apu' box.typ = 'bare-apu'
else: elif box.typ == 'cm4':
typ = 'rpi' box.typ = 'dual-eth-rpi'
print('no cfg file found for this', typ, print('This is a cm4, so guess its a dual-eth-rpi - please check type in cfg file')
elif box.typ == 'cm3':
print('This is a cm3 so guess its a ionopimax - please check type in cfg file')
box.typ = 'ionopimax'
template = TEMPLATES.get(box.typ)
if template is None:
box.typ = box.typ or 'unknown-box'
template = GENERIC_TEMPLATE % box.typ
print('no cfg file found for this', box.typ,
f'with id {box.id:06x} (hostname={box.hostname})') f'with id {box.id:06x} (hostname={box.hostname})')
newhostname = input('enter host name: ') newhostname = input('enter host name: ')
if not newhostname: if not newhostname:
@ -483,7 +547,7 @@ def handle_config():
newhostname = basename(cfgfile).rpartition('_')[0] newhostname = basename(cfgfile).rpartition('_')[0]
if doit: if doit:
print('bash sethostname.sh') print('bash sethostname.sh')
os.system(f'bash {TOOLS}/sethostname.sh') unix_cmd('bash', f'{TOOLS}/sethostname.sh')
else: else:
if cfgfile: if cfgfile:
print('replace host name %r by %r' % (box.hostname, newhostname)) print('replace host name %r by %r' % (box.hostname, newhostname))
@ -498,12 +562,14 @@ def handle_config():
netcfg = config['NETWORK'] netcfg = config['NETWORK']
for name in netcfg: for name in netcfg:
if name not in box.macaddr: if name not in box.macaddr:
print(name)
print(box.macaddr)
print(f'{name} is not a valid network interface name') print(f'{name} is not a valid network interface name')
raise RuntimeError('network interface name system does not match') raise RuntimeError('network interface name system does not match')
for ifname in box.macaddr: for ifname in box.macaddr:
content = create_if(ifname, netcfg.get(ifname, 'off')) content = create_if(ifname, netcfg.get(ifname, 'off'))
# content = '\n'.join('%s=%s' % kv for kv in content.items()) # content = '\n'.join('%s=%s' % kv for kv in content.items())
todo = write_when_new(f'/etc/network/interfaces.d/{ifname}', content) todo = write_when_new(f'/etc/network/interfaces.d/{ifname}', content, as_root=True)
if todo and not more_info: if todo and not more_info:
print('change', ifname) print('change', ifname)
show.dirty = True show.dirty = True
@ -522,7 +588,7 @@ def handle_config():
content.append(' range %s %s;\n' % rng) content.append(' range %s %s;\n' % rng)
content.append('}\n') content.append('}\n')
content = ''.join(content) content = ''.join(content)
todo = write_when_new('/etc/dhcp/dhcpd.conf', content) todo = write_when_new('/etc/dhcp/dhcpd.conf', content, as_root=True)
if todo: if todo:
print('change dhcpd.conf') print('change dhcpd.conf')
to_start['isc-dhcp-server'] = 'restart' to_start['isc-dhcp-server'] = 'restart'
@ -532,7 +598,7 @@ def handle_config():
r'INTERFACESv4="(.*)"', ' '.join([n for n in box.macaddr if n != box.main_if])) r'INTERFACESv4="(.*)"', ' '.join([n for n in box.macaddr if n != box.main_if]))
except FileNotFoundError: except FileNotFoundError:
if 'yes'.startswith(input('install isc-dhcp-server? [y]')): if 'yes'.startswith(input('install isc-dhcp-server? [y]')):
os.system('apt-get install isc-dhcp-server') unix_cmd('apt-get install isc-dhcp-server')
exit() exit()
elif doit: elif doit:
unix_cmd('systemctl stop isc-dhcp-server') unix_cmd('systemctl stop isc-dhcp-server')
@ -560,7 +626,7 @@ def handle_config():
to_start[service] = 'enable' to_start[service] = 'enable'
elif not active: elif not active:
to_start[service] = 'restart' to_start[service] = 'restart'
if write_when_new(f'/etc/systemd/system/{service}.service', servicecfg): if write_when_new(f'/etc/systemd/system/{service}.service', servicecfg, as_root=True):
show.dirty = True show.dirty = True
reload_systemd = True reload_systemd = True
if servicecfg and to_start.get('service') is None: if servicecfg and to_start.get('service') is None:
@ -618,7 +684,7 @@ else:
doit = True doit = True
handle_config() handle_config()
walk(Do()) walk(Do())
with open(f'{TOOLS}/current', 'w') as f: with open(TOOLS / 'current', 'w') as f:
f.write(result) f.write(result)
elif 'more'.startswith(answer.lower()): elif 'more'.startswith(answer.lower()):
more_info = True more_info = True

51
to_dual-eth-rpi/boot/config.txt Executable file
View File

@ -0,0 +1,51 @@
# For more options and information see
# http://rptl.io/configtxt
# Some settings may impact device functionality. See link above for details
# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=off
#dtparam=i2s=on
#dtparam=spi=on
# Enable audio (loads snd_bcm2835)
dtparam=audio=on
# Additional overlays and parameters are documented
# /boot/firmware/overlays/README
# Automatically load overlays for detected cameras
camera_auto_detect=1
# Automatically load overlays for detected DSI displays
display_auto_detect=1
# Automatically load initramfs files, if found
auto_initramfs=1
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2
# Don't have the firmware create an initial video= setting in cmdline.txt.
# Use the kernel's default instead.
disable_fw_kms_setup=1
# Run in 64-bit mode
arm_64bit=1
# Disable compensation for displays with overscan
disable_overscan=1
# Run as fast as firmware / board allows
arm_boost=1
[cm4]
# Enable host mode on the 2711 built-in XHCI USB controller.
# This line should be removed if the legacy DWC2 controller is required
# (e.g. for USB device mode) or if USB support is not required.
otg_mode=1
[all]
# runtime clock
dtoverlay=mcp7941x

View File

@ -0,0 +1,2 @@
99-ionopimax.rules
99-ionopi.rules

51
to_ionopi/boot/config.txt Executable file
View File

@ -0,0 +1,51 @@
# For more options and information see
# http://rptl.io/configtxt
# Some settings may impact device functionality. See link above for details
# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=off
#dtparam=i2s=on
#dtparam=spi=on
# Enable audio (loads snd_bcm2835)
dtparam=audio=on
# Additional overlays and parameters are documented
# /boot/firmware/overlays/README
# Automatically load overlays for detected cameras
camera_auto_detect=1
# Automatically load overlays for detected DSI displays
display_auto_detect=1
# Automatically load initramfs files, if found
auto_initramfs=1
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2
# Don't have the firmware create an initial video= setting in cmdline.txt.
# Use the kernel's default instead.
disable_fw_kms_setup=1
# Run in 64-bit mode
arm_64bit=1
# Disable compensation for displays with overscan
disable_overscan=1
# Run as fast as firmware / board allows
arm_boost=1
[cm4]
# Enable host mode on the 2711 built-in XHCI USB controller.
# This line should be removed if the legacy DWC2 controller is required
# (e.g. for USB device mode) or if USB support is not required.
otg_mode=1
[all]
dtoverlay=ionopi
# runtime clock
dtoverlay=i2c-rtc,mcp7941x

View File

@ -0,0 +1 @@
SUBSYSTEM=="ionopi", PROGRAM="/bin/sh -c 'find -L /sys/class/ionopi/ -maxdepth 2 -exec chown root:ionopi {} \; || true'"

View File

@ -0,0 +1 @@
99-ionopimax.rules

51
to_ionopimax/boot/config.txt Executable file
View File

@ -0,0 +1,51 @@
# For more options and information see
# http://rptl.io/configtxt
# Some settings may impact device functionality. See link above for details
# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=off
#dtparam=i2s=on
#dtparam=spi=on
# Enable audio (loads snd_bcm2835)
dtparam=audio=on
# Additional overlays and parameters are documented
# /boot/firmware/overlays/README
# Automatically load overlays for detected cameras
camera_auto_detect=1
# Automatically load overlays for detected DSI displays
display_auto_detect=1
# Automatically load initramfs files, if found
auto_initramfs=1
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2
# Don't have the firmware create an initial video= setting in cmdline.txt.
# Use the kernel's default instead.
disable_fw_kms_setup=1
# Run in 64-bit mode
arm_64bit=1
# Disable compensation for displays with overscan
disable_overscan=1
# Run as fast as firmware / board allows
arm_boost=1
[cm4]
# Enable host mode on the 2711 built-in XHCI USB controller.
# This line should be removed if the legacy DWC2 controller is required
# (e.g. for USB device mode) or if USB support is not required.
otg_mode=1
[all]
dtoverlay=ionopimax
# runtime clock
dtoverlay=i2c-rtc,mcp7941x

View File

@ -0,0 +1 @@
SUBSYSTEM=="ionopimax", PROGRAM="/bin/sh -c 'find -L /sys/class/ionopimax/ -maxdepth 2 -exec chown root:ionopimax {} \; || true'"

View File

@ -0,0 +1 @@
99-ionopi.rules

View File

@ -3,6 +3,7 @@ import socket
import threading import threading
import re import re
from glob import glob from glob import glob
from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
from netifaces import interfaces, ifaddresses, gateways, AF_INET, AF_LINK from netifaces import interfaces, ifaddresses, gateways, AF_INET, AF_LINK
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
@ -19,13 +20,18 @@ else:
os.system(cmd) os.system(cmd)
class UndefinedConfigFile(Exception):
"""config file not found or ambiguous"""
class BoxInfo: class BoxInfo:
TOOLS = '/home/l_samenv/boxtools' TOOLS = Path('/home/l_samenv/boxtools')
CFGPATH = f'{TOOLS}/cfg/%s_%06x.cfg' CFGPATH = str(TOOLS / 'cfg' / '%s_%06x.cfg')
BOX_TYPES = { BOX_TYPES = {
'00:0d:b9': 'apu', # bare apu or control box '00:0d:b9': 'apu', # bare apu or control box
'b8:27:eb': 'cm3', # iono pi 'b8:27:eb': 'cm3', # guess iono pi max
'd8:3a:dd': 'cm4', # dual-eth-rpi 'e4:5f:01': 'ionopi', # guess iono pi
'd8:3a:dd': 'cm4', # guess dual-eth-rpi
} }
def __init__(self): def __init__(self):
@ -50,6 +56,7 @@ class BoxInfo:
self.id = int(''.join(addr.split(':')[-3:]), 16) & 0xffffff self.id = int(''.join(addr.split(':')[-3:]), 16) & 0xffffff
self.typ = self.BOX_TYPES.get(addr[:8]) self.typ = self.BOX_TYPES.get(addr[:8])
self.main_if = ifname self.main_if = ifname
self.hwtype = self.typ # this is one of the values in BOX_TYPE and will not change
def get_macaddr(self): def get_macaddr(self):
return self.macaddr.get(self.main_if) return self.macaddr.get(self.main_if)
@ -57,9 +64,9 @@ class BoxInfo:
def read_config(self, section=None): def read_config(self, section=None):
cfgfiles = glob(self.CFGPATH % ('*', self.id)) cfgfiles = glob(self.CFGPATH % ('*', self.id))
if len(cfgfiles) > 1: if len(cfgfiles) > 1:
raise ValueError('ambiguous cfgfile: %r' % cfgfiles) raise AmbiguousConfigFile('ambiguous cfgfile: %r' % cfgfiles)
if section and not cfgfiles: if section and not cfgfiles:
raise ValueError('no cfg file found for %s' % self.id) raise UndefinedConfigFile('no cfg file found for %s' % self.id)
if cfgfiles: if cfgfiles:
self.cfgfile = cfgfiles[0] self.cfgfile = cfgfiles[0]
else: else:
@ -141,14 +148,16 @@ class MainIf:
return self.carrier, self.ip, self.hostnameresult[0], self.gateway return self.carrier, self.ip, self.hostnameresult[0], self.gateway
def unix_cmd(command, execute=None, stdout=PIPE): def unix_cmd(cmd, *args, execute=None, stdout=PIPE, sudo=True):
if execute != False: # None or True command = cmd.split() + list(args)
sudo = ['sudo'] if sudo else []
if execute is not False: # None or True
if execute: if execute:
print('$ %s' % command) print('$', *command)
result = Popen(command.split(), stdout=stdout).communicate()[0] result = Popen(sudo + command, stdout=stdout).communicate()[0]
return (result or b'').decode() return (result or b'').decode()
else: else:
print('> %s' % command) print('>', *command)
def check_service(service, set_on=None, execute=None): def check_service(service, set_on=None, execute=None):
@ -185,15 +194,12 @@ def change_firewall(set_on, ports, execute=None):
ports.add(22) # always add ssh ports.add(22) # always add ssh
active, enabled = check_service('nftables') active, enabled = check_service('nftables')
if not set_on: if not set_on:
if os.geteuid() == 0: check_service('nftables', False, execute)
check_service('nftables', False, execute)
else:
print('need sudo rights to modify firewall')
return active or enabled return active or enabled
pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL) pattern = re.compile('(tcp dport ({.*}) ct state new accept)', re.MULTILINE | re.DOTALL)
for filename in FIREWALL_CONF, os.path.join(BoxInfo.TOOLS, 'nftables.conf'): for filename in FIREWALL_CONF, BoxInfo.TOOLS / 'nftables.conf':
with open(filename) as f: with open(filename) as f:
content = f.read() content = f.read()