#!/usr/bin/env python3 import cgi, cgitb from collections import namedtuple import json import os import pwd import shlex import socket import subprocess from subprocess import DEVNULL, PIPE, STDOUT def split_by(xs, sep): group = [] for x in xs: if x == sep: yield group group = [] else: group.append(x) yield group def headers(): print("Content-Type: text/html; charset=utf-8") print() def debugging(): # TODO: figure out how to include this on the web page #cgitb.enable() pass def setup(): headers() debugging() def format_response(response): return json.dumps(response, ensure_ascii=False) def run(*args, **kwargs): output = subprocess.run(args, stdin=DEVNULL, stdout=PIPE, stderr=STDOUT, universal_newlines=True, **kwargs) return output.stdout class Command: def __init__(self, *args): self.args = args self.env = None def run(self): return run(*self.args, env=self.env) class Systemd(Command): def __init__(self, *args): super().__init__(*args) self.env = self.make_env() @staticmethod def make_env(): env = os.environ.copy() env['SYSTEMD_PAGER'] = '' env['SYSTEMD_COLORS'] = 'no' return env @staticmethod def su(user, cmd): new = Systemd('su', '-c', shlex.join(cmd.args), user.name) new.env = Systemd.fix_su_env(user, cmd.env.copy()) return new @staticmethod def fix_su_env(user, env): # https://unix.stackexchange.com/q/483948 # https://unix.stackexchange.com/q/346841 # https://unix.stackexchange.com/q/423632 # https://unix.stackexchange.com/q/245768 # https://unix.stackexchange.com/q/434494 env['XDG_RUNTIME_DIR'] = user.runtime_dir # I'm not sure the bus part works everywhere. bus_path = os.path.join(user.runtime_dir, 'bus') env['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=' + bus_path return env class Systemctl(Systemd): def __init__(self, *args): super().__init__('systemctl', *args, '--full') @staticmethod def system(*args): return Systemctl('--system', *args) @staticmethod def user(*args): return Systemctl('--user', *args) class Loginctl(Systemd): def __init__(self, *args): super().__init__('loginctl', *args) User = namedtuple('User', ['uid', 'name']) SystemdUser = namedtuple('SystemdUser', ['uid', 'name', 'runtime_dir']) def running_as_root(): # AFAIK, Python's http.server drops root privileges and executes the scripts as # user nobody. # It does no such thing if run as a regular user though. return os.geteuid() == 0 or running_as_nobody() def running_as_nobody(): for user in users(): if user.name == 'nobody': return user.uid == os.geteuid() return False def get_current_user(): uid = os.getuid() entry = pwd.getpwuid(uid) return User(entry.pw_uid, entry.pw_name) # A pitiful attempt to find a list of possibly-systemd-enabled users follows # (i.e. users that might be running a per-user systemd instance). # I don't know of a better way than probing /run/user/UID. # Maybe running something like `loginctl list-sessions` would be better? # These are the default values, see [1]. # The actual values are specified in /etc/login.defs. # Still, they can be overridden on the command line, so I don't think there # actually is a way to list non-system users (imma call them "human"). # # [1]: https://man7.org/linux/man-pages/man8/useradd.8.html UID_MIN = 1000 UID_MAX = 60000 def users(): for entry in pwd.getpwall(): yield User(entry.pw_uid, entry.pw_name) def human_users(): for user in users(): if user.uid < UID_MIN: continue if user.uid > UID_MAX: continue yield user # You know what, loginctl might just be the answer. # Namely, `loginctl list-users`: during my testing, it did only list the users # that were running a systemd instance. def systemd_users(): def list_users(): output = Loginctl('list-users', '--no-legend').run() lines = output.splitlines() if not lines: return for line in lines: # This assumes user names cannot contain spaces. # loginctl list-users output must be in the UID NAME format. info = line.split(' ', 2) if len(info) < 2: raise RuntimeError(f'invalid `loginctl list-users` output:\n{output}') uid, user = info[0], info[1] yield User(uid, user) def show_users(users): properties = 'UID', 'Name', 'RuntimePath' prop_args = (arg for prop in properties for arg in ('-p', prop)) user_args = (user.name for user in users) output = Loginctl('show-user', *prop_args, '--value', *user_args).run() lines = output.splitlines() # Assuming that for muptiple users, the properties will be separated by # an empty string. groups = split_by(lines, '') for group in groups: if len(group) != len(properties): raise RuntimeError(f'invalid `loginctl show-user` output:\n{output}') yield SystemdUser(int(group[0]), group[1], group[2]) return show_users(list_users()) def hostname(): return socket.gethostname() def top(): return run('top', '-b', '-n', '1', '-w', '512') def su(user, commands): return {k: Systemd.su(user, cmd) for k, cmd in commands.items()} def run_all(commands): return {k: cmd.run() for k, cmd in commands.items()} def status_system(): return { 'overview': Systemctl.system('status'), 'failed': Systemctl.system('list-units', '--failed'), 'timers': Systemctl.system('list-timers', '--all'), } def status_user(): return { 'overview': Systemctl.user('status'), 'failed': Systemctl.user('list-units', '--failed'), 'timers': Systemctl.user('list-timers', '--all'), } def timers_system(): return { 'timers': Systemctl.system('list-timers', '--all'), } def timers_user(): return { 'timers': Systemctl.user('list-timers', '--all'), } def system(commands): return run_all(commands()) def user(commands): if running_as_root(): return {user.name: run_all(su(user, commands())) for user in systemd_users()} else: return {get_current_user().name: run_all(commands())} def status(): status = { 'hostname': hostname(), 'top': top(), 'system': system(status_system), 'user': user(status_user), } return status def timers(): timers = { 'system': system(timers_system), 'user': user(timers_user), } return timers def do(): params = cgi.FieldStorage() what = params['what'].value if what == 'status': response = status() elif what == 'timers': response = timers() elif what == 'top': response = top() else: raise RuntimeError(f'invalid parameter "what": {what}') print(format_response(response)) def main(): setup() do() if __name__ == '__main__': main()