freedombox Debian release 21.15

-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmGupTgACgkQd8DHXntl
 CAhIxQ/9ErnBPnvyb/aNJVmxYi5xXJ521xMI2KfdmWipomayF8+31/P2DDz4rBEf
 Q1/f6xgWqtxhZho+XH0jtist99/AQKTCqc6zNv2tHfV2XwZqWaXANR4BQW5M2XfT
 3V5VEVhavH8obJmm9y+50g3NZnDx70GFnX5r9p4rS/6n5Iz5ZWBwuUq5BTtpp+1a
 GkwrFPlt4zYPHWMV4ZGDX5G6Xzr1Zwf5gJGBmLCd6a+wloV+3sxOcBOahNFqp+hK
 LOv1VTVlrJaa+5zcJ+DCMuMTiLtKp1CYxCKIX4I3A2DPkryYqFRIJAqLORhurz9Y
 q0iF3WnCYG1Srfb1MRCjRNfiYkm6TJF0v+5v33r/heU3nkM3jstRkLYdxE1IkUIy
 IPtPbpxxl55Ktc1AMvpV2y9yV4asRydBzID9zj8KoBD4lQ0VQJdLZNu0GYDJSEn5
 HJ8uFrUzL+2zwlRrvGHBndrmh+3N0j4R8sKnYL2xqMg1p1oDneufvAS5bQ1DknKb
 uHWNiI7Rvtz/btVPAhDdQ07DLt2TdlfWC5dlzYxRvmafL4Eba0/qEeP2FThdRt+e
 +0HmMPo9IL1wobXFuqkj5uHNJtPsv1PCkPYc2oEFzX8YTiooi7KqmBTQJssr6Aat
 Vx8m7veZByLby+2zFQ5ccwvMy/RlJX9ck83pFlqu+/G6/HfFMpk=
 =SnJ1
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmGzN6wACgkQd8DHXntl
 CAgW3xAAyOddaw/OB2oNeol/BViMXqTUG16ZzOXOoz4h+tu5zIdwgEx+0FMZAsBd
 +vhND6RmqRmtLEsi9791Xucz9rFpT3buK7vu71XpbYolg7nHFGiIiPK57gix3/z7
 DDeW/LUUxFOgMa54tHmgbmxfutl+ITxZFarEHBBlxOUKfozIaxfT0enexln6333+
 jiqin1LHqcjugLGVvP2rt4eOXlG2bQS2Dj5g2APLFn9KVFfxBS1pzpQvJX32ku2g
 LT40b33wsyypWoSt59bbROU185fKt76nzqCKSl1pXSMGQjypOfj7FxlbRgFGVSsY
 +Skrplkgj/uquyiX+sJK8Xf3V+rOnPW5vOictb8w/2/IiwOFHjshTFz7D1tnPAL6
 I4cbmGrnygDBhU5xSuI12YNshK3BR8n8b7qYE9YD2jExI0wK96v18R183eKWyM0K
 B4CkKiuEv0F2djeBXyzquAQk1WCrwzlggYb5RYqEBQAmbp0bphVE8kthOxAgwjY3
 I23jhzzruj297KpCfUGf/BG6sy2aQS23HAVuzeLWZXn6gNl0isWqgzr0psUkT9D9
 lVu6dzKlELAtdG6nXDCOIg1H6V8DPEfzjHaKsIeBItneS5fvKhBeXXesa3NLahhR
 iYl9OTgV7xz1O3q8r3i98ty95pyX8Z4JaNLYhbiID520Ek7gFvw=
 =ucZm
 -----END PGP SIGNATURE-----

Merge tag 'v21.15' into debian/bullseye-backports

freedombox Debian release 21.15

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
James Valleroy 2021-12-10 06:19:06 -05:00
commit 7bed7bfea3
157 changed files with 16935 additions and 16479 deletions

View File

@ -39,14 +39,14 @@ def subcommand_setup(arguments):
Path(ZONES_DIR).mkdir(exist_ok=True, parents=True)
action_utils.service_restart('bind9')
action_utils.service_restart('named')
def subcommand_configure(arguments):
"""Configure BIND."""
set_forwarders(arguments.forwarders)
set_dnssec(arguments.dnssec)
action_utils.service_restart('bind9')
action_utils.service_restart('named')
def main():

View File

@ -8,6 +8,7 @@ import argparse
import filecmp
import glob
import importlib
import inspect
import json
import os
import pathlib
@ -18,8 +19,11 @@ import sys
import configobj
from plinth import action_utils, cfg
from plinth import action_utils
from plinth import app as app_module
from plinth import cfg
from plinth.modules import letsencrypt as le
from plinth.modules.letsencrypt.components import LetsEncrypt
TEST_MODE = False
LE_DIRECTORY = '/etc/letsencrypt/'
@ -372,6 +376,14 @@ def _assert_source_directory(path):
or str(path).startswith(ETC_SSL_DIRECTORY))
def _get_managed_path(path):
"""Return the managed path given a certificate path."""
if '{domain}' in path:
return pathlib.Path(path.partition('{domain}')[0])
return pathlib.Path(path).parent
def _assert_managed_path(module, path):
"""Check that path is in fact managed by module."""
cfg.read()
@ -379,7 +391,24 @@ def _assert_managed_path(module, path):
module_path = module_file.read_text().strip()
module = importlib.import_module(module_path)
assert set(path.parents).intersection(set(module.managed_paths))
module_classes = inspect.getmembers(module, inspect.isclass)
app_classes = [
cls[1] for cls in module_classes if issubclass(cls[1], app_module.App)
]
managed_paths = []
for cls in app_classes:
app = cls()
components = app.get_components_of_type(LetsEncrypt)
for component in components:
if component.private_key_path:
managed_paths.append(
_get_managed_path(component.private_key_path))
if component.certificate_path:
managed_paths.append(
_get_managed_path(component.certificate_path))
assert set(path.parents).intersection(set(managed_paths))
def subcommand_run_pre_hooks(_):

View File

@ -5,6 +5,7 @@ Wrapper to handle package installation with apt-get.
"""
import argparse
import inspect
import json
import logging
import os
@ -17,9 +18,11 @@ import apt.cache
import apt_inst
import apt_pkg
from plinth import app as app_module
from plinth import cfg
from plinth.action_utils import (apt_hold_freedombox, is_package_manager_busy,
run_apt_command)
from plinth.package import Packages
logger = logging.getLogger(__name__)
@ -119,8 +122,19 @@ def _assert_managed_packages(module, packages):
module_path = file_handle.read().strip()
module = import_module(module_path)
module_classes = inspect.getmembers(module, inspect.isclass)
app_classes = [
cls[1] for cls in module_classes if issubclass(cls[1], app_module.App)
]
managed_packages = []
for cls in app_classes:
app = cls()
components = app.get_components_of_type(Packages)
for component in components:
managed_packages += component.packages
for package in packages:
assert package in module.managed_packages
assert package in managed_packages
def subcommand_is_package_manager_busy(_):

View File

@ -5,12 +5,12 @@ Wrapper to list and handle system services
"""
import argparse
import json
import os
import subprocess
from importlib import import_module
from plinth import action_utils, cfg
from plinth import action_utils
from plinth import app as app_module
from plinth import cfg, module_loader
from plinth.daemon import Daemon, RelatedDaemon
cfg.read()
module_config_path = os.path.join(cfg.config_dir, 'modules-enabled')
@ -39,8 +39,6 @@ def parse_arguments():
add_service_action(subparsers, 'mask', 'unmask a service')
add_service_action(subparsers, 'unmask', 'unmask a service')
subparsers.add_parser('list', help='List of running system services')
subparsers.required = True
return parser.parse_args()
@ -89,64 +87,21 @@ def subcommand_is_running(arguments):
print(action_utils.service_is_running(arguments.service))
def subcommand_list(_):
"""Get list of plinth-managed services with their status.
Status may be either running or not.
"""
managed_services = _get_managed_services()
services = dict.fromkeys(managed_services, {'running': False})
output = subprocess.check_output(['systemctl', 'list-units'])
for line in output.decode().strip().split('\n'):
if line.startswith('UNIT'):
continue
# Stop parsing on empty line after the service list
if not len(line):
break
try:
unit, load, active, sub = line.split()[:4]
except ValueError:
continue
suffix = '.service'
if unit.endswith(suffix):
name = unit[:-len(suffix)]
if name in services:
services[name] = {'running': (sub == 'running')}
print(json.dumps(services))
def _get_managed_services_of_module(modulepath):
"""Import a module and return content of its 'managed_services' variable"""
try:
module = import_module(modulepath)
except ImportError:
return []
else:
return getattr(module, 'managed_services', [])
def _get_managed_services():
"""
Get a set of all services managed by FreedomBox.
This collects all service-names inside the 'managed_services' variable of
modules inside 'module_config_path'
"""
"""Get a set of all services managed by FreedomBox."""
services = set()
for filename in os.listdir(module_config_path):
# Omit hidden files
if filename.startswith('.'):
continue
module_loader.load_modules()
app_module.apps_init()
for app in app_module.App.list():
components = app.get_components_of_type(Daemon)
for component in components:
services.add(component.unit)
if component.alias:
services.add(component.alias)
filepath = os.path.join(module_config_path, filename)
if os.path.isfile(filepath):
with open(filepath, 'r') as f:
modulepath = f.read().strip()
services.update(_get_managed_services_of_module(modulepath))
components = app.get_components_of_type(RelatedDaemon)
for component in components:
services.add(component.unit)
return services

View File

@ -14,7 +14,7 @@ import sys
from shutil import move
from plinth import action_utils
from plinth.modules import shadowsocks
from plinth.modules.shadowsocks import ShadowsocksApp
SHADOWSOCKS_CONFIG_SYMLINK = '/etc/shadowsocks-libev/freedombox.json'
SHADOWSOCKS_CONFIG_ACTUAL = \
@ -76,8 +76,8 @@ def subcommand_setup(_):
if not wrong_state_dir.is_symlink() and wrong_state_dir.is_dir():
wrong_state_dir.rmdir()
if action_utils.service_is_enabled(shadowsocks.managed_services[0]):
action_utils.service_restart(shadowsocks.managed_services[0])
if action_utils.service_is_enabled(ShadowsocksApp.DAEMON):
action_utils.service_restart(ShadowsocksApp.DAEMON)
def subcommand_get_config(_):
@ -110,8 +110,8 @@ def subcommand_merge_config(_):
# Don't try_restart because initial configuration may not be valid so
# shadowsocks will not be running even when enabled.
if action_utils.service_is_enabled(shadowsocks.managed_services[0]):
action_utils.service_restart(shadowsocks.managed_services[0])
if action_utils.service_is_enabled(ShadowsocksApp.DAEMON):
action_utils.service_restart(ShadowsocksApp.DAEMON)
def main():

66
debian/changelog vendored
View File

@ -1,3 +1,69 @@
freedombox (21.15) unstable; urgency=medium
[ trendspotter ]
* Translated using Weblate (Czech)
[ James Valleroy ]
* shaarli: Enable app
* tests: Add 'domain' mark for apps that add/remove domains
* locale: Update translation strings
* doc: Fetch latest manual
[ Petter Reinholdtsen ]
* Translated using Weblate (Norwegian Bokmål)
[ Sunil Mohan Adapa ]
* dynamicdns: Update URLs to the new dynamic DNS server
* firewall: Allow configuration upgrade to version 1.0.x
* *: Drop unused manual_page at module level
* app: Introduce API to setup an app
* package: Add parameter to specify skipping package recommendations
* package: Implement installing packages in the component
* actions: Get list of packages from Packages components
* security: Get the list of packages from Packages component
* *: Drop use of managed_packages and rely on Packages component
* doc/dev: Update documentation to not refer to managed_packages
* actions/service: Drop unused list action
* bind: Drop alias handling unnecessary in >= Bullseye
* security: Drop use of managed_services in security report
* daemon: Add new component to hold information about related daemons
* actions/service: Drop use of managed_services for Daemon component
* *: Drop use of managed_services, rely on Daemon component
* doc/dev: Remove mention of managed_services
* actions/letsencrypt: Drop use of managed_paths and use LE component
* *: Drop use of unnecessary managed_paths
* doc/dev: Drop discussion on managed_paths
* package: Introduce component API for package conflicts
* *: Drop module level package_conflicts and use component API
* packages: Move checking for unavailable packages to component
* app: Introduce API for managing setup state of the app
* doc/dev: Remove outdated reference to init() at module level
* *: Use the App's state management API
* setup: Drop unused API for app's state management
* *: Drop use of module level is_essential flag
* *: Drop use of module level version
* middleware, views: Reduce use of setup_helper
* web_server: Drop use of loaded_modules and use App.list
* first_boot: Drop use of loaded_modules and use App.list
* security: Drop use of loaded_modules and use App.list
* main: List apps instead of modules
* setup: Run setup on apps instead of modules
* setup: List dependencies for apps instead of modules
* setup: Use apps instead of modules to determine running first setup
* setup: Work on apps instead of modules for force upgrade
* module_loader, app: Move app init to app module
* *: Drop module level depends declaration
* doc/dev: Drop reference to module level depends declaration
* forms: Fix regression with TLS domain form in quassel and tt-rss
* email_server: Simplify domain configuration form
* email_server: Merge domain configuration with app view
* letsencrypt: On domain removal, don't revoke certificate, keep it
[ Johannes Keyser ]
* Translated using Weblate (German)
-- James Valleroy <jvalleroy@mailbox.org> Mon, 06 Dec 2021 18:51:28 -0500
freedombox (21.14.1~bpo11+1) bullseye-backports; urgency=medium
* Rebuild for bullseye-backports.

