Files
boxtools/install.py

563 lines
19 KiB
Python
Executable File

#!/usr/bin/python3
"""install.py
- copy files from to_system into system directories
- set host name / network settings from aputools/servercfg file
"""
if bytes == str:
raise NotImplementedError('python2 not supported')
import sys
import os
import filecmp
import shutil
import serial
import types
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'
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 = f"""[NETWORK]
{COMMENT}
enp1s0=192.168.127.254
enp2s0=192.168.2.2
enp3s0=192.168.3.3
enp4s0=dhcp
[ROUTER]
{COMMENT}
3001=192.168.127.254:3001
"""
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;
max-lease-time 7200;
authoritative;
"""
SERVICES = dict(router=router, frappy=frappy, display=display)
net_addr = {} # dict <if name> of <ethernet address>
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 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:
for f in files:
if f.endswith(more_info):
print('diff %s %s' % (join(self.dirpath, f), join(self.syspath, f)))
def show(self, title, dirpath, files):
if more_info == 'NONE':
print('%s %s:\n %s' % (title, self.syspath, ' '.join(files)))
else:
for f in files:
if f.endswith(more_info):
print('cat %s' % join(dirpath, f))
def newer(self, files):
self.show('get from', self.dirpath, files)
def older(self, files):
self.show('replace in', self.dirpath, files)
def missing(self, files):
self.show('install in', self.dirpath, files)
def delete(self, files):
self.show('remove from', self.syspath, files)
class Do(Walker):
def newer(self, files):
for file in files:
shutil.copy(join(self.syspath, file), join(self.dirpath, file))
def older(self, files):
for file in files:
shutil.copy(join(self.dirpath, file), join(self.syspath, file))
def missing(self, files):
self.older(files)
def delete(self, files):
for file in files:
os.remove(join(self.syspath, file))
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:
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)
print('---')
to_install = Install(False)
if not result:
print('fix first above errors')
elif to_system.dirty and more_info == 'NONE':
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')