mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-18 09:10:49 +00:00
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:
commit
7bed7bfea3
@ -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():
|
||||
|
||||
@ -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(_):
|
||||
|
||||
@ -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(_):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
66
debian/changelog
vendored
@ -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.
|
||||
|
||||
2
debian/tests/control
vendored
2
debian/tests/control
vendored
@ -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
|
||||
|
||||
#
|
||||
|
||||
@ -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.
|
||||
@ -14,7 +14,6 @@ and are updated when the API is updated.
|
||||
|
||||
app
|
||||
components/index
|
||||
app_module
|
||||
actions
|
||||
action_utils
|
||||
views
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 |
@ -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
|
||||
|
||||
@ -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 |
@ -3,4 +3,4 @@
|
||||
Package init file.
|
||||
"""
|
||||
|
||||
__version__ = '21.14.1'
|
||||
__version__ = '21.15'
|
||||
|
||||
@ -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()
|
||||
|
||||
163
plinth/app.py
163
plinth/app.py
@ -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")
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)])
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 = \
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -7,5 +7,5 @@ backup = {
|
||||
'config': {
|
||||
'files': ['/etc/bind/named.conf.options']
|
||||
},
|
||||
'services': ['bind9']
|
||||
'services': ['named']
|
||||
}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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'])
|
||||
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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 '
|
||||
|
||||
@ -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>.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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())),
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user