View File

@ -7,7 +7,7 @@
# - Ability to create and initialize database
# - Module inititailzation for essential modules
#
Test-Command: plinth --list-modules 2> /dev/null
Test-Command: plinth --list-apps 2> /dev/null
Restrictions: needs-root
#

View File

@ -1,56 +0,0 @@
.. SPDX-License-Identifier: CC-BY-SA-4.0
App Module
----------
These methods are optionally provided by the module in which an app is
implemented and FreedomBox calls/uses them if they are present.
<app-module>.depends
^^^^^^^^^^^^^^^^^^^^
Optional. This module property must contain a list of all apps that this
application depends on. The application is specified as string which is the
final part of the full module load path. For example, ``names``. Dependencies
are part of the :class:`~plinth.app.Info` component. Need for this attribute at
the module level will be removed in the future.
<app-module>.is_essential
^^^^^^^^^^^^^^^^^^^^^^^^^
Optional. If an app must be installed and configured by FreedomBox without user
intervention, this attribute must be set to True. This attribute is part of the
:class:`~plinth.app.Info` component. Need for this attribute at the module level
will be removed in the future.
<app-module>.version
^^^^^^^^^^^^^^^^^^^^
Required. Version number of an app. Increasing the version number of an app
triggers the setup() logic allowing the app to run upgrade scripts. This
attribute is part of the :class:`~plinth.app.Info` component. Need for this
attribute at the module level will be removed in the future.
<app-module>.managed_packages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Optional. This must contain the list of all packages that this app deals with.
This is mostly needed to enforce better security. This information may be moved
to a separate component in the future.
<app-module>.managed_services
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Optional. This must contain the list of all services that this app deals with.
This is mostly needed to enforce better security. This information is part of
the :class:`~plinth.daemon.Daemon` component. Need for this attribute at the
module level will be removed in the future.
<app-module>.managed_paths
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Optional. This must contain the list of all file system paths that this app
deals with. This is mostly used by the
:class:`~plinth.modules.letsencrypt.components.LetsEncrypt` component to enforce
better security. This requirement may be removed in the future.

View File

@ -14,7 +14,6 @@ and are updated when the API is updated.
app
components/index
app_module
actions
action_utils
views

View File

