FreedomBox/plinth/container.py
Sunil Mohan Adapa 0661d7da7c
app: Add ability to retrieve logs from all systemd units of an app
Tests:

- Upto 200 lines are shown in the logs. The description and unit name of the app
is correct.

- Apps without systemd units don't have 'View Logs' menu item.

- Nextcloud container logs are shown.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
2025-09-18 16:35:23 +03:00

147 lines
5.5 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Component to manage a container using podman."""
import contextlib
from django.utils.translation import gettext_noop
from plinth import app, log, privileged
from plinth.daemon import diagnose_port_listening
from plinth.diagnostic_check import (DiagnosticCheck,
DiagnosticCheckParameters, Result)
class Container(app.LeaderComponent, log.LogEmitter):
"""Component to manage a podman container."""
def __init__(self, component_id: str, name: str, image_name: str,
volume_name: str, volume_path: str,
volumes: dict[str, str] | None = None,
env: dict[str, str] | None = None,
binds_to: list[str] | None = None,
devices: dict[str, str] | None = None,
listen_ports: list[tuple[int, str]] | None = None):
"""Initialize a container component.
`name` is a string which is the name of the container to create and
manage. A systemd service unit with the same name is also created.
`image_name` is a string that represents the repository location from
which the container images must be pull from.
`volume_name` is a string with name of the storage volume to create for
the container to use.
`volume_path` is a string path on the host machine where the volume
files for the container is stored.
`volumes` is a dictionary mapping each string path on the host to a
string path inside the container. These are bind mounts made available
inside the container.
`env` is a dictionary of string key to string values that set the
environment variables for the processes inside the container to run in.
`binds_to` is a list of systemd service units that the container's own
systemd service unit will add BindsTo= and After= dependencies on.
`devices` is a list of strings with device paths that will be made
available inside the container. If any of the devices don't exist on
the host, they will not be added.
`listen_ports` is a list of tuples containing port number and 'tcp4' or
'tcp6' network types on which this container is expected to listen on
after starting the container. This information is used to run
diagnostic checks on the container.
"""
super().__init__(component_id)
self.name = name
self.image_name = image_name
self.volume_name = volume_name
self.volume_path = volume_path
self.volumes = volumes
self.env = env
self.binds_to = binds_to
self.devices = devices
self.listen_ports = listen_ports or []
# For logs
self.unit = self.name
def is_enabled(self):
"""Return if the container is enabled."""
return privileged.container_is_enabled(self.name)
def enable(self):
"""Run operations to enable and run the container."""
super().enable()
privileged.container_enable(self.name)
def disable(self):
"""Run operations to disable and stop the container."""
super().disable()
privileged.container_disable(self.name)
def is_running(self):
"""Return whether the container service is running."""
return privileged.is_running(self.name)
@contextlib.contextmanager
def ensure_running(self):
"""Ensure a service is running and return to previous state."""
from plinth.privileged import service as service_privileged
starting_state = self.is_running()
if not starting_state:
service_privileged.enable(self.name)
try:
yield starting_state
finally:
if not starting_state:
service_privileged.disable(self.name)
def setup(self, old_version: int):
"""Bring up and run the container."""
# Determine whether app should be disabled after setup
should_disable = old_version and not self.is_enabled()
privileged.container_setup(self.name, self.image_name,
self.volume_name, self.volume_path,
self.volumes, self.env, self.binds_to,
self.devices)
if should_disable:
self.disable()
def uninstall(self):
"""Remove the container."""
privileged.container_uninstall(self.name, self.image_name,
self.volume_name, self.volume_path)
def diagnose(self) -> list[DiagnosticCheck]:
"""Check if the container is running..
See :py:meth:`plinth.app.Component.diagnose`.
"""
results = []
results.append(self._diagnose_unit_is_running())
for port in self.listen_ports:
results.append(
diagnose_port_listening(port[0], port[1], None,
self.component_id))
return results
def _diagnose_unit_is_running(self) -> DiagnosticCheck:
"""Check if a daemon is running."""
check_id = f'container-running-{self.name}'
result = Result.PASSED if self.is_running() else Result.FAILED
description = gettext_noop('Container {container_name} is running')
parameters: DiagnosticCheckParameters = {
'container_name': str(self.name)
}
return DiagnosticCheck(check_id, description, result, parameters,
self.component_id)