introduce 'to_home' and systemd user services

- frappy is now a systemd user service
- add 'frappy' and 'boxweb' bash function
This commit is contained in:
2025-05-28 11:54:21 +02:00
parent 1a6b133afa
commit 99580aad78
5 changed files with 134 additions and 82 deletions

View File

@ -128,11 +128,22 @@ WantedBy = multi-user.target
FRAPPY_SERVICE = """[Unit]
Description = Running frappy server
[Service]
Environment=PYTHONPATH=/home/l_samenv/.local/lib/python3.11/site-packages/
ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/frappy-server %s
[Install]
WantedBy = default.target
"""
DISPLAY_SERVICE = f"""[Unit]
Description = status display
After = network.target
[Service]
User = l_samenv
ExecStart = /usr/bin/python3 /home/l_samenv/frappy/bin/frappy-server %s
User = root
ExecStart = /usr/bin/python3 {TOOLS}/display.py -d
[Install]
WantedBy = multi-user.target
@ -150,6 +161,17 @@ ExecStart = /usr/bin/python3 {TOOLS}/display.py -d
WantedBy = multi-user.target
"""
BOXWEB_SERVICE = """[Unit]
Description = Web service for local GUI on a box
After = network.target
[Service]
Environment=PYTHONPATH=/home/l_samenv/.local/lib/python3.11/site-packages/
ExecStart = /usr/bin/python3 /home/l_samenv/boxweb/flaskserver.py %i {port}
[Install]
WantedBy = multi-user.target
"""
pip_requirements = {
'root': {},
@ -164,8 +186,8 @@ dhcp_server_cfg = []
TO_SYSTEM = TOOLS / 'to_system', TOOLS / f'{box.typ}_system'
def do_cmd(*command):
unix_cmd(*command, execute=doit) # sudo=True by default
def do_cmd(*command, sudo=True):
unix_cmd(*command, execute=doit, sudo=sudo)
show.dirty = True
@ -179,7 +201,7 @@ def frappy(cfg=None, port=None, requirements='', **kwds):
cfg = '-p %s %s' % (port, cfg)
with open('/home/l_samenv/frappy/requirements.txt') as f:
req['frappy main'] = f.read()
return FRAPPY_SERVICE % cfg
return False, FRAPPY_SERVICE % cfg
def router(firewall=False, **opts):
@ -207,7 +229,7 @@ def router(firewall=False, **opts):
pip_requirements['root']['tools'] = f.read()
except FileNotFoundError:
pass
return ROUTER_SERVICE
return True, ROUTER_SERVICE
def display_update(cfg):
@ -225,7 +247,7 @@ def display_update(cfg):
def display(**opts):
if not opts:
return None
return DISPLAY_SERVICE
return True, DISPLAY_SERVICE
def pip():
@ -253,7 +275,13 @@ def pip():
show.dirty = True
SERVICES = dict(router=router, frappy=frappy, display=display)
def boxweb(port=80, **opts):
port = int(port)
return port <= 1024, BOXWEB_SERVICE.format(port=port)
SERVICES = dict(router=router, display=display, frappy=frappy, boxweb=boxweb)
AS_ROOT = {'router', 'display'}
def write_when_new(filename, content, as_root=False, ignore_reduction=False):
@ -391,44 +419,54 @@ def create_if(name, cfg):
return result + '\n'
def walk_dir(action, srcpath, dstroot, files):
if not files:
return
action.srcpath = srcpath = Path(srcpath)
action.dstpath = dstpath = dstroot / srcpath
match, mismatch, missing = filecmp.cmpfiles(srcpath, dstpath, files)
if mismatch:
newer = [f for f in mismatch if getmtime(dstpath / f) > getmtime(srcpath / 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(srcpath / DEL) as fil:
to_delete = []
for fname in fil:
fname = fname.strip()
if fname and exists(dstpath / fname):
if (srcpath / fname).exists():
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)
def walk(action):
action.as_root = True
for rootpath in TO_SYSTEM:
if not rootpath.exists():
continue
os.chdir(rootpath)
for dirpath, _, files in os.walk('.'):
syspath = Path(dirpath[1:]) # remove leading '.' -> absolute path
action.dirpath = dirpath = Path(dirpath)
action.syspath = syspath
if files:
match, mismatch, missing = filecmp.cmpfiles(dirpath, syspath, files)
if mismatch:
newer = [f for f in mismatch if getmtime(syspath / f) > getmtime(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(dirpath / DEL) as fil:
to_delete = []
for fname in fil:
fname = fname.strip()
if fname and exists(syspath / fname):
if (dirpath / fname).exists():
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)
for srcpath, _, files in os.walk('.'):
walk_dir(action, srcpath, Path('/'), files)
os.chdir(TOOLS / 'to_home')
action.as_root = False
for srcpath, _, files in os.walk('.'):
walk_dir(action, srcpath, Path.home(), files)
class Walker:
dirpath = None
syspath = None
srcpath = None
dstpath = None
as_root = None
class Show(Walker):
@ -439,46 +477,45 @@ class Show(Walker):
if more_info:
for f in files:
if f.endswith(more_info):
print('diff %s %s' % (self.dirpath / f, self.syspath / f))
print('diff %s %s' % (self.srcpath / f, self.dstpath / f))
print('.' * 80)
else:
print('%s %s:\n %s' % (title, self.syspath, ' '.join(files)))
print('%s %s:\n %s' % (title, self.dstpath, ' '.join(files)))
def show(self, title, dirpath, files):
def show(self, title, srcpath, files):
self.dirty = True
if more_info:
for f in files:
print('cat %s' % (dirpath / f))
print('cat %s' % (srcpath / f))
print('.' * 80)
else:
print('%s %s:\n %s' % (title, self.syspath, ' '.join(files)))
print('%s %s:\n %s' % (title, self.dstpath, ' '.join(files)))
def newer(self, files):
self.show('get from', self.dirpath, files)
self.show('get from', self.srcpath, files)
def older(self, files):
self.show('replace in', self.dirpath, files)
self.show('replace in', self.srcpath, files)
def missing(self, files):
self.show('install in', self.dirpath, files)
self.show('install in', self.srcpath, files)
def delete(self, files):
self.show('remove from', self.syspath, files)
self.show('remove from', self.dstpath, files)
class Do(Walker):
def newer(self, files):
usesudo = not self.syspath.is_relative_to(Path.home())
unix_cmd('mkdir', '-p', str(self.syspath), sudo=usesudo)
unix_cmd('cp', *(str(self.dirpath / f) for f in files), str(self.syspath),
sudo=usesudo)
unix_cmd('mkdir', '-p', str(self.dstpath), sudo=self.as_root)
unix_cmd('cp', *(str(self.srcpath / f) for f in files), str(self.dstpath),
sudo=self.as_root)
older = newer
missing = newer
def delete(self, files):
unix_cmd('rm', '-f', *(str(self.syspath / f) for f in files))
unix_cmd('rm', '-f', *(str(self.dstpath / f) for f in files))
def handle_config():
@ -562,15 +599,15 @@ def handle_config():
box.hostname = newhostname
if cfgfile is None:
return False
to_start = {}
to_start = {} # dict <service> of <action>, <as_root>
ifname = ''
try:
netcfg = config.get('NETWORK', {})
if netcfg: # when not network is specified, do not handle network at all
for name in netcfg:
if name not in box.network_interfaces:
print(f'{name} is not a valid network interface name')
raise RuntimeError('network interface name system does not match')
print(f'{name} is currently not a valid network interface - skip')
continue
for ifname in box.network_interfaces:
content = create_if(ifname, netcfg.get(ifname, 'off'))
# content = '\n'.join('%s=%s' % kv for kv in content.items())
@ -578,7 +615,7 @@ def handle_config():
if todo and not more_info:
print('change', ifname)
show.dirty = True
to_start[ifname] = 'if_restart'
to_start[ifname] = 'if_restart', True
if dhcp_server_cfg:
content = [DHCP_HEADER]
for subnet, rangelist in dhcp_server_cfg:
@ -596,7 +633,7 @@ def handle_config():
todo = write_when_new('/etc/dhcp/dhcpd.conf', content, as_root=True)
if todo:
print('change dhcpd.conf')
to_start['isc-dhcp-server'] = 'restart'
to_start['isc-dhcp-server'] = 'restart', True
show.dirty = True
try:
if replace_in_file(
@ -612,7 +649,7 @@ def handle_config():
unix_cmd('systemctl disable isc-dhcp-server')
displaycfg = config.get('DISPLAY')
if displaycfg and display_update(displaycfg):
to_start['display'] = 'restart'
to_start['display'] = 'restart', True
if change_to_gitea(doit):
show.dirty = True
except Exception as e:
@ -620,43 +657,54 @@ def handle_config():
raise
# return False
reload_systemd = False
reload_systemd = set()
for service, service_func in SERVICES.items():
section = service.upper()
section_dict = config.get(section, {})
servicecfg = service_func(**section_dict)
ret = service_func(**section_dict)
active, enabled = check_service(service)
if servicecfg is None:
if ret is None:
if active or enabled:
check_service(service, False, doit)
show.dirty = True
continue
else:
as_root, servicecfg = ret
if not enabled:
to_start[service] = 'enable'
to_start[service] = 'enable', as_root
elif not active:
to_start[service] = 'restart'
if write_when_new(f'/etc/systemd/system/{service}.service', servicecfg, as_root=True):
to_start[service] = 'restart', as_root
if as_root:
systemdir = Path('/etc/systemd/system')
else:
systemdir = Path('~/.config/systemd/user').expanduser()
if write_when_new(systemdir / f'{service}.service', servicecfg, as_root=as_root):
show.dirty = True
reload_systemd = True
reload_systemd.add(as_root)
if servicecfg and to_start.get('service') is None:
to_start[service] = 'restart'
to_start[service] = 'restart', as_root
if 'dialout' not in unix_cmd('id l_samenv'):
do_cmd('usermod -a -G dialout l_samenv')
pip()
if reload_systemd:
do_cmd('systemctl daemon-reload')
for service, action in to_start.items():
for as_root in reload_systemd:
if as_root:
do_cmd('systemctl daemon-reload', sudo=True)
else:
do_cmd('systemctl --user daemon-reload', sudo=False)
for service, (action, _) in to_start.items():
show.dirty = True
if action == 'if_restart':
do_cmd(f'ifdown {service}')
for service, action in to_start.items():
for service, (action, as_root) in to_start.items():
if action == 'if_restart':
do_cmd(f'ifup {service}')
else:
if action == 'restart':
do_cmd(f'systemctl restart {service}')
do_cmd(f'systemctl enable {service}')
if action == 'restart': # else 'enable'
do_cmd('systemctl', 'restart', service, sudo=as_root)
if not as_root:
do_cmd(f'loginctl enable-linger l_samenv')
do_cmd('systemctl', 'enable', service, sudo=as_root)
if box.change_if_names:
print('interface name system has to be changed from enp*s0 to eth*')
@ -674,7 +722,6 @@ def handle_config():
return '\n'.join(result)
more_info = False
doit = False
print(' ')
show = Show()

View File

@ -202,17 +202,22 @@ class MainIf:
def unix_cmd(cmd, *args, execute=None, stdout=PIPE, sudo=True):
command = cmd.split() + list(args)
sudo = ['sudo'] if sudo else []
if command[0] == 'systemctl' and not sudo:
command.insert(1, '--user')
if sudo:
command.insert(0, 'sudo')
# sudo = ['sudo'] if sudo else []
if execute is not False: # None or True
if execute:
print('$', *command)
result = Popen(sudo + command, stdout=stdout).communicate()[0]
# result = Popen(sudo + command, stdout=stdout).communicate()[0]
result = Popen(command, stdout=stdout).communicate()[0]
return (result or b'').decode()
else:
print('>', *command)
def check_service(service, set_on=None, execute=None):
def check_service(service, set_on=None, execute=None, as_root=True):
"""check or set state of systemd service
set_on is None or not given: query only
@ -221,7 +226,7 @@ def check_service(service, set_on=None, execute=None):
sim: print out command instead of executing
"""
result = unix_cmd(f'systemctl show -p WantedBy -p ActiveState {service}')
result = unix_cmd(f'systemctl show -p WantedBy -p ActiveState {service}', sudo=False)
enabled = False
active = False
for line in result.split('\n'):
@ -231,14 +236,14 @@ def check_service(service, set_on=None, execute=None):
active = True
if set_on:
if not active:
unix_cmd(f'systemctl start {service}', execute=execute)
unix_cmd('systemctl', 'start', service, execute=execute, sudo=as_root)
if not enabled:
unix_cmd(f'systemctl enable {service}', execute=execute)
unix_cmd('systemctl', 'enable', service, execute=execute, sudo=as_root)
elif set_on is not None:
if active:
unix_cmd(f'systemctl stop {service}', execute=execute)
unix_cmd('systemctl', 'stop', service, execute=execute, sudo=as_root)
if enabled:
unix_cmd(f'systemctl disable {service}', execute=execute)
unix_cmd('systemctl', 'disable', service, execute=execute, sudo=as_root)
return active, enabled