diff options
-rwxr-xr-x | cgi-bin/get.py | 237 | ||||
-rwxr-xr-x | cgi-bin/get.sh | 17 | ||||
-rw-r--r-- | index.html | 150 |
3 files changed, 317 insertions, 87 deletions
diff --git a/cgi-bin/get.py b/cgi-bin/get.py index 99c4569..0a1fde8 100755 --- a/cgi-bin/get.py +++ b/cgi-bin/get.py @@ -1,11 +1,25 @@ #!/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 PIPE, STDOUT +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(): @@ -29,10 +43,157 @@ def format_response(response): def run(*args, **kwargs): - output = subprocess.run(args, stdout=PIPE, stderr=STDOUT, universal_newlines=True, **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() @@ -41,69 +202,67 @@ def top(): return run('top', '-b', '-n', '1', '-w', '512') -systemd_env = os.environ.copy() -systemd_env['SYSTEMD_PAGER'] = '' -systemd_env['SYSTEMD_COLORS'] = 'no' - +def su(user, commands): + return {k: Systemd.su(user, cmd) for k, cmd in commands.items()} -def run_systemd_command(*args): - return run(*args, env=systemd_env) +def run_all(commands): + return {k: cmd.run() for k, cmd in commands.items()} -def run_systemctl(*args): - return run_systemd_command('systemctl', *args) - -def system_status_overview(): - return run_systemctl('--system', 'status', '--full') +def status_system(): + return { + 'overview': Systemctl.system('status'), + 'failed': Systemctl.system('list-units', '--failed'), + 'timers': Systemctl.system('list-timers', '--all'), + } -def system_status_failed(): - return run_systemctl('--system', 'list-units', '--failed', '--full') +def status_user(): + return { + 'overview': Systemctl.user('status'), + 'failed': Systemctl.user('list-units', '--failed'), + 'timers': Systemctl.user('list-timers', '--all'), + } -def user_status_overview(): - return run_systemctl('--user', 'status', '--full') +def timers_system(): + return { + 'timers': Systemctl.system('list-timers', '--all'), + } -def user_status_failed(): - return run_systemctl('--user', 'list-units', '--failed', '--full') +def timers_user(): + return { + 'timers': Systemctl.user('list-timers', '--all'), + } -def system_timers(): - return run_systemctl('--system', 'list-timers', '--all', '--full') +def system(commands): + return run_all(commands()) -def user_timers(): - return run_systemctl('--user', 'list-timers', '--all', '--full') +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': { - 'overview': system_status_overview(), - 'failed': system_status_failed(), - 'timers': system_timers(), - }, - 'user': { - 'overview': user_status_overview(), - 'failed': user_status_failed(), - 'timers': user_timers(), - }, + 'system': system(status_system), + 'user': user(status_user), } return status def timers(): timers = { - 'system': { - 'timers': system_timers(), - }, - 'user': { - 'timers': user_timers(), - }, + 'system': system(timers_system), + 'user': user(timers_user), } return timers diff --git a/cgi-bin/get.sh b/cgi-bin/get.sh new file mode 100755 index 0000000..ea4f4ce --- /dev/null +++ b/cgi-bin/get.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o pipefail + +script_dir="$( dirname -- "${BASH_SOURCE[0]}" )" +script_dir="$( cd -- "$script_dir" && pwd )" +readonly script_dir + +# Python's http.server runs CGI scripts under user nobody. +# This is not what we want unfortunately. +# The best solution I could find so far is to create an entry in +# /etc/sudoers.d, allowing the nobody user to run the real scripts w/ sudo. +if [ "$( id --user --name )" == nobody ]; then + sudo --non-interactive --preserve-env "$script_dir/get.py" +else + "$script_dir/get.py" +fi @@ -35,38 +35,25 @@ h1, .h1 { <pre class="pre-scrollable" id="top"></pre> </div> <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_failed_system">+</button><a href="#collapse_systemctl_failed_system" data-toggle="collapse"><code>systemctl --system list-units --failed</code></a></p> - <div class="collapse show" id="collapse_systemctl_failed_system"> - <pre class="pre-scrollable" id="systemctl_failed_system"></pre> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_failed_system">+</button><a href="#collapse_failed_system" data-toggle="collapse"><code>systemctl --system list-units --failed</code></a></p> + <div class="collapse show" id="collapse_failed_system"> + <pre class="pre-scrollable" id="failed_system"></pre> </div> <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_failed_user">+</button><a href="#collapse_systemctl_failed_user" data-toggle="collapse"><code>systemctl --user list-units --failed</code></a></p> - <div class="collapse show" id="collapse_systemctl_failed_user"> - <pre class="pre-scrollable" id="systemctl_failed_user"></pre> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_overview_system">+</button><a href="#collapse_overview_system" data-toggle="collapse"><code>systemctl --system status</code></a></p> + <div class="collapse" id="collapse_overview_system"> + <pre class="pre-scrollable" id="overview_system"></pre> </div> <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_status_system">+</button><a href="#collapse_systemctl_status_system" data-toggle="collapse"><code>systemctl --system status</code></a></p> - <div class="collapse" id="collapse_systemctl_status_system"> - <pre class="pre-scrollable" id="systemctl_status_system"></pre> - </div> - <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_status_user">+</button><a href="#collapse_systemctl_status_user" data-toggle="collapse"><code>systemctl --user status</code></a></p> - <div class="collapse" id="collapse_systemctl_status_user"> - <pre class="pre-scrollable" id="systemctl_status_user"></pre> - </div> - <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_timers_system">+</button><a href="#collapse_systemctl_timers_system" data-toggle="collapse"><code>systemctl --system list-timers --all</code></a> <span class="float-right"><small>refreshed every <span id="systemctl_timers_system_refresh_interval">-</span> seconds</small></span></p> - <div class="collapse" id="collapse_systemctl_timers_system"> - <pre class="pre-scrollable" id="systemctl_timers_system"></pre> - </div> - <hr> - <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_systemctl_timers_user">+</button><a href="#collapse_systemctl_timers_user" data-toggle="collapse"><code>systemctl --user list-timers --all</code></a> <span class="float-right"><small>refreshed every <span id="systemctl_timers_user_refresh_interval">-</span> seconds</small></span></p> - <div class="collapse" id="collapse_systemctl_timers_user"> - <pre class="pre-scrollable" id="systemctl_timers_user"></pre> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_timers_system">+</button><a href="#collapse_timers_system" data-toggle="collapse"><code>systemctl --system list-timers --all</code></a> <span class="float-right"><small>refreshed every <span id="timers_system_refresh_interval">-</span> seconds</small></span></p> + <div class="collapse" id="collapse_timers_system"> + <pre class="pre-scrollable" id="timers_system"></pre> </div> <hr> </div> </div> + <div id="users"> + </div> </div> <script src="js/jquery-3.3.1.min.js"></script> <script src="js/bootstrap.bundle.min.js"></script> @@ -79,32 +66,18 @@ function shutdown() { $.get('cgi-bin/poweroff.sh'); } -function refresh_top() { - $.get('cgi-bin/get.py?what=top', function(data) { - $('#top').text(JSON.parse(data)); - }); +function set_hostname(data) { + $('#hostname').text(data); + $('title').text(data); } -function refresh_timers() { - $.get('cgi-bin/get.py?what=timers', function(data) { - data = JSON.parse(data); - $('#systemctl_timers_system').text(data['system']['timers']); - $('#systemctl_timers_user').text(data['user']['timers']); - }); +function set_top(data) { + $('#top').text(data); } -function refresh_status() { - $.get('cgi-bin/get.py?what=status', function(data) { - data = JSON.parse(data); - $('#hostname').text(data['hostname']); - $('title').text(data['hostname']); - $('#top').text(data['top']); - $('#systemctl_failed_system').text(data['system']['failed']); - $('#systemctl_failed_user').text(data['user']['failed']); - $('#systemctl_status_system').text(data['system']['overview']); - $('#systemctl_status_user').text(data['user']['overview']); - $('#systemctl_timers_system').text(data['system']['timers']); - $('#systemctl_timers_user').text(data['user']['timers']); +function refresh_top() { + $.get('cgi-bin/get.sh?what=top', function(data) { + set_top(JSON.parse(data)); }); } @@ -115,14 +88,95 @@ function loop_top() { $('#top_refresh_interval').text(top_refresh_interval_seconds); } +function set_system(data) { + if ('failed' in data) { + $('#failed_system').text(data['failed']); + } + if ('overview' in data) { + $('#overview_system').text(data['overview']); + } + if ('timers' in data) { + $('#timers_system').text(data['timers']); + } +} + +var users = []; + +function add_user(name) { + if (users.includes(name)) { + return; + } + let text = ` +<div class="row"> + <div class="col"> + <h2>${name}</h2> + <hr> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_failed_user_${name}">+</button><a href="#collapse_failed_user_${name}" data-toggle="collapse"><code>systemctl --user list-units --failed</code></a></p> + <div class="collapse show" id="collapse_failed_user_${name}"> + <pre class="pre-scrollable" id="failed_user_${name}"></pre> + </div> + <hr> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_overview_user_${name}">+</button><a href="#collapse_overview_user_${name}" data-toggle="collapse"><code>systemctl --user status</code></a></p> + <div class="collapse" id="collapse_overview_user_${name}"> + <pre class="pre-scrollable" id="overview_user_${name}"></pre> + </div> + <hr> + <p><button type="button" class="btn btn-outline-primary btn-sm button-expand" data-toggle="collapse" data-target="#collapse_timers_user_${name}">+</button><a href="#collapse_timers_user_${name}" data-toggle="collapse"><code>systemctl --user list-timers --all</code></a> <span class="float-right"><small>refreshed every <span id="timers_user_refresh_interval_${name}">${timers_refresh_interval_seconds}</span> seconds</small></span></p> + <div class="collapse" id="collapse_timers_user_${name}"> + <pre class="pre-scrollable" id="timers_user_${name}"></pre> + </div> + <hr> + </div> +</div> +`; + $('#users').append(text); + users.push(name); +} + +function set_user(name, data) { + add_user(name); + if ('failed' in data) { + $('#failed_user_' + name).text(data['failed']); + } + if ('overview' in data) { + $('#overview_user_' + name).text(data['overview']); + } + if ('timers' in data) { + $('#timers_user_' + name).text(data['timers']); + } +} + +function set_users(data) { + Object.keys(data).forEach(function(name) { + set_user(name, data[name]); + }); +} + +function refresh_status() { + $.get('cgi-bin/get.sh?what=status', function(data) { + data = JSON.parse(data); + set_top(data['top']); + set_hostname(data['hostname']); + set_system(data['system']); + set_users(data['user']); + }); +} + +function refresh_timers() { + $.get('cgi-bin/get.sh?what=timers', function(data) { + data = JSON.parse(data); + set_system(data['system']); + set_users(data['user']); + }); +} + var timers_refresh_interval_seconds = 30; function loop_timers() { setInterval(function() { refresh_timers(); }, timers_refresh_interval_seconds * 1000); - $('#systemctl_timers_system_refresh_interval').text(timers_refresh_interval_seconds); - $('#systemctl_timers_user_refresh_interval').text(timers_refresh_interval_seconds); + $('#timers_system_refresh_interval').text(timers_refresh_interval_seconds); } function loop() { |