mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
privileged_daemon: Introduce a command line client for the API
- Similar call signature as actions/actions.
Tests:
- Running make install places a binary file in /usr/bin/freedombox-cmd
- Running 'freedombox-cmd upgrades get_log --no-args' works.
- Running 'freedombox-cmd upgrades activate_backports --no-args' works.
- Running 'freedombox-cmd storage usage_info --no-args' works.
- Running 'freedombox-cmd sshfs is_mounted --no-args' works.
- echo '{"args": ["USERNAME", "AUTH_USER", "AUTH_PASSWORD"], "kwargs": {}}' |
sudo freedombox-cmd users remove_user.
- Distribution upgrade from Bookworm to Trixie works.
- Snapshots are disabled and re-enabled during upgrade.
- /etc/apt/sources.list changes are completed on completion.
- If a command fails, the return code is not 0.
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
parent
143e4a00bd
commit
a580437de2
1
Makefile
1
Makefile
@ -106,6 +106,7 @@ install:
|
||||
|
||||
# Actions
|
||||
$(INSTALL) -D -t $(DESTDIR)/usr/share/plinth/actions actions/actions
|
||||
$(INSTALL) -D -t $(BIN_DIR) bin/freedombox-cmd
|
||||
|
||||
# Static web server files
|
||||
rm -rf $(STATIC_FILES_DIRECTORY)
|
||||
|
||||
6
bin/freedombox-cmd
Executable file
6
bin/freedombox-cmd
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import plinth.privileged_daemon
|
||||
|
||||
plinth.privileged_daemon.client_main()
|
||||
@ -225,7 +225,7 @@ mount -o remount /freedombox
|
||||
if [[ "{distribution}" == "stable" && ! -e $BACKPORTS_SOURCES_LIST ]]
|
||||
then
|
||||
echo "> In machine: Enable backports"
|
||||
/freedombox/actions/actions upgrades activate_backports --no-args
|
||||
/freedombox/bin/freedombox-cmd upgrades activate_backports --no-args
|
||||
fi
|
||||
|
||||
echo "> In machine: Upgrade packages"
|
||||
|
||||
@ -83,8 +83,8 @@ def privileged(func):
|
||||
def _run_privileged_method(func, module_name, action_name, args, kwargs):
|
||||
"""Execute a privileged method either using a server or sudo."""
|
||||
try:
|
||||
return _run_privileged_method_on_server(func, module_name, action_name,
|
||||
list(args), dict(kwargs))
|
||||
return run_privileged_method_on_server(func, module_name, action_name,
|
||||
list(args), dict(kwargs))
|
||||
except (
|
||||
FileNotFoundError, # When the .socket file is not present
|
||||
ConnectionRefusedError, # When is daemon not running
|
||||
@ -124,15 +124,16 @@ def _request_to_server(request: dict) -> socket.socket:
|
||||
return client_socket
|
||||
|
||||
|
||||
def _run_privileged_method_on_server(func, module_name, action_name, args,
|
||||
kwargs):
|
||||
def run_privileged_method_on_server(func, module_name, action_name, args,
|
||||
kwargs):
|
||||
"""Execute a privileged method using a server."""
|
||||
run_in_background = kwargs.pop('_run_in_background', False)
|
||||
raw_output = kwargs.pop('_raw_output', False)
|
||||
log_error = kwargs.pop('_log_error', True)
|
||||
|
||||
_log_action(func, module_name, action_name, args, kwargs,
|
||||
run_in_background, is_server=True)
|
||||
if func:
|
||||
_log_action(func, module_name, action_name, args, kwargs,
|
||||
run_in_background, is_server=True)
|
||||
|
||||
request = {
|
||||
'module': module_name,
|
||||
@ -201,9 +202,10 @@ def _wait_for_server_response(func, module_name, action_name, args, kwargs,
|
||||
return_value['exception']['name'], exception_args,
|
||||
stdout, stderr, traceback)
|
||||
|
||||
exception.get_html_message = _get_html_message
|
||||
if func:
|
||||
exception.get_html_message = _get_html_message
|
||||
|
||||
if log_error:
|
||||
if log_error and func:
|
||||
formatted_args = _format_args(func, args, kwargs)
|
||||
exception_args, stdout, stderr, traceback = _format_error(
|
||||
exception, return_value)
|
||||
|
||||
@ -117,7 +117,7 @@ def fixture_no_privileged__server():
|
||||
|
||||
Tests on using privileged daemon are not yet implemented.
|
||||
"""
|
||||
with patch('plinth.actions._run_privileged_method_on_server') as mock:
|
||||
with patch('plinth.actions.run_privileged_method_on_server') as mock:
|
||||
mock.side_effect = NotImplementedError
|
||||
yield
|
||||
|
||||
|
||||
@ -146,12 +146,12 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
|
||||
# 'reconnect', 'ServerAliveInternal' and 'ServerAliveCountMax' allow the
|
||||
# client (FreedomBox) to keep control of the SSH connection even when the
|
||||
# SSH server misbehaves. Without these options, other commands such as
|
||||
# '/usr/share/plinth/actions/actions storage usage_info --no-args', 'df',
|
||||
# '/usr/share/plinth/actions/actions sshfs is_mounted --no-args', or
|
||||
# 'mountpoint' might block indefinitely (even when manually invoked from
|
||||
# the command line). This situation has some lateral effects, causing major
|
||||
# system instability in the course of ~11 days, and leaving the system in
|
||||
# such state that the only solution is a reboot.
|
||||
# 'freedombox-cmd storage usage_info --no-args', 'df', 'freedombox-cmd
|
||||
# sshfs is_mounted --no-args', or 'mountpoint' might block indefinitely
|
||||
# (even when manually invoked from the command line). This situation has
|
||||
# some lateral effects, causing major system instability in the course of
|
||||
# ~11 days, and leaving the system in such state that the only solution is
|
||||
# a reboot.
|
||||
cmd = [
|
||||
'sshfs', remote_path, mountpoint, '-o',
|
||||
f'UserKnownHostsFile={user_known_hosts_file}', '-o',
|
||||
|
||||
@ -247,7 +247,7 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]:
|
||||
if snapshot_module.is_apt_snapshots_enabled(aug):
|
||||
logger.info('Disabling apt snapshots during dist upgrade...')
|
||||
subprocess.run([
|
||||
'/usr/share/plinth/actions/actions',
|
||||
'/usr/bin/freedombox-cmd',
|
||||
'snapshot',
|
||||
'disable_apt_snapshot',
|
||||
], input='{"args": ["yes"], "kwargs": {}}'.encode(), check=True)
|
||||
@ -260,8 +260,7 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]:
|
||||
if reenable:
|
||||
logger.info('Re-enabling apt snapshots...')
|
||||
subprocess.run([
|
||||
'/usr/share/plinth/actions/actions', 'snapshot',
|
||||
'disable_apt_snapshot'
|
||||
'/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot'
|
||||
], input='{"args": ["no"], "kwargs": {}}'.encode(), check=True)
|
||||
else:
|
||||
logger.info('Not re-enabling apt snapshots, as they were disabled '
|
||||
@ -388,8 +387,8 @@ def _trigger_on_complete():
|
||||
subprocess.run([
|
||||
'systemd-run', '--unit=freedombox-dist-upgrade-on-complete',
|
||||
'--description=Finish up upgrade to new stable Debian release',
|
||||
'/usr/share/plinth/actions/actions', 'upgrades',
|
||||
'dist_upgrade_on_complete', '--no-args'
|
||||
'/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete',
|
||||
'--no-args'
|
||||
], check=True)
|
||||
|
||||
|
||||
@ -443,7 +442,7 @@ def start_service():
|
||||
f'--property=BindPaths={temp_sources_list}:{sources_list}'
|
||||
]
|
||||
subprocess.run(['systemd-run'] + args + [
|
||||
'systemd-inhibit', '/usr/share/plinth/actions/actions', 'upgrades',
|
||||
'systemd-inhibit', '/usr/bin/freedombox-cmd', 'upgrades',
|
||||
'dist_upgrade', '--no-args'
|
||||
], check=True)
|
||||
|
||||
|
||||
@ -266,17 +266,14 @@ def test_snapshot_run_and_disable(is_supported, is_apt_snapshots_enabled, run):
|
||||
call(['snapper', 'create', '--description', 'before dist-upgrade'],
|
||||
check=True),
|
||||
call([
|
||||
'/usr/share/plinth/actions/actions', 'snapshot',
|
||||
'disable_apt_snapshot'
|
||||
'/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot'
|
||||
], input=b'{"args": ["yes"], "kwargs": {}}', check=True)
|
||||
]
|
||||
run.reset_mock()
|
||||
|
||||
assert run.call_args_list == [
|
||||
call([
|
||||
'/usr/share/plinth/actions/actions', 'snapshot',
|
||||
'disable_apt_snapshot'
|
||||
], input=b'{"args": ["no"], "kwargs": {}}', check=True)
|
||||
call(['/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot'],
|
||||
input=b'{"args": ["no"], "kwargs": {}}', check=True)
|
||||
]
|
||||
|
||||
|
||||
@ -419,8 +416,8 @@ def test_trigger_on_complete(run):
|
||||
run.assert_called_with([
|
||||
'systemd-run', '--unit=freedombox-dist-upgrade-on-complete',
|
||||
'--description=Finish up upgrade to new stable Debian release',
|
||||
'/usr/share/plinth/actions/actions', 'upgrades',
|
||||
'dist_upgrade_on_complete', '--no-args'
|
||||
'/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete',
|
||||
'--no-args'
|
||||
], check=True)
|
||||
|
||||
|
||||
|
||||
@ -56,9 +56,9 @@
|
||||
{% blocktrans trimmed %}
|
||||
Delete these accounts from command line and refresh the page to create
|
||||
an account that is usable with {{ box_name }}. On the command line run
|
||||
the command "echo '{"args": ["USERNAME", "PASSWORD"], "kwargs": {}}' |
|
||||
sudo /usr/share/plinth/actions/actions users remove_user". If an
|
||||
account is already usable with {{ box_name }}, skip this step.
|
||||
the command "echo '{"args": ["USERNAME", "AUTH_USER", "AUTH_PASSWORD"],
|
||||
"kwargs": {}}' | sudo freedombox-cmd users remove_user". If an account
|
||||
is already usable with {{ box_name }}, skip this step.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""The main method for a daemon that runs privileged methods."""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
@ -32,6 +33,9 @@ idle_shutdown_time: int | None = 5 * 60 # 5 minutes
|
||||
|
||||
freedombox_develop = False
|
||||
|
||||
EXIT_SYNTAX = 10
|
||||
EXIT_PERM = 20
|
||||
|
||||
|
||||
class RequestHandler(socketserver.StreamRequestHandler):
|
||||
"""Handle a single streaming request.
|
||||
@ -210,6 +214,46 @@ class Server(socketserver.ThreadingUnixStreamServer):
|
||||
return True
|
||||
|
||||
|
||||
def client_main() -> None:
|
||||
"""Parse arguments for the client for privileged daemon."""
|
||||
log.action_init()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('module', help='Module to trigger action in')
|
||||
parser.add_argument('action', help='Action to trigger in module')
|
||||
parser.add_argument('--no-args', default=False, action='store_true',
|
||||
help='Do not read arguments from stdin')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
try:
|
||||
arguments: dict = {'args': [], 'kwargs': {}}
|
||||
if not args.no_args:
|
||||
input_ = sys.stdin.read()
|
||||
if input_:
|
||||
arguments = json.loads(input_)
|
||||
except json.JSONDecodeError as exception:
|
||||
raise SyntaxError('Arguments on stdin not JSON.') from exception
|
||||
|
||||
return_value = actions.run_privileged_method(None, args.module,
|
||||
args.action,
|
||||
arguments['args'],
|
||||
arguments['kwargs'])
|
||||
print(json.dumps(return_value, cls=actions.JSONEncoder))
|
||||
except PermissionError as exception:
|
||||
logger.error(exception.args[0])
|
||||
sys.exit(EXIT_PERM)
|
||||
except SyntaxError as exception:
|
||||
logger.error(exception.args[0])
|
||||
sys.exit(EXIT_SYNTAX)
|
||||
except TypeError as exception:
|
||||
logger.error(exception.args[0])
|
||||
sys.exit(EXIT_SYNTAX)
|
||||
except Exception as exception:
|
||||
logger.exception(exception)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the server, listen on socket, and serve forever."""
|
||||
global freedombox_develop, idle_shutdown_time
|
||||
|
||||
@ -104,6 +104,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project.scripts]
|
||||
plinth = "plinth.__main__:main"
|
||||
freedombox-privileged = "plinth.privileged_daemon:main"
|
||||
freedombox-cmd = "plinth.privileged_daemon:client_main"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "plinth.__version__"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user