@ -92,15 +92,13 @@ our app's class.
from plinth.daemon import Daemon
managed_services = ['transmission-daemon']
class TransmissionApp(app_module.App):
...
def __init__(self):
...
daemon = Daemon('daemon-transmission', managed_services[0],
daemon = Daemon('daemon-transmission', 'transmission-daemon',
listen_ports=[(9091, 'tcp4')])
self.add(daemon)
@ -126,7 +124,6 @@ Debian packages to be installed.
from plinth.package import Packages
managed_packages = ['transmission-daemon']
class TransmissionApp(app_module.App):
...
@ -134,7 +131,7 @@ Debian packages to be installed.
def __init__(self):
...
packages = Packages('packages-transmission', managed_packages)
packages = Packages('packages-transmission', ['transmission-daemon'])
self.add(packages)
The first argument uniquely identifies this instance of the `Packages`

View File

@ -16,11 +16,9 @@ installation:
.. code-block:: python3
:caption: ``__init__.py``
managed_packages = ['transmission-daemon']
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
new_configuration = {
'rpc-whitelist-enabled': False,

View File

@ -88,20 +88,4 @@ class later.
super().__init__()
As soon as FreedomBox Service (Plinth) starts, it will load all the enabled
modules. After this, it gives a chance to each of the modules to initialize
itself by calling the ``init()`` method if there is such a method available as
``<module>.init()``. The app class must be instantiated here.
.. code-block:: python3
:caption: ``__init__.py``
app = None
def init():
"""Initialize the Transmission module."""
global app
app = TransmissionApp()
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
app.set_enabled(True)
modules and create app instances for App classes in the module.

View File

@ -26,11 +26,11 @@ On the other hand, the GnuDIP protocol will only transport a salted MD5 value of
=== Using the GnuDIP protocol ===
1. Register an account with any Dynamic DNS service provider. A free service provided by the !FreedomBox community is available at https://gnudip.datasystems24.net .
1. Register an account with any Dynamic DNS service provider. A free service provided by the !FreedomBox community is available at https://ddns.freedombox.org .
1. In !FreedomBox UI, enable the Dynamic DNS Service.
1. Select ''GnuDIP'' as ''Service type'', enter your Dynamic DNS service provider address (for example, gnudip.datasystems24.net) into ''GnuDIP Server Address'' field.
1. Select ''GnuDIP'' as ''Service type'', enter your Dynamic DNS service provider address (for example, ddns.freedombox.org) into ''GnuDIP Server Address'' field.
{{attachment:DynamicDNS-Settings.png|Dynamic DNS Settings|width=800}}
@ -62,7 +62,7 @@ This feature is implemented because the most popular Dynamic DNS providers are u
=== Recap: How to create a DNS name with GnuDIP ===
/* to delete or to replace the old text */
1. Access to [[https://gnudip.datasystems24.net|GnuIP login page]] (answer Yes to all pop ups)
1. Access to [[https://ddns.freedombox.org|GnuIP login page]] (answer Yes to all pop ups)
1. Click on "Self Register"
1. Fill the registration form (Username and domain will form the public IP address [username.domain])
1. Take note of the username/hostname and password that will be used on the !FreedomBox app.
@ -72,11 +72,11 @@ This feature is implemented because the most popular Dynamic DNS providers are u
1. Click on "Set Up" in the top menu.
1. Activate Dynamic DNS
1. Choose GnuDIP service.
1. Add server address (gnudip.datasystems24.net)
1. Add server address (ddns.freedombox.org)
1. Add your fresh domain name (username.domain, ie [username].freedombox.rocks)
1. Add your fresh username (the one used in your new IP address) and password
1. Add your GnuDIP password
1. Fill the option with http://myip.datasystems24.de (try this url in your browser, you will figure out immediately)
1. Fill the option with https://ddns.freedombox.org/ip/ (try this url in your browser, you will figure out immediately)
## END_INCLUDE

View File

@ -10,6 +10,66 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 21.15 (2021-12-06) ==
=== Highlights ===
* dynamicdns: Update URLs to the new dynamic DNS server
* firewall: Allow configuration upgrade to version 1.0.x
* shaarli: Enable app (only available in testing and unstable)
=== Other Changes ===
* *: Drop module level depends declaration
* *: Drop module level package_conflicts and use component API
* *: Drop unused manual_page at module level
* *: Drop use of managed_packages and rely on Packages component
* *: Drop use of managed_services, rely on Daemon component
* *: Drop use of module level is_essential flag
* *: Drop use of module level version
* *: Drop use of unnecessary managed_paths
* *: Use the App's state management API
* actions/letsencrypt: Drop use of managed_paths and use LE component
* actions/service: Drop unused list action
* actions/service: Drop use of managed_services for Daemon component
* actions: Get list of packages from Packages components
* app: Introduce API for managing setup state of the app
* app: Introduce API to setup an app
* bind: Drop alias handling unnecessary in >= Bullseye
* daemon: Add new component to hold information about related daemons
* doc/dev: Drop discussion on managed_paths
* doc/dev: Drop reference to module level depends declaration
* doc/dev: Remove mention of managed_services
* doc/dev: Remove outdated reference to init() at module level
* doc/dev: Update documentation to not refer to managed_packages
* email_server: Merge domain configuration with app view
* email_server: Simplify domain configuration form
* first_boot: Drop use of loaded_modules and use App.list
* forms: Fix regression with TLS domain form in quassel and tt-rss
* letsencrypt: On domain removal, don't revoke certificate, keep it
* locale: Update translations for Czech, German, Norwegian Bokmål
* main: List apps instead of modules
* middleware, views: Reduce use of setup_helper
* module_loader, app: Move app init to app module
* package: Add parameter to specify skipping package recommendations
* package: Implement installing packages in the component
* package: Introduce component API for package conflicts
* packages: Move checking for unavailable packages to component
* security: Drop use of loaded_modules and use App.list
* security: Drop use of managed_services in security report
* security: Get the list of packages from Packages component
* setup: Drop unused API for app's state management
* setup: List dependencies for apps instead of modules
* setup: Run setup on apps instead of modules
* setup: Use apps instead of modules to determine running first setup
* setup: Work on apps instead of modules for force upgrade
* tests: Add 'domain' mark for apps that add/remove domains
* web_server: Drop use of loaded_modules and use App.list
== FreedomBox 21.14.1 (2021-11-24) ==
* config: Add packages component to a re-add zram-tools dependency
== FreedomBox 21.14 (2021-11-22) ==
=== Highlights ===

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -26,11 +26,11 @@ Por otra parte el protocolo GnuDIP solo transportará un valor MD5 salpimentado
=== Emplear el protocolo GnuDIP ===
1. Registra una cuenta en cualquier proveedor de servicio de DNS Dinamico. Hay un servicio gratuito provisto por la comunidad !FreedomBox disponible en https://gnudip.datasystems24.net .
1. Registra una cuenta en cualquier proveedor de servicio de DNS Dinamico. Hay un servicio gratuito provisto por la comunidad !FreedomBox disponible en https://ddns.freedombox.org .
1. Habilita el Servicio de DNS Dinamico en el interfaz de usuario de !FreedomBox.
1. Selecciona ''GnuDIP'' como ''tipo de servicio'', introduce la dirección de tu proveedor de servicio de DNS Dinamico (por ejemplo, gnudip.datasystems24.net) en el campo ''Dirección del servidor GnuDIP''.
1. Selecciona ''GnuDIP'' como ''tipo de servicio'', introduce la dirección de tu proveedor de servicio de DNS Dinamico (por ejemplo, ddns.freedombox.org) en el campo ''Dirección del servidor GnuDIP''.
{{attachment:DynamicDNS-Settings.png|Dynamic DNS Settings|width=800}}
@ -62,7 +62,7 @@ Se implementa esta funcionalidad porque los proveedores de servicio de DNS Dinam
=== Recap: How to create a DNS name with GnuDIP ===
/* to delete or to replace the old text */
1. Access to [[https://gnudip.datasystems24.net|GnuIP login page]] (answer Yes to all pop ups)
1. Access to [[https://ddns.freedombox.org|GnuIP login page]] (answer Yes to all pop ups)
1. Click on "Self Register"
1. Fill the registration form (Username and domain will form the public IP address [username.domain])
1. Take note of the username/hostname and password that will be used on the !FreedomBox app.
@ -72,11 +72,11 @@ Se implementa esta funcionalidad porque los proveedores de servicio de DNS Dinam
1. Click on "Set Up" in the top menu.
1. Activate Dynamic DNS
1. Choose GnuDIP service.
1. Add server address (gnudip.datasystems24.net)
1. Add server address (ddns.freedombox.org)
1. Add your fresh domain name (username.domain, ie [username].freedombox.rocks)
1. Add your fresh username (the one used in your new IP address) and password
1. Add your GnuDIP password
1. Fill the option with http://myip.datasystems24.de (try this url in your browser, you will figure out immediately)
1. Fill the option with https://ddns.freedombox.org/ip/ (try this url in your browser, you will figure out immediately)
## END_INCLUDE

View File

@ -10,6 +10,66 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
The following are the release notes for each !FreedomBox version.
== FreedomBox 21.15 (2021-12-06) ==
=== Highlights ===
* dynamicdns: Update URLs to the new dynamic DNS server
* firewall: Allow configuration upgrade to version 1.0.x
* shaarli: Enable app (only available in testing and unstable)
=== Other Changes ===
* *: Drop module level depends declaration
* *: Drop module level package_conflicts and use component API
* *: Drop unused manual_page at module level
* *: Drop use of managed_packages and rely on Packages component
* *: Drop use of managed_services, rely on Daemon component
* *: Drop use of module level is_essential flag
* *: Drop use of module level version
* *: Drop use of unnecessary managed_paths
* *: Use the App's state management API
* actions/letsencrypt: Drop use of managed_paths and use LE component
* actions/service: Drop unused list action
* actions/service: Drop use of managed_services for Daemon component
* actions: Get list of packages from Packages components
* app: Introduce API for managing setup state of the app
* app: Introduce API to setup an app
* bind: Drop alias handling unnecessary in >= Bullseye
* daemon: Add new component to hold information about related daemons
* doc/dev: Drop discussion on managed_paths
* doc/dev: Drop reference to module level depends declaration
* doc/dev: Remove mention of managed_services
* doc/dev: Remove outdated reference to init() at module level
* doc/dev: Update documentation to not refer to managed_packages
* email_server: Merge domain configuration with app view
* email_server: Simplify domain configuration form
* first_boot: Drop use of loaded_modules and use App.list
* forms: Fix regression with TLS domain form in quassel and tt-rss
* letsencrypt: On domain removal, don't revoke certificate, keep it
* locale: Update translations for Czech, German, Norwegian Bokmål
* main: List apps instead of modules
* middleware, views: Reduce use of setup_helper
* module_loader, app: Move app init to app module
* package: Add parameter to specify skipping package recommendations
* package: Implement installing packages in the component
* package: Introduce component API for package conflicts
* packages: Move checking for unavailable packages to component
* security: Drop use of loaded_modules and use App.list
* security: Drop use of managed_services in security report
* security: Get the list of packages from Packages component
* setup: Drop unused API for app's state management
* setup: List dependencies for apps instead of modules
* setup: Run setup on apps instead of modules
* setup: Use apps instead of modules to determine running first setup
* setup: Work on apps instead of modules for force upgrade
* tests: Add 'domain' mark for apps that add/remove domains
* web_server: Drop use of loaded_modules and use App.list
== FreedomBox 21.14.1 (2021-11-24) ==
* config: Add packages component to a re-add zram-tools dependency
== FreedomBox 21.14 (2021-11-22) ==
=== Highlights ===

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -3,4 +3,4 @@
Package init file.
"""
__version__ = '21.14.1'
__version__ = '21.15'

View File

@ -5,8 +5,10 @@ import argparse
import logging
import sys
from . import (__version__, cfg, frontpage, glib, log, menu, module_loader,
setup, utils, web_framework, web_server)
from . import __version__
from . import app as app_module
from . import (cfg, frontpage, glib, log, menu, module_loader, setup, utils,
web_framework, web_server)
if utils.is_axes_old():
import axes
@ -29,37 +31,36 @@ def parse_arguments():
'--develop', action='store_true', default=None,
help=('run Plinth *insecurely* from current folder; '
'enable auto-reloading and debugging options'))
parser.add_argument(
'--setup', default=False, nargs='*',
help='run setup tasks on all essential modules and exit')
parser.add_argument('--setup', default=False, nargs='*',
help='run setup tasks on all essential apps and exit')
parser.add_argument(
'--setup-no-install', default=False, nargs='*',
help='run setup tasks without installing packages and exit')
parser.add_argument('--list-dependencies', default=False, nargs='*',
help='list package dependencies for essential modules')
parser.add_argument('--list-modules', default=False, nargs='*',
help='list modules')
parser.add_argument('--list-apps', default=False, nargs='*',
help='list apps')
return parser.parse_args()
def run_setup_and_exit(module_list, allow_install=True):
"""Run setup on all essential modules and exit."""
def run_setup_and_exit(app_ids, allow_install=True):
"""Run setup on all essential apps and exit."""
error_code = 0
try:
setup.run_setup_on_modules(module_list, allow_install)
setup.run_setup_on_apps(app_ids, allow_install)
except Exception:
error_code = 1
sys.exit(error_code)
def list_dependencies(module_list):
"""List dependencies for all essential modules and exit."""
def list_dependencies(app_ids):
"""List dependencies for all essential apps and exit."""
error_code = 0
try:
if module_list:
setup.list_dependencies(module_list=module_list)
if app_ids:
setup.list_dependencies(app_ids=app_ids)
else:
setup.list_dependencies(essential=True)
except Exception as exception:
@ -69,15 +70,18 @@ def list_dependencies(module_list):
sys.exit(error_code)
def list_modules(modules_type):
"""List all/essential/optional modules and exit."""
for module_name, module in module_loader.loaded_modules.items():
module_is_essential = getattr(module, 'is_essential', False)
if 'essential' in modules_type and not module_is_essential:
def list_apps(apps_type):
"""List all/essential/optional apps and exit."""
for app in app_module.App.list():
is_essential = app.info.is_essential
if 'essential' in apps_type and not is_essential:
continue
elif 'optional' in modules_type and module_is_essential:
if 'optional' in apps_type and is_essential:
continue
print('{module_name}'.format(module_name=module_name))
print(f'{app.app_id}')
sys.exit()
@ -109,9 +113,15 @@ def main():
if arguments.list_dependencies is not False:
log.default_level = 'ERROR'
module_loader.load_modules()
module_loader.apps_init()
app_module.apps_init()
list_dependencies(arguments.list_dependencies)
if arguments.list_apps is not False:
log.default_level = 'ERROR'
module_loader.load_modules()
app_module.apps_init()
list_apps(arguments.list_apps)
log.init()
web_framework.init()
@ -127,8 +137,8 @@ def main():
menu.init()
module_loader.load_modules()
module_loader.apps_init()
module_loader.apps_post_init()
app_module.apps_init()
app_module.apps_post_init()
frontpage.add_custom_shortcuts()
if arguments.setup is not False:
@ -137,9 +147,6 @@ def main():
if arguments.setup_no_install is not False:
run_setup_and_exit(arguments.setup_no_install, allow_install=False)
if arguments.list_modules is not False:
list_modules(arguments.list_modules)
setup.run_setup_in_background()
glib.run()

View File

@ -4,9 +4,18 @@ Base class for all Freedombox applications.
"""
import collections
import enum
import inspect
import logging
import sys
from plinth import cfg
from plinth.signals import post_app_loading
from . import clients as clients_module
logger = logging.getLogger(__name__)
class App:
"""Implement common functionality for an app.
@ -40,6 +49,12 @@ class App:
_all_apps = collections.OrderedDict()
class SetupState(enum.Enum):
"""Various states of app being setup."""
NEEDS_SETUP = 'needs-setup'
NEEDS_UPDATE = 'needs-update'
UP_TO_DATE = 'up-to-date'
def __init__(self):
"""Build the app by adding components.
@ -113,6 +128,56 @@ class App:
"""
return self.get_component(self.app_id + '-info')
def setup(self, old_version):
"""Install and configure the app and its components."""
for component in self.components.values():
component.setup(old_version=old_version)
def get_setup_state(self) -> SetupState:
"""Return whether the app is not setup or needs upgrade."""
current_version = self.get_setup_version()
if current_version and self.info.version <= current_version:
return self.SetupState.UP_TO_DATE
# If an app needs installing/updating but no setup method is available,
# then automatically set version.
#
# Minor violation of 'get' only discipline for convenience.
module = sys.modules[self.__module__]
if not hasattr(module, 'setup'):
self.set_setup_version(self.info.version)
return self.SetupState.UP_TO_DATE
if not current_version:
return self.SetupState.NEEDS_SETUP
return self.SetupState.NEEDS_UPDATE
def get_setup_version(self) -> int:
"""Return the setup version of the app."""
# XXX: Optimize version gets
from . import models
try:
app_entry = models.Module.objects.get(pk=self.app_id)
return app_entry.setup_version
except models.Module.DoesNotExist:
return 0
def needs_setup(self) -> bool:
"""Return whether the app needs to be setup.
A simple shortcut for get_setup_state() == NEEDS_SETUP
"""
return self.get_setup_state() == self.SetupState.NEEDS_SETUP
def set_setup_version(self, version: int) -> None:
"""Set the app's setup version."""
from . import models
models.Module.objects.update_or_create(
pk=self.app_id, defaults={'setup_version': version})
def enable(self):
"""Enable all the components of the app."""
for component in self.components.values():
@ -221,6 +286,9 @@ class Component:
"""
return App.get(self.app_id)
def setup(self, old_version):
"""Run operations to install and configure the component."""
def enable(self):
"""Run operations to enable the component."""
@ -393,3 +461,98 @@ class Info(FollowerComponent):
self.donation_url = donation_url
if clients:
clients_module.validate(clients)
def apps_init():
"""Create apps by constructing them with components."""
from . import module_loader # noqa # Avoid circular import
for module_name, module in module_loader.loaded_modules.items():
_initialize_module(module_name, module)
_sort_apps()
logger.info('Initialized apps - %s', ', '.join(
(app.app_id for app in App.list())))
def _sort_apps():
"""Sort apps list according to their essential/dependency order."""
apps = App._all_apps
ordered_modules = []
remaining_apps = dict(apps) # Make a copy
# Place all essential modules ahead of others in module load order
sorted_apps = sorted(
apps, key=lambda app_id: not App.get(app_id).info.is_essential)
for app_id in sorted_apps:
if app_id not in remaining_apps:
continue
app = remaining_apps.pop(app_id)
try:
_insert_apps(app_id, app, remaining_apps, ordered_modules)
except KeyError:
logger.error('Unsatified dependency for app - %s', app_id)
new_all_apps = collections.OrderedDict()
for app_id in ordered_modules:
new_all_apps[app_id] = apps[app_id]
App._all_apps = new_all_apps
def _insert_apps(app_id, app, remaining_apps, ordered_apps):
"""Insert apps into a list based on dependency order."""
if app_id in ordered_apps:
return
for dependency in app.info.depends:
if dependency in ordered_apps:
continue
try:
app = remaining_apps.pop(dependency)
except KeyError:
logger.error('Not found or circular dependency - %s, %s', app_id,
dependency)
raise
_insert_apps(dependency, app, remaining_apps, ordered_apps)
ordered_apps.append(app_id)
def _initialize_module(module_name, module):
"""Perform initialization on all apps in a module."""
# Perform setup related initialization on the module
from . import setup # noqa # Avoid circular import
setup.init(module_name, module)
try:
module_classes = inspect.getmembers(module, inspect.isclass)
app_classes = [
cls for _, cls in module_classes if issubclass(cls, App)
]
for app_class in app_classes:
module.app = app_class()
except Exception as exception:
logger.exception('Exception while running init for %s: %s', module,
exception)
if cfg.develop:
raise
def apps_post_init():
"""Run post initialization on each app."""
for app in App.list():
try:
app.post_init()
if not app.needs_setup() and app.is_enabled():
app.set_enabled(True)
except Exception as exception:
logger.exception('Exception while running post init for %s: %s',
app.app_id, exception)
if cfg.develop:
raise
logger.debug('App initialization completed.')
post_app_loading.send_robust(sender="app")

View File

@ -107,6 +107,33 @@ class Daemon(app.LeaderComponent):
return [testname, result]
class RelatedDaemon(app.FollowerComponent):
"""Component to hold information about additional systemd units handled.
Unlike a daemon described by the Daemon component which is enabled/disabled
when the app is enabled/disabled, the daemon described by this component is
unaffected by the app's enabled/disabled status. The app only has an
indirect interest in this daemon.
This component primarily holds information about such daemon and does
nothing else. This information is used to check if the app is allowed to
perform operations on the daemon.
"""
def __init__(self, component_id, unit):
"""Initialize a new related daemon component.
'component_id' must be a unique string across all apps and components
of a app. Conventionally starts with 'related-daemon-'.
'unit' must the name of systemd unit.
"""
super().__init__(component_id)
self.unit = unit
def app_is_running(app_):
"""Return whether all the daemons in the app are running."""
for component in app_.components.values():

View File

@ -39,16 +39,16 @@ class DomainSelectionForm(forms.Form):
'changed later.'), choices=[])
def _get_domain_choices():
"""Double domain entries for inclusion in the choice field."""
from plinth.modules.names import get_available_tls_domains
return ((domain, domain) for domain in get_available_tls_domains())
class TLSDomainForm(forms.Form):
"""Form to select a TLS domain for an app."""
def get_domain_choices():
"""Double domain entries for inclusion in the choice field."""
from plinth.modules.names import get_available_tls_domains
return ((domain, domain) for domain in get_available_tls_domains())
domain = forms.ChoiceField(
choices=get_domain_choices(),
choices=_get_domain_choices,
label=_('TLS domain'),
help_text=_(
'Select a domain to use TLS with. If the list is empty, please '

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ Common Django middleware.
"""
import logging
import sys
from django import urls
from django.conf import settings
@ -15,7 +16,7 @@ from django.utils.deprecation import MiddlewareMixin
from django.utils.translation import gettext_lazy as _
from stronghold.utils import is_view_func_public
import plinth
from plinth import app as app_module
from plinth import setup
from plinth.package import PackageException
from plinth.utils import is_user_admin
@ -25,14 +26,15 @@ from . import views
logger = logging.getLogger(__name__)
def _collect_setup_result(request, module):
def _collect_setup_result(request, app):
"""Show success/fail message from previous install operation."""
if not module.setup_helper.is_finished:
setup_helper = sys.modules[app.__module__].setup_helper
if not setup_helper.is_finished:
return
exception = module.setup_helper.collect_result()
exception = setup_helper.collect_result()
if not exception:
if not setup._is_module_essential(module):
if not app.info.is_essential:
messages.success(request, _('Application installed.'))
else:
if isinstance(exception, PackageException):
@ -71,16 +73,17 @@ class SetupMiddleware(MiddlewareMixin):
# Requested URL does not belong to any application
return
module_name = resolver_match.namespaces[0]
module = plinth.module_loader.loaded_modules[module_name]
app_id = resolver_match.namespaces[0]
app = app_module.App.get(app_id)
is_admin = is_user_admin(request)
# Collect and show setup operation result to admins
if is_admin:
_collect_setup_result(request, module)
_collect_setup_result(request, app)
# Check if application is up-to-date
if module.setup_helper.get_state() == 'up-to-date':
if app.get_setup_state() == \
app_module.App.SetupState.UP_TO_DATE:
return
if not is_admin:
@ -88,7 +91,7 @@ class SetupMiddleware(MiddlewareMixin):
# Only allow logged-in users to access any setup page
view = login_required(views.SetupView.as_view())
return view(request, setup_helper=module.setup_helper)
return view(request, app_id=app_id)
class AdminRequiredMiddleware(MiddlewareMixin):

View File

@ -5,15 +5,14 @@ Discover, load and manage FreedomBox applications.
import collections
import importlib
import inspect
import logging
import pathlib
import re
import django
from plinth import app, cfg, setup
from plinth.signals import post_module_loading, pre_module_loading
from plinth import cfg
from plinth.signals import pre_module_loading
logger = logging.getLogger(__name__)
@ -28,74 +27,23 @@ def include_urls():
_include_module_urls(module_import_path, module_name)
def _is_module_essential(module):
"""Return if a module is an essential module."""
return getattr(module, 'is_essential', False)
def load_modules():
"""
Read names of enabled modules in modules/enabled directory and
import them from modules directory.
"""
pre_module_loading.send_robust(sender="module_loader")
modules = {}
for module_import_path in get_modules_to_load():
module_name = module_import_path.split('.')[-1]
try:
modules[module_name] = importlib.import_module(module_import_path)
loaded_modules[module_name] = importlib.import_module(
module_import_path)
except Exception as exception:
logger.exception('Could not import %s: %s', module_import_path,
exception)
if cfg.develop:
raise
ordered_modules = []
remaining_modules = dict(modules) # Make a copy
# Place all essential modules ahead of others in module load order
sorted_modules = sorted(
modules, key=lambda module: not _is_module_essential(modules[module]))
for module_name in sorted_modules:
if module_name not in remaining_modules:
continue
module = remaining_modules.pop(module_name)
try:
_insert_modules(module_name, module, remaining_modules,
ordered_modules)
except KeyError:
logger.error('Unsatified dependency for module - %s', module_name)
for module_name in ordered_modules:
loaded_modules[module_name] = modules[module_name]
def _insert_modules(module_name, module, remaining_modules, ordered_modules):
"""Insert modules into a list based on dependency order"""
if module_name in ordered_modules:
return
dependencies = []
try:
dependencies = module.depends
except AttributeError:
pass
for dependency in dependencies:
if dependency in ordered_modules:
continue
try:
module = remaining_modules.pop(dependency)
except KeyError:
logger.error('Not found or circular dependency - %s, %s',
module_name, dependency)
raise
_insert_modules(dependency, module, remaining_modules, ordered_modules)
ordered_modules.append(module_name)
def _include_module_urls(module_import_path, module_name):
"""Include the module's URLs in global project URLs list"""
@ -112,54 +60,6 @@ def _include_module_urls(module_import_path, module_name):
raise
def apps_init():
"""Create apps by constructing them with components."""
logger.info('Initializing apps - %s', ', '.join(loaded_modules))
for module_name, module in loaded_modules.items():
_initialize_module(module_name, module)
def _initialize_module(module_name, module):
"""Perform module initialization"""
# Perform setup related initialization on the module
setup.init(module_name, module)
try:
module_classes = inspect.getmembers(module, inspect.isclass)
app_class = [
cls for cls in module_classes if issubclass(cls[1], app.App)
]
if module_classes and app_class:
module.app = app_class[0][1]()
except Exception as exception:
logger.exception('Exception while running init for %s: %s', module,
exception)
if cfg.develop:
raise
def apps_post_init():
"""Run post initialization on each app."""
for module in loaded_modules.values():
if not hasattr(module, 'app') or not module.app:
continue
try:
module.app.post_init()
if module.setup_helper.get_state(
) != 'needs-setup' and module.app.is_enabled():
module.app.set_enabled(True)
except Exception as exception:
logger.exception('Exception while running post init for %s: %s',
module, exception)
if cfg.develop:
raise
logger.debug('App initialization completed.')
post_module_loading.send_robust(sender="module_loader")
def get_modules_to_load():
"""Get the list of modules to be loaded"""
global _modules_to_load

View File

@ -9,22 +9,12 @@ from django.utils.translation import gettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import cfg
from plinth.daemon import Daemon
from plinth.daemon import Daemon, RelatedDaemon
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.package import Packages
from plinth.utils import format_lazy, is_valid_user_name
version = 9
is_essential = True
managed_services = ['apache2', 'uwsgi']
managed_packages = [
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
]
app = None
@ -33,16 +23,19 @@ class ApacheApp(app_module.App):
app_id = 'apache'
_version = 9
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential,
name=_('Apache HTTP Server'))
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Apache HTTP Server'))
self.add(info)
packages = Packages('packages-apache', managed_packages)
packages = Packages('packages-apache', [
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
])
self.add(packages)
web_server_ports = Firewall('firewall-web', _('Web Server'),
@ -57,16 +50,19 @@ class ApacheApp(app_module.App):
self.add(freedombox_ports)
letsencrypt = LetsEncrypt('letsencrypt-apache', domains='*',
daemons=[managed_services[0]])
daemons=['apache2'])
self.add(letsencrypt)
daemon = Daemon('daemon-apache', managed_services[0])
daemon = Daemon('daemon-apache', 'apache2')
self.add(daemon)
daemon = RelatedDaemon('related-daemon-apache', 'uwsgi')
self.add(daemon)
def setup(helper, old_version=None):
"""Configure the module."""
helper.install(managed_packages)
app.setup(old_version)
actions.superuser_run(
'apache',
['setup', '--old-version', str(old_version)])

View File

@ -3,4 +3,20 @@
FreedomBox app for api for android app.
"""
version = 1
from plinth import app as app_module
class ApiApp(app_module.App):
"""FreedomBox app for API for Android app."""
app_id = 'api'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True)
self.add(info)

View File

@ -21,16 +21,6 @@ from . import manifest
# pylint: disable=C0103
version = 1
is_essential = True
depends = ['names']
managed_services = ['avahi-daemon']
managed_packages = ['avahi-daemon', 'avahi-utils']
_description = [
format_lazy(
_('Service discovery allows other devices on the network to '
@ -42,8 +32,6 @@ _description = [
'hostile local network.'), box_name=_(cfg.box_name))
]
manual_page = 'ServiceDiscovery'
app = None
@ -52,11 +40,14 @@ class AvahiApp(app_module.App):
app_id = 'avahi'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, depends=['names'],
name=_('Service Discovery'), icon='fa-compass',
description=_description,
manual_page='ServiceDiscovery')
@ -66,7 +57,7 @@ class AvahiApp(app_module.App):
'avahi:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-avahi', managed_packages)
packages = Packages('packages-avahi', ['avahi-daemon', 'avahi-utils'])
self.add(packages)
domain_type = DomainType('domain-type-local',
@ -78,7 +69,7 @@ class AvahiApp(app_module.App):
is_external=False)
self.add(firewall)
daemon = Daemon('daemon-avahi', managed_services[0])
daemon = Daemon('daemon-avahi', 'avahi-daemon')
self.add(daemon)
backup_restore = BackupRestore('backup-restore-avahi',
@ -98,7 +89,7 @@ class AvahiApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
# Reload avahi-daemon now that first-run does not reboot. After performing
# FreedomBox Service (Plinth) package installation, new Avahi files will be
# available and require restart.

View File

@ -7,7 +7,10 @@ import pytest
from plinth.tests.functional import BaseAppTests
pytestmark = [pytest.mark.system, pytest.mark.essential, pytest.mark.avahi]
pytestmark = [
pytest.mark.system, pytest.mark.essential, pytest.mark.domain,
pytest.mark.avahi
]
class TestAvahiApp(BaseAppTests):

View File

@ -23,14 +23,6 @@ from . import api
logger = logging.getLogger(__name__)
version = 3
is_essential = True
managed_packages = ['borgbackup', 'sshfs']
depends = ['storage']
_description = [
_('Backups allows creating and managing backup archives.'),
]
@ -47,14 +39,16 @@ class BackupsApp(app_module.App):
app_id = 'backups'
_version = 3
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(
app_id=self.app_id, version=version, depends=depends,
name=_('Backups'), icon='fa-files-o', description=_description,
manual_page='Backups',
app_id=self.app_id, version=self._version, is_essential=True,
depends=['storage'], name=_('Backups'), icon='fa-files-o',
description=_description, manual_page='Backups',
donation_url='https://www.borgbackup.org/support/fund.html')
self.add(info)
@ -62,7 +56,7 @@ class BackupsApp(app_module.App):
'backups:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-backups', managed_packages)
packages = Packages('packages-backups', ['borgbackup', 'sshfs'])
self.add(packages)
@staticmethod
@ -76,7 +70,7 @@ class BackupsApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
from . import repository
helper.call('post', actions.superuser_run, 'backups',
['setup', '--path', repository.RootBorgRepository.PATH])

View File

@ -10,7 +10,6 @@ TODO:
- Implement unit tests.
"""
import importlib
import logging
from plinth import action_utils, actions
@ -174,13 +173,14 @@ def _install_apps_before_restore(components):
data getting backed up into older version of the app.
"""
modules_to_setup = []
apps_to_setup = []
for component in components:
module = importlib.import_module(component.app.__class__.__module__)
if module.setup_helper.get_state() in ('needs-setup', 'needs-update'):
modules_to_setup.append(component.app.app_id)
if component.app.get_setup_state() in (
app_module.App.SetupState.NEEDS_SETUP,
app_module.App.SetupState.NEEDS_UPDATE):
apps_to_setup.append(component.app.app_id)
setup.run_setup_on_modules(modules_to_setup)
setup.run_setup_on_apps(apps_to_setup)
def _get_backup_restore_component(app):
@ -198,8 +198,7 @@ def get_all_components_for_backup():
for app_ in app_module.App.list():
try:
module = importlib.import_module(app_.__class__.__module__)
if module.setup_helper.get_state() != 'needs-setup':
if not app_.needs_setup():
components.append(_get_backup_restore_component(app_))
except TypeError: # Application not available for backup/restore
pass

View File

@ -95,23 +95,23 @@ class TestBackupProcesses:
@staticmethod
@patch('plinth.modules.backups.api._install_apps_before_restore')
@patch('plinth.module_loader.loaded_modules.items')
def test_restore_apps(mock_install, modules):
def test_restore_apps(mock_install):
"""Test that restore_handler is called."""
modules.return_value = [('a', MagicMock())]
restore_handler = MagicMock()
api.restore_apps(restore_handler)
restore_handler.assert_called_once()
@staticmethod
@patch('importlib.import_module')
@patch('plinth.app.App.get_setup_state')
@patch('plinth.app.App.list')
def test_get_all_components_for_backup(apps_list, import_module):
def test_get_all_components_for_backup(apps_list, get_setup_state):
"""Test listing components supporting backup and needing backup."""
modules = [MagicMock(), MagicMock(), MagicMock()]
import_module.side_effect = modules
get_setup_state.side_effect = [
App.SetupState.UP_TO_DATE,
App.SetupState.NEEDS_SETUP,
App.SetupState.UP_TO_DATE,
]
apps = [_get_test_app('a'), _get_test_app('b'), _get_test_app('c')]
modules[1].setup_helper.get_state.side_effect = ['needs-setup']
apps_list.return_value = apps
returned_components = api.get_all_components_for_backup()

View File

@ -15,8 +15,6 @@ from plinth.app import App
from ..components import BackupRestore
from ..schedule import Schedule
setup_helper = MagicMock()
class AppTest(App):
"""Sample App for testing."""
@ -426,11 +424,12 @@ cases = [
@pytest.mark.parametrize(
'schedule_params,archives_data,test_now,run_periods,cleanups', cases)
@patch('plinth.app.App.get_setup_state')
@patch('plinth.modules.backups.repository.get_instance')
def test_run_schedule(get_instance, schedule_params, archives_data, test_now,
run_periods, cleanups):
def test_run_schedule(get_instance, get_setup_state, schedule_params,
archives_data, test_now, run_periods, cleanups):
"""Test that backups are run at expected time."""
setup_helper.get_state.return_value = 'up-to-date'
get_setup_state.return_value = App.SetupState.UP_TO_DATE
repository = MagicMock()
repository.list_archives.side_effect = \

View File

@ -17,10 +17,6 @@ from plinth.package import Packages
from . import manifest
version = 2
managed_packages = ['bepasty']
_description = [
_('bepasty is a web application that allows large files to be uploaded '
'and shared. Text and code snippets can also be pasted and shared. '
@ -58,11 +54,13 @@ class BepastyApp(app_module.App):
app_id = 'bepasty'
_version = 2
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(self.app_id, version, name=_('bepasty'),
info = app_module.Info(self.app_id, self._version, name=_('bepasty'),
icon_filename='bepasty',
short_description=_('File & Snippet Sharing'),
description=_description, manual_page='bepasty',
@ -80,7 +78,7 @@ class BepastyApp(app_module.App):
clients=manifest.clients)
self.add(shortcut)
packages = Packages('packages-bepasty', managed_packages)
packages = Packages('packages-bepasty', ['bepasty'])
self.add(packages)
firewall = Firewall('firewall-bepasty', info.name,
@ -101,7 +99,7 @@ class BepastyApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call('post', actions.superuser_run, 'bepasty',
['setup', '--domain-name', 'freedombox.local'])
helper.call('post', app.enable)

View File

@ -21,12 +21,6 @@ from plinth.utils import format_lazy
from . import manifest
version = 2
managed_services = ['bind9', 'named']
managed_packages = ['bind9']
_description = [
_('BIND enables you to publish your Domain Name System (DNS) information '
'on the Internet, and to resolve DNS queries for your user devices on '
@ -72,11 +66,13 @@ class BindApp(app_module.App):
app_id = 'bind'
_version = 2
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('BIND'), icon='fa-globe-w',
short_description=_('Domain Name Server'),
description=_description)
@ -87,7 +83,7 @@ class BindApp(app_module.App):
parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-bind', managed_packages)
packages = Packages('packages-bind', ['bind9'])
self.add(packages)
firewall = Firewall('firewall-bind', info.name, ports=['dns'],
@ -95,11 +91,8 @@ class BindApp(app_module.App):
self.add(firewall)
daemon = Daemon(
'daemon-bind', managed_services[0], listen_ports=[(53, 'tcp6'),
(53, 'udp6'),
(53, 'tcp4'),
(53, 'udp4')],
alias=managed_services[1])
'daemon-bind', 'named', listen_ports=[(53, 'tcp6'), (53, 'udp6'),
(53, 'tcp4'), (53, 'udp4')])
self.add(daemon)
backup_restore = BackupRestore('backup-restore-bind',
@ -109,7 +102,7 @@ class BindApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call(
'post', actions.superuser_run, 'bind',
['setup', '--old-version', str(old_version)])
@ -118,7 +111,7 @@ def setup(helper, old_version=None):
def force_upgrade(helper, _packages):
"""Force upgrade the managed packages to resolve conffile prompt."""
helper.install(managed_packages, force_configuration='old')
helper.install(['bind9'], force_configuration='old')
return True

View File

@ -7,5 +7,5 @@ backup = {
'config': {
'files': ['/etc/bind/named.conf.options']
},
'services': ['bind9']
'services': ['named']
}

View File

@ -21,12 +21,6 @@ from plinth.utils import format_lazy
from . import manifest
version = 1
managed_services = ['calibre-server-freedombox']
managed_packages = ['calibre']
_description = [
format_lazy(
_('calibre server provides online access to your e-book collection. '
@ -52,13 +46,17 @@ class CalibreApp(app_module.App):
app_id = 'calibre'
_version = 1
DAEMON = 'calibre-server-freedombox'
def __init__(self):
"""Create components for the app."""
super().__init__()
groups = {'calibre': _('Use calibre e-book libraries')}
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('calibre'), icon_filename='calibre',
short_description=_('E-book Library'),
description=_description, manual_page='Calibre',
@ -79,7 +77,7 @@ class CalibreApp(app_module.App):
allowed_groups=list(groups))
self.add(shortcut)
packages = Packages('packages-calibre', managed_packages)
packages = Packages('packages-calibre', ['calibre'])
self.add(packages)
firewall = Firewall('firewall-calibre', info.name,
@ -90,7 +88,7 @@ class CalibreApp(app_module.App):
urls=['https://{host}/calibre'])
self.add(webserver)
daemon = Daemon('daemon-calibre', managed_services[0],
daemon = Daemon('daemon-calibre', self.DAEMON,
listen_ports=[(8844, 'tcp4')])
self.add(daemon)
@ -106,7 +104,7 @@ class CalibreApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call('post', app.enable)
@ -125,10 +123,10 @@ def list_libraries():
def create_library(name):
"""Create an empty library."""
actions.superuser_run('calibre', ['create-library', name])
actions.superuser_run('service', ['try-restart', managed_services[0]])
actions.superuser_run('service', ['try-restart', CalibreApp.DAEMON])
def delete_library(name):
"""Delete a library and its contents."""
actions.superuser_run('calibre', ['delete-library', name])
actions.superuser_run('service', ['try-restart', managed_services[0]])
actions.superuser_run('service', ['try-restart', CalibreApp.DAEMON])

View File

@ -20,14 +20,6 @@ from plinth.utils import format_lazy
from . import manifest, utils
version = 1
is_essential = True
managed_services = ['cockpit.socket']
managed_packages = ['cockpit']
_description = [
format_lazy(
_('Cockpit is a server manager that makes it easy to administer '
@ -58,12 +50,16 @@ class CockpitApp(app_module.App):
app_id = 'cockpit'
_version = 1
DAEMON = 'cockpit.socket'
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential, name=_('Cockpit'),
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Cockpit'),
icon='fa-wrench', icon_filename='cockpit',
short_description=_('Server Administration'),
description=_description, manual_page='Cockpit',
@ -83,7 +79,7 @@ class CockpitApp(app_module.App):
allowed_groups=['admin'])
self.add(shortcut)
packages = Packages('packages-cockpit', managed_packages)
packages = Packages('packages-cockpit', ['cockpit'])
self.add(packages)
firewall = Firewall('firewall-cockpit', info.name,
@ -94,7 +90,7 @@ class CockpitApp(app_module.App):
urls=['https://{host}/_cockpit/'])
self.add(webserver)
daemon = Daemon('daemon-cockpit', managed_services[0])
daemon = Daemon('daemon-cockpit', self.DAEMON)
self.add(daemon)
backup_restore = BackupRestore('backup-restore-cockpit',
@ -110,7 +106,7 @@ class CockpitApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
domains = names.components.DomainName.list_names('https')
helper.call('post', actions.superuser_run, 'cockpit',
['setup'] + list(domains))
@ -120,19 +116,17 @@ def setup(helper, old_version=None):
def on_domain_added(sender, domain_type, name, description='', services=None,
**kwargs):
"""Handle addition of a new domain."""
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
if not app.needs_setup():
if name not in utils.get_domains():
actions.superuser_run('cockpit', ['add-domain', name])
actions.superuser_run('service',
['try-restart', managed_services[0]])
['try-restart', CockpitApp.DAEMON])
def on_domain_removed(sender, domain_type, name, **kwargs):
"""Handle removal of a domain."""
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
if not app.needs_setup():
if name in utils.get_domains():
actions.superuser_run('cockpit', ['remove-domain', name])
actions.superuser_run('service',
['try-restart', managed_services[0]])
['try-restart', CockpitApp.DAEMON])

View File

@ -12,27 +12,18 @@ from django.utils.translation import gettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import frontpage, menu
from plinth.daemon import RelatedDaemon
from plinth.modules.apache import (get_users_with_website, user_of_uws_url,
uws_url_of_user)
from plinth.modules.names.components import DomainType
from plinth.package import Packages
from plinth.signals import domain_added
version = 3
is_essential = True
managed_services = ['systemd-journald', 'rsyslog']
_description = [
_('Here you can set some general configuration options '
'like hostname, domain name, webserver home page etc.')
]
depends = ['apache', 'firewall', 'names']
managed_packages = ['zram-tools']
APACHE_CONF_ENABLED_DIR = '/etc/apache2/conf-enabled'
APACHE_HOMEPAGE_CONF_FILE_NAME = 'freedombox-apache-homepage.conf'
APACHE_HOMEPAGE_CONFIG = os.path.join(APACHE_CONF_ENABLED_DIR,
@ -49,15 +40,18 @@ class ConfigApp(app_module.App):
app_id = 'config'
_version = 3
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential, depends=depends,
name=_('General Configuration'), icon='fa-cog',
description=_description,
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True,
depends=['apache', 'firewall', 'names'
], name=_('General Configuration'),
icon='fa-cog', description=_description,
manual_page='Configure')
self.add(info)
@ -65,9 +59,15 @@ class ConfigApp(app_module.App):
'config:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-config', managed_packages)
packages = Packages('packages-config', ['zram-tools'])
self.add(packages)
daemon1 = RelatedDaemon('related-daemon-config1', 'systemd-journald')
self.add(daemon1)
daemon2 = RelatedDaemon('related-daemon-config2', 'rsyslog')
self.add(daemon2)
domain_type = DomainType('domain-type-static', _('Domain Name'),
'config:index', can_have_certificate=True)
self.add(domain_type)
@ -192,7 +192,7 @@ def set_advanced_mode(advanced_mode):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
_migrate_home_page_config()
# systemd-journald is socket activated, it may not be running and it does

View File

@ -6,7 +6,10 @@ Functional, browser based tests for config app.
import pytest
from plinth.tests import functional
pytestmark = [pytest.mark.system, pytest.mark.essential, pytest.mark.config]
pytestmark = [
pytest.mark.system, pytest.mark.essential, pytest.mark.domain,
pytest.mark.config
]
@pytest.fixture(scope='module', autouse=True)

View File

@ -25,14 +25,6 @@ from plinth.utils import format_lazy
from . import manifest
version = 1
managed_services = ['coturn']
managed_packages = ['coturn']
managed_paths = [pathlib.Path('/etc/coturn/')]
_description = [
_('Coturn is a server to facilitate audio/video calls and conferences by '
'providing an implementation of TURN and STUN protocols. WebRTC, SIP '
@ -55,11 +47,13 @@ class CoturnApp(app_module.App):
app_id = 'coturn'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('Coturn'), icon_filename='coturn',
short_description=_('VoIP Helper'),
description=_description, manual_page='Coturn')
@ -70,7 +64,7 @@ class CoturnApp(app_module.App):
parent_url_name='apps')
self.add(menu_item)
packages = Packages('packages-coturn', managed_packages)
packages = Packages('packages-coturn', ['coturn'])
self.add(packages)
firewall = Firewall('firewall-coturn', info.name,
@ -78,8 +72,8 @@ class CoturnApp(app_module.App):
self.add(firewall)
letsencrypt = LetsEncrypt(
'letsencrypt-coturn', domains=get_domains,
daemons=managed_services, should_copy_certificates=True,
'letsencrypt-coturn', domains=get_domains, daemons=['coturn'],
should_copy_certificates=True,
private_key_path='/etc/coturn/certs/pkey.pem',
certificate_path='/etc/coturn/certs/cert.pem',
user_owner='turnserver', group_owner='turnserver',
@ -87,13 +81,22 @@ class CoturnApp(app_module.App):
self.add(letsencrypt)
daemon = Daemon(
'daemon-coturn', managed_services[0],
listen_ports=[(3478, 'udp4'), (3478, 'udp6'), (3478, 'tcp4'),
(3478, 'tcp6'), (3479, 'udp4'), (3479, 'udp6'),
(3479, 'tcp4'), (3479, 'tcp6'), (5349, 'udp4'),
(5349, 'udp6'), (5349, 'tcp4'), (5349, 'tcp6'),
(5350, 'udp4'), (5350, 'udp6'), (5350, 'tcp4'),
(5350, 'tcp6')])
'daemon-coturn', 'coturn', listen_ports=[(3478, 'udp4'),
(3478, 'udp6'),
(3478, 'tcp4'),
(3478, 'tcp6'),
(3479, 'udp4'),
(3479, 'udp6'),
(3479, 'tcp4'),
(3479, 'tcp6'),
(5349, 'udp4'),
(5349, 'udp6'),
(5349, 'tcp4'),
(5349, 'tcp6'),
(5350, 'udp4'),
(5350, 'udp6'),
(5350, 'tcp4'),
(5350, 'tcp6')])
self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-coturn',
@ -107,7 +110,7 @@ class CoturnApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call('post', actions.superuser_run, 'coturn', ['setup'])
helper.call('post', app.enable)
app.get_component('letsencrypt-coturn').setup_certificates()

View File

@ -14,12 +14,6 @@ from plinth.modules.backups.components import BackupRestore
from . import manifest
version = 2
is_essential = True
managed_services = ['systemd-timesyncd']
_description = [
_('Network time server is a program that maintains the system time '
'in synchronization with servers on the Internet.')
@ -33,6 +27,8 @@ class DateTimeApp(app_module.App):
app_id = 'datetime'
_version = 2
_time_managed = None
@property
@ -64,10 +60,9 @@ class DateTimeApp(app_module.App):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential,
name=_('Date & Time'), icon='fa-clock-o',
description=_description,
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Date & Time'),
icon='fa-clock-o', description=_description,
manual_page='DateTime')
self.add(info)
@ -76,7 +71,7 @@ class DateTimeApp(app_module.App):
self.add(menu_item)
if self._is_time_managed():
daemon = Daemon('daemon-datetime', managed_services[0])
daemon = Daemon('daemon-datetime', 'systemd-timesyncd')
self.add(daemon)
backup_restore = BackupRestore('backup-restore-datetime',

View File

@ -18,12 +18,6 @@ from plinth.package import Packages
from . import manifest
version = 6
managed_services = ['deluged', 'deluge-web']
managed_packages = ['deluged', 'deluge-web']
_description = [
_('Deluge is a BitTorrent client that features a Web UI.'),
_('The default password is \'deluge\', but you should log in and '
@ -40,6 +34,8 @@ class DelugeApp(app_module.App):
app_id = 'deluge'
_version = 6
def __init__(self):
"""Create components for the app."""
super().__init__()
@ -49,7 +45,7 @@ class DelugeApp(app_module.App):
}
info = app_module.Info(
app_id=self.app_id, version=version, name=_('Deluge'),
app_id=self.app_id, version=self._version, name=_('Deluge'),
icon_filename='deluge',
short_description=_('BitTorrent Web Client'),
description=_description, manual_page='Deluge',
@ -70,7 +66,7 @@ class DelugeApp(app_module.App):
allowed_groups=list(groups))
self.add(shortcut)
packages = Packages('packages-deluge', managed_packages)
packages = Packages('packages-deluge', ['deluged', 'deluge-web'])
self.add(packages)
firewall = Firewall('firewall-deluge', info.name,
@ -81,11 +77,11 @@ class DelugeApp(app_module.App):
urls=['https://{host}/deluge'])
self.add(webserver)
daemon = Daemon('daemon-deluged', managed_services[0],
daemon = Daemon('daemon-deluged', 'deluged',
listen_ports=[(58846, 'tcp4')])
self.add(daemon)
daemon_web = Daemon('daemon-deluge-web', managed_services[1],
daemon_web = Daemon('daemon-deluge-web', 'deluge-web',
listen_ports=[(8112, 'tcp4')])
self.add(daemon_web)
@ -101,7 +97,7 @@ class DelugeApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
add_user_to_share_group(SYSTEM_USER)
helper.call('post', actions.superuser_run, 'deluge', ['setup'])
helper.call('post', app.enable)

View File

@ -4,7 +4,6 @@ FreedomBox app for system diagnostics.
"""
import collections
import importlib
import logging
import pathlib
import threading
@ -20,10 +19,6 @@ from plinth.modules.backups.components import BackupRestore
from . import manifest
version = 1
is_essential = True
_description = [
_('The system diagnostic test will run a number of checks on your '
'system to confirm that applications and services are working as '
@ -46,13 +41,14 @@ class DiagnosticsApp(app_module.App):
app_id = 'diagnostics'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential,
name=_('Diagnostics'), icon='fa-heartbeat',
description=_description,
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Diagnostics'),
icon='fa-heartbeat', description=_description,
manual_page='Diagnostics')
self.add(info)
@ -115,8 +111,7 @@ def run_on_all_enabled_modules():
for app in app_module.App.list():
# Don't run diagnostics on apps have not been setup yet.
# However, run on apps that need an upgrade.
module = importlib.import_module(app.__class__.__module__)
if module.setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
continue
if not app.is_enabled():

View File

@ -36,12 +36,6 @@ def get_configured_domain_name():
return lazy_domain_name
version = 1
managed_services = ['diaspora']
managed_packages = ['diaspora']
_description = [
_('diaspora* is a decentralized social network where you can store '
'and control your own data.'),
@ -61,12 +55,14 @@ class DiasporaApp(app_module.App):
app_id = 'diaspora'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
from . import manifest
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('diaspora*'), icon_filename='diaspora',
short_description=_('Federated Social Network'),
description=_description,
@ -84,7 +80,7 @@ class DiasporaApp(app_module.App):
clients=info.clients, login_required=True)
self.add(shortcut)
packages = Packages('packages-diaspora', managed_packages)
packages = Packages('packages-diaspora', ['diaspora'])
self.add(packages)
firewall = Firewall('firewall-diaspora', info.name,
@ -94,7 +90,7 @@ class DiasporaApp(app_module.App):
webserver = Webserver('webserver-diaspora', 'diaspora-plinth')
self.add(webserver)
daemon = Daemon('daemon-diaspora', managed_services[0])
daemon = Daemon('daemon-diaspora', 'diaspora')
self.add(daemon)
def diagnose(self):
@ -127,7 +123,7 @@ class Shortcut(frontpage.Shortcut):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.call('pre', actions.superuser_run, 'diaspora', ['pre-install'])
helper.install(managed_packages)
app.setup(old_version)
helper.call('custom_config', actions.superuser_run, 'diaspora',
['disable-ssl'])

View File

@ -17,14 +17,6 @@ from plinth.utils import format_lazy
from . import manifest
version = 1
is_essential = True
depends = ['names']
managed_packages = ['ez-ipupdate']
_description = [
format_lazy(
_('If your Internet provider changes your IP address periodically '
@ -49,12 +41,14 @@ class DynamicDNSApp(app_module.App):
app_id = 'dynamicdns'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential, depends=depends,
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, depends=['names'],
name=_('Dynamic DNS Client'), icon='fa-refresh',
description=_description,
manual_page='DynamicDNS')
@ -64,7 +58,7 @@ class DynamicDNSApp(app_module.App):
'dynamicdns:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-dynamicdns', managed_packages)
packages = Packages('packages-dynamicdns', ['ez-ipupdate'])
self.add(packages)
domain_type = DomainType('domain-type-dynamic',
@ -101,7 +95,7 @@ class DynamicDNSApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
def get_status():

View File

@ -56,7 +56,7 @@ class ConfigureForm(forms.Form):
'router) this URL is used to determine the real '
'IP address. The URL should simply return the IP where '
'the client comes from (example: '
'http://myip.datasystems24.de).'),
'https://ddns.freedombox.org/ip/).'),
box_name=gettext_lazy(cfg.box_name))
help_user = \
gettext_lazy('The username that was used when the account was '

View File

@ -11,8 +11,8 @@
<p>
{% blocktrans trimmed %}
If you are looking for a free dynamic DNS account, you may find
a free GnuDIP service at <a href='http://gnudip.datasystems24.net'
target='_blank'>gnudip.datasystems24.net</a> or you may find
a free GnuDIP service at <a href='https://ddns.freedombox.org'
target='_blank'>ddns.freedombox.org</a> or you may find
free update URL based services at
<a href='http://freedns.afraid.org/' target='_blank'>
freedns.afraid.org</a>.

View File

@ -6,10 +6,12 @@ Functional, browser based tests for dynamicdns app.
import time
import pytest
from plinth.tests import functional
pytestmark = [
pytest.mark.system, pytest.mark.essential, pytest.mark.dynamicdns
pytest.mark.system, pytest.mark.essential, pytest.mark.domain,
pytest.mark.dynamicdns
]
@ -55,7 +57,7 @@ def _configure(browser):
browser.find_by_id('id_dynamicdns_user').fill('tester')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip.datasystems24.de')
'https://ddns.freedombox.org/ip/')
functional.submit(browser)
# After a domain name change, Let's Encrypt will restart the web
@ -76,7 +78,7 @@ def _has_original_config(browser):
ipurl = browser.find_by_id('id_dynamicdns_ipurl').value
if enabled and service_type == 'GnuDIP' and server == 'example.com' \
and domain == 'freedombox.example.com' and user == 'tester' \
and ipurl == 'http://myip.datasystems24.de':
and ipurl == 'https://ddns.freedombox.org/ip/':
return True
else:
return False
@ -93,7 +95,7 @@ def _change_config(browser):
browser.find_by_id('id_dynamicdns_user').fill('tester2')
browser.find_by_id('id_dynamicdns_secret').fill('testingtesting2')
browser.find_by_id('id_dynamicdns_ipurl').fill(
'http://myip2.datasystems24.de')
'https://ddns2.freedombox.org/ip/')
functional.submit(browser)
# After a domain name change, Let's Encrypt will restart the web

View File

@ -5,7 +5,6 @@ FreedomBox app to configure ejabberd server.
import json
import logging
import pathlib
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -28,14 +27,6 @@ from plinth.utils import format_lazy
from . import manifest
version = 4
managed_services = ['ejabberd']
managed_packages = ['ejabberd']
managed_paths = [pathlib.Path('/etc/ejabberd/')]
_description = [
_('XMPP is an open and standardized communication protocol. Here '
'you can run and configure your XMPP server, called ejabberd.'),
@ -53,8 +44,6 @@ _description = [
'an external server.'), coturn_url=reverse_lazy('coturn:index'))
]
depends = ['coturn']
logger = logging.getLogger(__name__)
app = None
@ -65,16 +54,17 @@ class EjabberdApp(app_module.App):
app_id = 'ejabberd'
_version = 4
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
name=_('ejabberd'), icon_filename='ejabberd',
short_description=_('Chat Server'),
description=_description,
manual_page='ejabberd',
clients=manifest.clients)
info = app_module.Info(
app_id=self.app_id, version=self._version, depends=['coturn'],
name=_('ejabberd'), icon_filename='ejabberd',
short_description=_('Chat Server'), description=_description,
manual_page='ejabberd', clients=manifest.clients)
self.add(info)
menu_item = menu.Menu('menu-ejabberd', info.name,
@ -90,7 +80,7 @@ class EjabberdApp(app_module.App):
login_required=True)
self.add(shortcut)
packages = Packages('packages-ejabberd', managed_packages)
packages = Packages('packages-ejabberd', ['ejabberd'])
self.add(packages)
firewall = Firewall('firewall-ejabberd', info.name,
@ -112,9 +102,12 @@ class EjabberdApp(app_module.App):
self.add(letsencrypt)
daemon = Daemon(
'daemon-ejabberd', managed_services[0],
listen_ports=[(5222, 'tcp4'), (5222, 'tcp6'), (5269, 'tcp4'),
(5269, 'tcp6'), (5443, 'tcp4'), (5443, 'tcp6')])
'daemon-ejabberd', 'ejabberd', listen_ports=[(5222, 'tcp4'),
(5222, 'tcp6'),
(5269, 'tcp4'),
(5269, 'tcp6'),
(5443, 'tcp4'),
(5443, 'tcp6')])
self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-ejabberd',
@ -152,7 +145,7 @@ def setup(helper, old_version=None):
helper.call('pre', actions.superuser_run, 'ejabberd',
['pre-install', '--domainname', domainname])
# XXX: Configure all other domain names
helper.install(managed_packages)
app.setup(old_version)
helper.call('post',
app.get_component('letsencrypt-ejabberd').setup_certificates,
[domainname])
@ -171,8 +164,7 @@ def get_domains():
XXX: Retrieve the list from ejabberd configuration.
"""
setup_helper = globals()['setup_helper']
if setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
return []
domain_name = config.get_domainname()
@ -188,8 +180,7 @@ def on_pre_hostname_change(sender, old_hostname, new_hostname, **kwargs):
"""
del sender # Unused
del kwargs # Unused
setup_helper = globals()['setup_helper']
if setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
return
actions.superuser_run('ejabberd', [
@ -202,8 +193,7 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
"""Update ejabberd config after hostname change."""
del sender # Unused
del kwargs # Unused
setup_helper = globals()['setup_helper']
if setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
return
actions.superuser_run('ejabberd', [
@ -215,8 +205,7 @@ def on_post_hostname_change(sender, old_hostname, new_hostname, **kwargs):
def on_domain_added(sender, domain_type, name, description='', services=None,
**kwargs):
"""Update ejabberd config after domain name change."""
setup_helper = globals()['setup_helper']
if setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
return
conf = actions.superuser_run('ejabberd', ['get-configuration'])
@ -229,8 +218,7 @@ def on_domain_added(sender, domain_type, name, description='', services=None,
def update_turn_configuration(config: TurnConfiguration, managed=True,
force=False):
"""Update ejabberd's STUN/TURN server configuration."""
setup_helper = globals()['setup_helper']
if not force and setup_helper.get_state() == 'needs-setup':
if app.needs_setup():
return
params = ['configure-turn']

View File

@ -5,7 +5,7 @@ Test module for ejabberd STUN/TURN configuration.
import pathlib
import shutil
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
@ -58,10 +58,9 @@ def fixture_test_configuration(call_action, conf_file):
Patches actions.superuser_run with the fixture call_action.
The module state is patched to be 'up-to-date'.
"""
with patch('plinth.actions.superuser_run', call_action):
helper = MagicMock()
helper.get_state.return_value = 'up-to-date'
ejabberd.setup_helper = helper
with patch('plinth.actions.superuser_run',
call_action), patch('plinth.modules.ejabberd.app') as app:
app.needs_setup.return_value = False
yield

View File

@ -14,31 +14,11 @@ from plinth.modules.apache.components import Webserver
from plinth.modules.config import get_domainname
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.package import packages_installed, remove
from plinth.package import Packages, remove
from plinth.signals import domain_added, domain_removed
from . import audit, manifest
version = 1
# Other likely install conflicts have been discarded:
# - msmtp, nullmailer, sendmail don't cause install faults.
# - qmail and smail are missing in Bullseye (Not tested,
# but less likely due to that).
package_conflicts = ('exim4-base', 'exim4-config', 'exim4-daemon-light')
package_conflicts_action = 'ignore'
packages = [
'postfix-ldap',
'postfix-sqlite',
'dovecot-pop3d',
'dovecot-imapd',
'dovecot-ldap',
'dovecot-lmtpd',
'dovecot-managesieved',
]
packages_bloat = ['rspamd']
clamav_packages = ['clamav', 'clamav-daemon']
clamav_daemons = ['clamav-daemon', 'clamav-freshclam']
@ -47,10 +27,6 @@ port_info = {
'dovecot': ('imaps', 993, 'pop3s', 995),
}
managed_services = ['postfix', 'dovecot', 'rspamd']
managed_packages = packages + packages_bloat
_description = [
_('<a href="/plinth/apps/roundcube/">Roundcube app</a> provides web '
'interface for users to access email.'),
@ -67,10 +43,30 @@ class EmailServerApp(plinth.app.App):
app_id = 'email_server'
app_name = _('Email Server')
_version = 1
def __init__(self):
"""The app's constructor"""
super().__init__()
self._add_ui_components()
# Other likely install conflicts have been discarded:
# - msmtp, nullmailer, sendmail don't cause install faults.
# - qmail and smail are missing in Bullseye (Not tested,
# but less likely due to that).
packages = Packages(
'packages-email-server', [
'postfix-ldap', 'postfix-sqlite', 'dovecot-pop3d',
'dovecot-imapd', 'dovecot-ldap', 'dovecot-lmtpd',
'dovecot-managesieved'
], conflicts=['exim4-base', 'exim4-config', 'exim4-daemon-light'],
conflicts_action=Packages.ConflictsAction.IGNORE)
self.add(packages)
packages = Packages('packages-email-server-skip-rec', ['rspamd'],
skip_recommends=True)
self.add(packages)
self._add_daemons()
self._add_firewall_ports()
@ -91,7 +87,7 @@ class EmailServerApp(plinth.app.App):
def _add_ui_components(self):
info = plinth.app.Info(
app_id=self.app_id, version=version, name=self.app_name,
app_id=self.app_id, version=self._version, name=self.app_name,
short_description=_('Powered by Postfix, Dovecot & Rspamd'),
description=_description, manual_page='EmailServer',
clients=manifest.clients,
@ -108,7 +104,7 @@ class EmailServerApp(plinth.app.App):
self.add(menu_item)
def _add_daemons(self):
for srvname in managed_services:
for srvname in ['postfix', 'dovecot', 'rspamd']:
# Construct `listen_ports` parameter for the daemon
mixed = port_info.get(srvname, ())
port_numbers = [v for v in mixed if isinstance(v, int)]
@ -131,10 +127,15 @@ class EmailServerApp(plinth.app.App):
ports=all_port_names, is_external=True)
self.add(firewall)
@staticmethod
def post_init():
"""Perform post initialization operations."""
domain_added.connect(on_domain_added)
domain_removed.connect(on_domain_removed)
def diagnose(self):
"""Run diagnostics and return the results"""
results = super().diagnose()
results.extend([r.summarize() for r in audit.domain.get()])
results.extend([r.summarize() for r in audit.ldap.get()])
results.extend([r.summarize() for r in audit.spam.get()])
results.extend([r.summarize() for r in audit.tls.get()])
@ -152,7 +153,8 @@ def setup(helper, old_version=None):
"""Installs and configures module"""
def _clear_conflicts():
packages_to_remove = packages_installed(package_conflicts)
component = app.get_component('packages-email-server')
packages_to_remove = component.find_conflicts()
if packages_to_remove:
logger.info('Removing conflicting packages: %s',
packages_to_remove)
@ -160,20 +162,37 @@ def setup(helper, old_version=None):
# Install
helper.call('pre', _clear_conflicts)
helper.install(packages)
helper.install(packages_bloat, skip_recommends=True)
app.setup(old_version)
# Setup
helper.call('post', audit.home.repair)
helper.call('post', audit.domain.repair)
helper.call('post', audit.domain.set_domains)
helper.call('post', audit.ldap.repair)
helper.call('post', audit.spam.repair)
helper.call('post', audit.tls.repair)
helper.call('post', audit.rcube.repair)
# Reload
for srvname in managed_services:
actions.superuser_run('service', ['reload', srvname])
actions.superuser_run('service', ['reload', 'postfix'])
actions.superuser_run('service', ['reload', 'dovecot'])
actions.superuser_run('service', ['reload', 'rspamd'])
# Expose to public internet
helper.call('post', app.enable)
def on_domain_added(sender, domain_type, name, description='', services=None,
**kwargs):
"""Handle addition of a new domain."""
if app.needs_setup():
return
audit.domain.set_domains()
def on_domain_removed(sender, domain_type, name, **kwargs):
"""Handle removal of a domain."""
if app.needs_setup():
return
audit.domain.set_domains()

View File

@ -1,317 +1,57 @@
"""Configure email domains"""
# SPDX-License-Identifier: AGPL-3.0-or-later
import io
import json
import os
import pathlib
import re
import select
import subprocess
import sys
import time
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from plinth.actions import superuser_run
from plinth.errors import ActionError
from plinth.modules.config import get_domainname
from plinth.modules.email_server import interproc, postconf
from . import models
EXIT_VALIDATION = 40
managed_keys = ['_mailname', 'mydomain', 'myhostname', 'mydestination']
from plinth.modules import config
from plinth.modules.email_server import postconf
from plinth.modules.names.components import DomainName
class ClientError(RuntimeError):
pass
def get():
translation_table = [
(check_postfix_domains, _('Postfix domain name config')),
]
results = []
with postconf.mutex.lock_all():
for check, title in translation_table:
results.append(check(title))
return results
def repair():
superuser_run('email_server', ['domain', 'set_up'])
def repair_component(action_name):
allowed_actions = {'set_up': ['postfix']}
if action_name not in allowed_actions:
return
superuser_run('email_server', ['domain', action_name])
return allowed_actions[action_name]
def action_set_up():
with postconf.mutex.lock_all():
fix_postfix_domains(check_postfix_domains())
def check_postfix_domains(title=''):
diagnosis = models.MainCfDiagnosis(title, action='set_up')
domain = get_domainname() or 'localhost'
postconf_keys = (k for k in managed_keys if not k.startswith('_'))
conf = postconf.get_many_unsafe(postconf_keys, flag='-x')
dest_set = set(postconf.parse_maps(conf['mydestination']))
deletion_set = set()
temp = _amend_mailname(domain)
if temp is not None:
diagnosis.error('Set /etc/mailname to %s', temp)
diagnosis.flag('_mailname', temp)
# Amend $mydomain and conf['mydomain']
temp = _amend_mydomain(conf['mydomain'], domain)
if temp is not None:
diagnosis.error('Set $mydomain to %s', temp)
diagnosis.flag('mydomain', temp)
deletion_set.add(conf['mydomain'])
conf['mydomain'] = temp
# Amend $myhostname and conf['myhostname']
temp = _amend_myhostname(conf['myhostname'], conf['mydomain'])
if temp is not None:
diagnosis.error('Set $myhostname to %s', temp)
diagnosis.flag('myhostname', temp)
deletion_set.add(conf['myhostname'])
conf['myhostname'] = temp
# Delete old domain names
deletion_set.discard('localhost')
dest_set.difference_update(deletion_set)
# Amend $mydestination
temp = _amend_mydestination(dest_set, conf['mydomain'], conf['myhostname'],
diagnosis.error)
if temp is not None:
diagnosis.flag('mydestination', temp)
elif len(deletion_set) > 0:
corrected_value = ', '.join(sorted(dest_set))
diagnosis.error('Update $mydestination')
diagnosis.flag('mydestination', corrected_value)
return diagnosis
def _amend_mailname(domain):
with open('/etc/mailname', 'r') as fd:
mailname = fd.readline().strip()
# If mailname is not localhost, refresh it
if mailname != 'localhost':
temp = _change_to_domain_name(mailname, domain, False)
if temp != mailname:
return temp
return None
def _amend_mydomain(conf_value, domain):
temp = _change_to_domain_name(conf_value, domain, False)
if temp != conf_value:
return temp
return None
def _amend_myhostname(conf_value, mydomain):
if conf_value != mydomain:
if not conf_value.endswith('.' + mydomain):
return mydomain
return None
def _amend_mydestination(dest_set, mydomain, myhostname, error):
addition_set = set()
if mydomain not in dest_set:
error('Value of $mydomain is not in $mydestination')
addition_set.add('$mydomain')
addition_set.add('$myhostname')
if myhostname not in dest_set:
error('Value of $myhostname is not in $mydestination')
addition_set.add('$mydomain')
addition_set.add('$myhostname')
if 'localhost' not in dest_set:
error('localhost is not in $mydestination')
addition_set.add('localhost')
if addition_set:
addition_set.update(dest_set)
return ', '.join(sorted(addition_set))
return None
def _change_to_domain_name(value, domain, allow_old_fqdn):
# Detect invalid values
if not value or '.' not in value:
return domain
if not allow_old_fqdn and value != domain:
return domain
else:
return value
def fix_postfix_domains(diagnosis):
diagnosis.apply_changes(_apply_domain_changes)
def _apply_domain_changes(conf_dict):
for key, value in conf_dict.items():
if key.startswith('_'):
update = globals()['su_set' + key]
update(value)
post = {k: v for (k, v) in conf_dict.items() if not k.startswith('_')}
postconf.set_many_unsafe(post)
def get_domain_config():
def get_domains():
"""Return the current domain configuration."""
postconf_keys = [key for key in managed_keys if not key.startswith('_')]
config = postconf.get_many(postconf_keys)
config['_mailname'] = pathlib.Path('/etc/mailname').read_text().strip()
return config
conf = postconf.get_many(['mydomain', 'mydestination'])
domains = set(postconf.parse_maps(conf['mydestination']))
defaults = {'$myhostname', 'localhost.$mydomain', 'localhost'}
domains.difference_update(defaults)
return {'primary_domain': conf['mydomain'], 'all_domains': domains}
def set_keys(raw):
# Serialize the keys we know
config_dict = {k: v for (k, v) in raw.items() if k in managed_keys}
if not config_dict:
raise ClientError('To update a key, specify a new value')
def set_domains(primary_domain=None):
"""Set the primary domain and all the domains for postfix. """
all_domains = DomainName.list_names()
if not primary_domain:
primary_domain = get_domains()['primary_domain']
if primary_domain not in all_domains:
primary_domain = config.get_domainname() or list(all_domains)[0]
ipc = b'%s\n' % json.dumps(config_dict).encode('utf8')
if len(ipc) > 4096:
raise ClientError('POST data exceeds max line length')
try:
superuser_run('email_server', ['domain', 'set_keys'], input=ipc)
except ActionError as e:
stdout = e.args[1]
if not stdout.startswith('ClientError:'):
raise RuntimeError('Action script failure') from e
else:
raise ValidationError(stdout) from e
superuser_run(
'email_server',
['domain', 'set_domains', primary_domain, ','.join(all_domains)])
def action_set_keys():
try:
_action_set_keys()
except ClientError as e:
print('ClientError:', end=' ')
print(e.args[0])
sys.exit(EXIT_VALIDATION)
def action_set_domains(primary_domain, all_domains):
"""Set the primary domain and all the domains for postfix. """
all_domains = [_clean_domain(domain) for domain in all_domains.split(',')]
primary_domain = _clean_domain(primary_domain)
defaults = {'$myhostname', 'localhost.$mydomain', 'localhost'}
all_domains = set(all_domains).union(defaults)
conf = {
'myhostname': primary_domain,
'mydomain': primary_domain,
'mydestination': ', '.join(all_domains)
}
postconf.set_many(conf)
pathlib.Path('/etc/mailname').write_text(primary_domain + '\n')
subprocess.run(['systemctl', 'try-reload-or-restart', 'postfix'],
check=True)
def _action_set_keys():
line = _stdin_readline()
if not line.startswith('{') or not line.endswith('}\n'):
raise ClientError('Bad stdin data')
clean_dict = {}
# Input validation
for key, value in json.loads(line).items():
if key not in managed_keys:
raise ClientError('Key not allowed: %r' % key)
if not isinstance(value, str):
raise ClientError('Bad value type from key: %r' % key)
clean_function = globals()['clean_' + key.lstrip('_')]
clean_dict[key] = clean_function(value)
# Apply changes (postconf)
postconf_dict = dict(
filter(lambda kv: not kv[0].startswith('_'), clean_dict.items()))
postconf.set_many(postconf_dict)
# Apply changes (special)
for key, value in clean_dict.items():
if key.startswith('_'):
set_function = globals()['su_set' + key]
set_function(value)
# Important: reload postfix after acquiring lock
with postconf.mutex.lock_all():
# systemctl reload postfix
args = ['systemctl', 'reload', 'postfix']
completed = subprocess.run(args, capture_output=True, check=False)
if completed.returncode != 0:
interproc.log_subprocess(completed)
raise OSError('Could not reload postfix')
def clean_mailname(mailname):
mailname = mailname.lower().strip()
if not re.match('^[a-z0-9-\\.]+$', mailname):
raise ClientError('Invalid character in host/domain/mail name')
# XXX: need more verification
return mailname
def clean_mydomain(raw):
return clean_mailname(raw)
def clean_myhostname(raw):
return clean_mailname(raw)
def clean_mydestination(raw):
ascii_code = (ord(c) for c in raw)
valid = all(32 <= a <= 126 for a in ascii_code)
if not valid:
raise ClientError('Bad input for $mydestination')
else:
return raw
def su_set_mailname(cleaned):
with interproc.atomically_rewrite('/etc/mailname') as fd:
fd.write(cleaned)
fd.write('\n')
def _stdin_readline():
membuf = io.BytesIO()
bytes_saved = 0
fd = sys.stdin.buffer
time_started = time.monotonic()
# Reading stdin with timeout
# https://stackoverflow.com/a/21429655
os.set_blocking(fd.fileno(), False)
while bytes_saved < 4096:
rlist, wlist, xlist = select.select([fd], [], [], 1.0)
if fd in rlist:
data = os.read(fd.fileno(), 4096)
membuf.write(data)
bytes_saved += len(data)
if len(data) == 0 or b'\n' in data: # end of file or line
break
if time.monotonic() - time_started > 5:
raise TimeoutError()
# Read a line
membuf.seek(0)
line = membuf.readline()
if not line.endswith(b'\n'):
raise ClientError('Line was too long')
try:
return line.decode('utf8')
except ValueError as e:
raise ClientError('UTF-8 decode failed') from e
def _clean_domain(domain):
domain = domain.lower().strip()
assert re.match('^[a-z0-9-\\.]+$', domain)
return domain

View File

@ -9,6 +9,8 @@ from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from plinth.modules.names.components import DomainName
from . import aliases as aliases_module
domain_validator = RegexValidator(r'^[A-Za-z0-9-\.]+$',
@ -25,27 +27,20 @@ class EmailServerForm(forms.Form):
super().__init__(*args, **kwargs)
class DomainsForm(forms.Form):
_mailname = forms.CharField(required=True, strip=True,
validators=[domain_validator])
mydomain = forms.CharField(required=True, strip=True,
validators=[domain_validator])
myhostname = forms.CharField(required=True, strip=True,
validators=[domain_validator])
mydestination = forms.CharField(required=True, strip=True,
validators=[destination_validator])
def _get_domain_choices():
"""Double domain entries for inclusion in the choice field."""
return ((domain.name, domain.name) for domain in DomainName.list())
def clean(self):
"""Convert values to lower case."""
data = self.cleaned_data
if '_mailname' in data:
data['_mailname'] = data['_mailname'].lower()
if 'myhostname' in data:
data['myhostname'] = data['myhostname'].lower()
if 'mydestination' in data:
data['mydestination'] = data['mydestination'].lower()
class DomainForm(forms.Form):
primary_domain = forms.ChoiceField(
choices=_get_domain_choices,
label=_('Primary domain'),
help_text=_(
'Mails are received for all domains configured in the system. '
'Among these, select the most important one.'),
required=True,
)
class AliasCreateForm(forms.Form):

View File

@ -11,10 +11,6 @@
{% endblock %}
{% block subsubmenu %}
<a class="btn btn-default" role="button"
href="{% url 'email_server:domains' %}">
{% trans "Domains" %}
</a>
<a class="btn btn-default" role="button" href="/rspamd/">
{% trans "Manage Spam" %}
<span class="fa fa-external-link"></span>

View File

@ -8,8 +8,6 @@ from . import views
urlpatterns = [
path('apps/email_server/', views.EmailServerView.as_view(), name='index'),
path('apps/email_server/domains', views.DomainsView.as_view(),
name='domains'),
path('apps/email_server/my_aliases',
non_admin_view(views.AliasView.as_view()), name='aliases'),
path('apps/email_server/config.xml', public(views.XmlView.as_view())),

View File

@ -44,16 +44,25 @@ class ExceptionsMixin(View):
class EmailServerView(ExceptionsMixin, AppView):
"""Server configuration page"""
app_id = 'email_server'
form_class = forms.DomainForm
template_name = 'email_server.html'
audit_modules = ('domain', 'tls', 'rcube')
audit_modules = ('tls', 'rcube')
def get_initial(self):
"""Return the initial values to populate in the form."""
initial = super().get_initial()
domains = audit.domain.get_domains()
initial['primary_domain'] = domains['primary_domain']
return initial
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
dlist = []
for module_name in self.audit_modules:
self._get_audit_results(module_name, dlist)
dlist.sort(key=audit.models.Diagnosis.sorting_key)
context = super().get_context_data(*args, **kwargs)
context['related_diagnostics'] = dlist
return context
@ -75,12 +84,29 @@ class EmailServerView(ExceptionsMixin, AppView):
def post(self, request):
repair_field = request.POST.get('repair')
module_name, sep, action_name = repair_field.partition('.')
if not sep or module_name not in self.audit_modules:
return HttpResponseBadRequest('Bad post data')
if repair_field:
module_name, sep, action_name = repair_field.partition('.')
if not sep or module_name not in self.audit_modules:
return HttpResponseBadRequest('Bad post data')
self._repair(module_name, action_name)
return redirect(request.path)
self._repair(module_name, action_name)
return redirect(request.path)
return super().post(request)
def form_valid(self, form):
"""Update the settings for changed domain values."""
old_data = form.initial
new_data = form.cleaned_data
if old_data['primary_domain'] != new_data['primary_domain']:
try:
audit.domain.set_domains(new_data['primary_domain'])
messages.success(self.request, _('Configuration updated'))
except Exception:
messages.success(self.request,
_('An error occurred during configuration.'))
return super().form_valid(form)
def _repair(self, module_name, action_name):
"""Repair the configuration of the given audit module."""
@ -180,47 +206,6 @@ class AliasView(FormView):
aliases_module.put(username, form.cleaned_data['alias'])
class DomainsView(FormView):
"""View to allow editing domain related settings."""
template_name = 'form.html'
form_class = forms.DomainsForm
prefix = 'domain'
success_url = reverse_lazy('email_server:domains')
def get_initial(self):
"""Return the initial values to populate in the form."""
initial = super().get_initial()
initial.update(audit.domain.get_domain_config())
return initial
def get_context_data(self, **kwargs):
"""Add the title to the document."""
context = super().get_context_data(**kwargs)
context['title'] = _('Domains')
return context
def form_valid(self, form):
"""Update the settings for changed domain values."""
old_data = form.initial
new_data = form.cleaned_data
config = {}
for key in form.initial:
if old_data[key] != new_data[key]:
config[key] = new_data[key]
if config:
try:
audit.domain.set_keys(config)
messages.success(self.request, _('Configuration updated'))
except Exception:
messages.success(self.request,
_('Error updating configuration'))
else:
messages.info(self.request, _('Setting unchanged'))
return super().form_valid(form)
class XmlView(TemplateView):
template_name = 'email_autoconfig.xml'

View File

@ -21,14 +21,6 @@ from . import manifest
gio = import_from_gi('Gio', '2.0')
glib = import_from_gi('GLib', '2.0')
version = 2
is_essential = True
managed_packages = ['firewalld', 'nftables']
managed_services = ['firewalld']
_description = [
format_lazy(
_('Firewall is a security system that controls the incoming and '
@ -58,14 +50,16 @@ class FirewallApp(app_module.App):
app_id = 'firewall'
_version = 2
can_be_disabled = False
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential, name=_('Firewall'),
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True, name=_('Firewall'),
icon='fa-shield', description=_description,
manual_page='Firewall')
self.add(info)
@ -74,10 +68,10 @@ class FirewallApp(app_module.App):
'firewall:index', parent_url_name='system')
self.add(menu_item)
packages = Packages('packages-firewall', managed_packages)
packages = Packages('packages-firewall', ['firewalld', 'nftables'])
self.add(packages)
daemon = Daemon('daemon-firewall', managed_services[0])
daemon = Daemon('daemon-firewall', 'firewalld')
self.add(daemon)
backup_restore = BackupRestore('backup-restore-firewall',
@ -98,7 +92,7 @@ def _run_setup():
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
_run_setup()
@ -107,9 +101,9 @@ def force_upgrade(helper, packages):
if 'firewalld' not in packages:
return False
# firewalld 0.6.x -> 0.7.x, 0.6.x -> 0.8.x, 0.7.x -> 0.8.x
# firewalld 0.6.x -> 0.7.x, 0.6.x -> 0.8.x, 0.7.x -> 0.8.x, 0.9.x -> 1.0.x
package = packages['firewalld']
if Version(package['current_version']) >= Version('0.9') or \
if Version(package['current_version']) >= Version('1.0') or \
Version(package['new_version']) < Version('0.7'):
return False

View File

@ -5,16 +5,14 @@ FreedomBox app for first boot wizard.
import operator
import os
import sys
from django.urls import reverse
from plinth import app, cfg, module_loader
from plinth import app as app_module
from plinth import cfg
from plinth.signals import post_setup
version = 1
is_essential = True
first_boot_steps = [
{
'id': 'firstboot_welcome',
@ -34,14 +32,23 @@ _all_first_boot_steps = None
_is_completed = None
class FirstBootApp(app.App):
class FirstBootApp(app_module.App):
"""FreedomBox app for First Boot."""
app_id = 'first_boot'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True)
self.add(info)
def post_init(self):
"""Perform post initialization operations."""
post_setup.connect(_clear_first_boot_steps)
@ -71,11 +78,11 @@ def _get_steps():
return _all_first_boot_steps
steps = []
modules = module_loader.loaded_modules
for module_object in modules.values():
if getattr(module_object, 'first_boot_steps', None):
if module_object.setup_helper.get_state() != 'needs-setup':
steps.extend(module_object.first_boot_steps)
for app in app_module.App.list():
module = sys.modules[app.__module__]
if getattr(module, 'first_boot_steps', None):
if not app.needs_setup():
steps.extend(module.first_boot_steps)
_all_first_boot_steps = sorted(steps, key=operator.itemgetter('order'))
return _all_first_boot_steps

View File

@ -22,10 +22,6 @@ from . import manifest
from .forms import is_repo_url
from .manifest import GIT_REPO_PATH
version = 1
managed_packages = ['gitweb', 'highlight']
_description = [
_('Git is a distributed version-control system for tracking changes in '
'source code during software development. Gitweb provides a web '
@ -46,6 +42,8 @@ class GitwebApp(app_module.App):
app_id = 'gitweb'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
@ -54,7 +52,7 @@ class GitwebApp(app_module.App):
self.repos = []
info = app_module.Info(app_id=self.app_id, version=version,
info = app_module.Info(app_id=self.app_id, version=self._version,
name=_('Gitweb'), icon_filename='gitweb',
short_description=_('Simple Git Hosting'),
description=_description, manual_page='GitWeb',
@ -74,7 +72,7 @@ class GitwebApp(app_module.App):
allowed_groups=list(groups))
self.add(shortcut)
packages = Packages('packages-gitweb', managed_packages)
packages = Packages('packages-gitweb', ['gitweb', 'highlight'])
self.add(packages)
firewall = Firewall('firewall-gitweb', info.name,
@ -99,8 +97,7 @@ class GitwebApp(app_module.App):
def post_init(self):
"""Perform post initialization operations."""
setup_helper = globals()['setup_helper']
if setup_helper.get_state() != 'needs-setup':
if not self.needs_setup():
self.update_service_access()
def set_shortcut_login_required(self, login_required):
@ -161,7 +158,7 @@ class GitwebBackupRestore(BackupRestore):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call('post', actions.superuser_run, 'gitweb', ['setup'])
helper.call('post', app.enable)

View File

@ -11,9 +11,6 @@ from django.utils.translation import pgettext_lazy
from plinth import app as app_module
from plinth import cfg, menu, web_server
version = 1
is_essential = True
app = None
@ -22,12 +19,14 @@ class HelpApp(app_module.App):
app_id = 'help'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
is_essential=is_essential)
info = app_module.Info(app_id=self.app_id, version=self._version,
is_essential=True)
self.add(info)
menu_item = menu.Menu('menu-help', _('Documentation'), None, 'fa-book',

View File

@ -18,14 +18,6 @@ from plinth.package import Packages
from . import manifest
version = 1
service_name = 'i2p'
managed_services = [service_name]
managed_packages = ['i2p']
_description = [
_('The Invisible Internet Project is an anonymous network layer intended '
'to protect communication from censorship and surveillance. I2P '
@ -51,6 +43,8 @@ class I2PApp(app_module.App):
app_id = 'i2p'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
@ -58,7 +52,7 @@ class I2PApp(app_module.App):
groups = {'i2p': _('Manage I2P application')}
info = app_module.Info(
app_id=self.app_id, version=version, name=_('I2P'),
app_id=self.app_id, version=self._version, name=_('I2P'),
icon_filename='i2p', short_description=_('Anonymity Network'),
description=_description, manual_page='I2P',
clients=manifest.clients,
@ -78,7 +72,7 @@ class I2PApp(app_module.App):
allowed_groups=list(groups))
self.add(shortcut)
packages = Packages('packages-i2p', managed_packages)
packages = Packages('packages-i2p', ['i2p'])
self.add(packages)
firewall = Firewall('firewall-i2p-web', info.name,
@ -94,8 +88,7 @@ class I2PApp(app_module.App):
urls=['https://{host}/i2p/'])
self.add(webserver)
daemon = Daemon('daemon-i2p', managed_services[0],
listen_ports=[(7657, 'tcp6')])
daemon = Daemon('daemon-i2p', 'i2p', listen_ports=[(7657, 'tcp6')])
self.add(daemon)
users_and_groups = UsersAndGroups('users-and-groups-i2p',
@ -108,7 +101,7 @@ class I2PApp(app_module.App):
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
app.setup(old_version)
helper.call('post', app.disable)
# Add favorites to the configuration

Some files were not shown because too many files have changed in this diff Show More