mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
freedombox Debian release 24.5
-----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmXdU4cWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICMstD/9zF868IP5XxLe2+ZV8QNkHeBpX ElOWKA6uK7jrcR39oMX7JJnRDWFNE3IG44f3GWDfKQ8Vpann3XNGhcRvR9rhlsq6 TEU9y518WsCLvfkFZ7TVOrx3CVXpBzrJClt3kvIGoT/UB4z2IQ4p5Vsuei1TiD1K R5hBBQBSTDxKIIqv+whHLaaWnUI/d2vlZeZuddfI8BPT2juhvOTxlaLlMIxXdzhU IMbsFMbOaFy9+F/OyWs/euLNHhGoNEwqruFL/JuACBY5UtfQpnyHozY7dq/T09Ho 6djoLpvhVK808Vx6tNicXeC1XpARsQ6brBPasB96A7q9rYuDY++QP9Mexj7YLZJ/ Q4TRAKsRGtIMbg411QoRO5bQqag67PLZ/b+0UwJDgwbQrzsnKI8iC4LOrxvU2rMI Uo1yWcnFxJZ8P7xZ+DStQU830ssddm78w38ja2R8qoNdxUS/RODz9uZxLSNoF9+v RKt6pf72SlJXXOiZdTgNIiuh2o2n4J/SdKxZtjeFeqHS9+5HoD4LgA28rE5ElGTQ fP5KGzp+X6v6TcvgIbhJ1yX2SEm5/SiuAnmPwGQ5ItjNh9SXPEU/7ekt7u5iDkf4 x7ZyD43dmk8y6cO4v6zQFOqCyd7m0+hgFv5UwDonpOd2e6CqI7xQzMyWFofZdQbv PseKweEOq9y0frebYg== =iF1H -----END PGP SIGNATURE----- gpgsig -----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmXjWscWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICL/ZD/oC5JkU8BA6lIbhxoP4Z4Vgg5Dw hL9Gbh0VPyV87ZNChxvp9JUFiVrrj44WMydtv8KE+GD5Dc8byp2Ipr4cPtAcMy36 f40DSbxPwD59ku+BlyOhQpCk5C0wgG0hageWFg27X77FTedNo7PQGpVryrZY/MTY aM9bL5hmlJoFvOoC6oqiF11yRdZJ24cUNSIn/JXcBs7bMhepW/6y/sDb719ZBHwG QtTPMYSWlrKsUIeKYkO8v3rBht2gGw851PhwmnqZ8oTEhnYc6XU/kdcyon6hFEiG fDSEw5X3OwJdxzR7CTXPD7qN2lZsA84GS7iyNafEDVa6hZmAV88T1xbD0+QoPz+V 6S149azfG7QFMx/UVs5Cpf73pIKZvRH1oxHjhX55DOWk9a0B4IAwR3sRFFsfZPsd A0QL6A9qmdoT7+eWy9VTqyqkKFMm0hbyTVNDIemxhVkQHiUqveK1XmzdQ+RNBerL g14T1XBePwxy8oAFsY7uw4PP8QLv7vk4F4/cuCrpGYtqrU/+iTIZMp57kO15LIwT 81+L+F7KaN2XvWBdEgPB6Xt3vqbR+sr6EJmhp+4BKxevkXRtSCpBkC7GHP/p7ktO yeoz/4HtI6q3o+1mqbllR7a4tBEM8qc+D5CzwPz6MCGCHdQqx5cMC+xgbo5EAduT hICdutjwRgmJ4QeDnQ== =1fNa -----END PGP SIGNATURE----- Merge tag 'v24.5' into debian/bookworm-backports freedombox Debian release 24.5
This commit is contained in:
commit
14d69e2cf7
@ -159,6 +159,7 @@ set -xe pipefail
|
||||
|
||||
cd /freedombox/
|
||||
|
||||
sudo apt-get -y install make
|
||||
sudo make provision-dev
|
||||
|
||||
echo 'alias freedombox-develop="cd /freedombox; sudo -u plinth /freedombox/run --develop"' \
|
||||
|
||||
27
debian/changelog
vendored
27
debian/changelog
vendored
@ -1,3 +1,30 @@
|
||||
freedombox (24.5) unstable; urgency=medium
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* container: Fix issue with missing make command on stable image
|
||||
* setup: Minor refactoring of force upgrader class instantiation
|
||||
* setup: Ensure that force upgrade won't run when app is not installed
|
||||
* setup: Ensure that apt is updated before checking force upgrade
|
||||
* firewalld: Implement force upgrading to any 2.x versions
|
||||
* backups: tests: Don't use pytest marks on fixtures
|
||||
* tor: tests: Fix issue with pytest 8.x versions
|
||||
* tor: tests: Convert to pytest style tests from class based tests
|
||||
* pyproject.toml: Exclude the build directory from mypy checks
|
||||
* gitweb, users: Minor fixes for newer pycodestyle
|
||||
* daemon: Add new component for daemons shared across apps
|
||||
* wordpress: Add shared daemon component for mariadb/mysql
|
||||
* zoph: Add shared daemon component for mariadb/mysql
|
||||
|
||||
[ James Valleroy ]
|
||||
* setup: Try force upgrade before running app setup
|
||||
* tests: Patch apps_init for enable/disable daemon test
|
||||
* doc: Fetch latest manual
|
||||
|
||||
[ Olaf Schaf ]
|
||||
* Translated using Weblate (German)
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Mon, 26 Feb 2024 20:58:45 -0500
|
||||
|
||||
freedombox (24.4~bpo12+1) bookworm-backports; urgency=medium
|
||||
|
||||
* Rebuild for bookworm-backports.
|
||||
|
||||
@ -8,3 +8,6 @@ Daemon
|
||||
|
||||
.. autoclass:: plinth.daemon.RelatedDaemon
|
||||
:members:
|
||||
|
||||
.. autoclass:: plinth.daemon.SharedDaemon
|
||||
:members:
|
||||
|
||||
@ -8,6 +8,25 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 24.5 (2024-02-26) ==
|
||||
|
||||
* backups: tests: Don't use pytest marks on fixtures
|
||||
* container: Fix issue with missing make command on stable image
|
||||
* daemon: Add new component for daemons shared across apps
|
||||
* firewalld: Implement force upgrading to any 2.x versions
|
||||
* gitweb, users: Minor fixes for newer pycodestyle
|
||||
* locale: Update translations for German
|
||||
* pyproject.toml: Exclude the build directory from mypy checks
|
||||
* setup: Ensure that apt is updated before checking force upgrade
|
||||
* setup: Ensure that force upgrade won't run when app is not installed
|
||||
* setup: Minor refactoring of force upgrader class instantiation
|
||||
* setup: Try force upgrade before running app setup
|
||||
* tests: Patch apps_init for enable/disable daemon test
|
||||
* tor: tests: Convert to pytest style tests from class based tests
|
||||
* tor: tests: Fix issue with pytest 8.x versions
|
||||
* wordpress: Add shared daemon component for mariadb/mysql
|
||||
* zoph: Add shared daemon component for mariadb/mysql
|
||||
|
||||
== FreedomBox 24.4 (2024-02-12) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -8,6 +8,25 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 24.5 (2024-02-26) ==
|
||||
|
||||
* backups: tests: Don't use pytest marks on fixtures
|
||||
* container: Fix issue with missing make command on stable image
|
||||
* daemon: Add new component for daemons shared across apps
|
||||
* firewalld: Implement force upgrading to any 2.x versions
|
||||
* gitweb, users: Minor fixes for newer pycodestyle
|
||||
* locale: Update translations for German
|
||||
* pyproject.toml: Exclude the build directory from mypy checks
|
||||
* setup: Ensure that apt is updated before checking force upgrade
|
||||
* setup: Ensure that force upgrade won't run when app is not installed
|
||||
* setup: Minor refactoring of force upgrader class instantiation
|
||||
* setup: Try force upgrade before running app setup
|
||||
* tests: Patch apps_init for enable/disable daemon test
|
||||
* tor: tests: Convert to pytest style tests from class based tests
|
||||
* tor: tests: Fix issue with pytest 8.x versions
|
||||
* wordpress: Add shared daemon component for mariadb/mysql
|
||||
* zoph: Add shared daemon component for mariadb/mysql
|
||||
|
||||
== FreedomBox 24.4 (2024-02-12) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
Package init file.
|
||||
"""
|
||||
|
||||
__version__ = '24.4'
|
||||
__version__ = '24.5'
|
||||
|
||||
@ -135,6 +135,41 @@ class RelatedDaemon(app.FollowerComponent):
|
||||
self.unit = unit
|
||||
|
||||
|
||||
class SharedDaemon(Daemon):
|
||||
"""Component to manage a daemon that is used by multiple apps.
|
||||
|
||||
Daemons such as a database server are a hard requirement for an app.
|
||||
However, there may be multiple apps using that server. This component
|
||||
ensures that server is enabled and running when app is enabled. It runs
|
||||
diagnostics on the daemon when app is diagnosed. The primary difference
|
||||
from the Daemon component is that when the app is disabled the daemon must
|
||||
only be disabled if there is no other app using this daemon.
|
||||
"""
|
||||
|
||||
# A shared daemon may be running even when an app is disabled because
|
||||
# another app might be using the daemon. Hence, the enabled/disabled state
|
||||
# of this component can't be used to determine the enabled/disabled state
|
||||
# of the app.
|
||||
is_leader = False
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Do nothing. Enabled state is still determined by unit status."""
|
||||
|
||||
def disable(self):
|
||||
"""Disable the daemon iff this is the last app using the daemon."""
|
||||
other_apps_enabled = False
|
||||
for other_app in app.App.list():
|
||||
if other_app.app_id == self.app_id:
|
||||
continue
|
||||
|
||||
for component in other_app.get_components_of_type(SharedDaemon):
|
||||
if component.unit == self.unit and other_app.is_enabled():
|
||||
other_apps_enabled = True
|
||||
|
||||
if not other_apps_enabled:
|
||||
super().disable()
|
||||
|
||||
|
||||
def app_is_running(app_):
|
||||
"""Return whether all the daemons in the app are running."""
|
||||
for component in app_.components.values():
|
||||
|
||||
@ -10,8 +10,8 @@ msgstr ""
|
||||
"Project-Id-Version: FreedomBox UI\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-02-12 20:35-0500\n"
|
||||
"PO-Revision-Date: 2024-02-11 20:14+0000\n"
|
||||
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
||||
"PO-Revision-Date: 2024-02-22 14:02+0000\n"
|
||||
"Last-Translator: Olaf Schaf <pakuti123000@gmail.com>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
|
||||
"freedombox/de/>\n"
|
||||
"Language: de\n"
|
||||
@ -19,7 +19,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.4-dev\n"
|
||||
"X-Generator: Weblate 5.5-dev\n"
|
||||
|
||||
#: config.py:120
|
||||
#, python-brace-format
|
||||
@ -7042,10 +7042,8 @@ msgid "Read-only root filesystem"
|
||||
msgstr "Schreibgeschütztes Root-Dateisystem"
|
||||
|
||||
#: modules/storage/__init__.py:391
|
||||
#, fuzzy
|
||||
#| msgid "Go to Networks"
|
||||
msgid "Go to Power"
|
||||
msgstr "Zur Stromversorgung"
|
||||
msgstr "Springe zur Stromversorgung"
|
||||
|
||||
#: modules/storage/forms.py:63
|
||||
msgid "Invalid directory name."
|
||||
|
||||
@ -59,9 +59,8 @@ def fixture_create_temp_user(temp_home, password, needs_root):
|
||||
subprocess.check_call(['sudo', 'userdel', username])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('needs_sudo')
|
||||
@pytest.fixture(name='has_ssh_key', scope='module', autouse=True)
|
||||
def fixture_ssh_key(temp_home, temp_user, password, needs_root):
|
||||
def fixture_ssh_key(temp_home, temp_user, password, needs_root, needs_sudo):
|
||||
subprocess.check_call([
|
||||
'sudo', '-n', '-u', temp_user, 'ssh-keygen', '-t', 'rsa', '-b', '1024',
|
||||
'-N', '', '-f', f'{temp_home}/.ssh/id_rsa', '-q'
|
||||
|
||||
@ -87,9 +87,9 @@ class FirewallApp(app_module.App):
|
||||
if 'firewalld' not in packages:
|
||||
return False
|
||||
|
||||
# Allow upgrade from any version to any version below 2.0
|
||||
# Allow upgrade from any version to any version below 3.0
|
||||
package = packages['firewalld']
|
||||
if Version(package['new_version']) > Version('2~'):
|
||||
if Version(package['new_version']) > Version('3~'):
|
||||
return False
|
||||
|
||||
install(['firewalld'], force_configuration='new')
|
||||
|
||||
@ -53,7 +53,7 @@ def gitweb_patch():
|
||||
with patch('plinth.modules.gitweb.get_repo_list') as get_repo_list, \
|
||||
patch('plinth.app.App.get') as app_get, \
|
||||
patch(f'{privileged}.create_repo'), \
|
||||
patch(f'{privileged}.repo_exists') as repo_exists,\
|
||||
patch(f'{privileged}.repo_exists') as repo_exists, \
|
||||
patch(f'{privileged}.repo_info') as repo_info, \
|
||||
patch(f'{privileged}.rename_repo'), \
|
||||
patch(f'{privileged}.set_repo_description'), \
|
||||
|
||||
@ -10,7 +10,7 @@ from django.utils.translation import gettext_noop
|
||||
from plinth import action_utils
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, kvstore, menu
|
||||
from plinth import setup as setup_module
|
||||
from plinth import setup as setup_module_ # Not setup_module, for pytest
|
||||
from plinth.daemon import (Daemon, app_is_running, diagnose_netcat,
|
||||
diagnose_port_listening)
|
||||
from plinth.modules import torproxy
|
||||
@ -209,7 +209,7 @@ class TorApp(app_module.App):
|
||||
kvstore.set(torproxy.PREINSTALL_CONFIG_KEY, json.dumps(config))
|
||||
# This creates the operation, which will run after the current
|
||||
# operation (Tor setup) is completed.
|
||||
setup_module.run_setup_on_app('torproxy')
|
||||
setup_module_.run_setup_on_app('torproxy')
|
||||
|
||||
if not old_version:
|
||||
logger.info('Enabling Tor app')
|
||||
|
||||
@ -11,72 +11,64 @@ from django.core.exceptions import ValidationError
|
||||
from plinth.modules.tor import forms, utils
|
||||
|
||||
|
||||
class TestTor:
|
||||
"""Test cases for testing the Tor module."""
|
||||
@patch('plinth.app.App.get')
|
||||
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
||||
def test_get_status(_app_get):
|
||||
"""Test that get_status does not raise any unhandled exceptions.
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.app.App.get')
|
||||
@pytest.mark.usefixtures('needs_root', 'load_cfg')
|
||||
def test_get_status(_app_get):
|
||||
"""Test that get_status does not raise any unhandled exceptions.
|
||||
|
||||
This should work regardless of whether tor is installed, or
|
||||
/etc/tor/instances/plinth/torrc exists.
|
||||
"""
|
||||
utils.get_status()
|
||||
This should work regardless of whether tor is installed, or
|
||||
/etc/tor/instances/plinth/torrc exists.
|
||||
"""
|
||||
utils.get_status()
|
||||
|
||||
|
||||
class TestTorForm:
|
||||
"""Test whether Tor configration form works."""
|
||||
def test_bridge_validator():
|
||||
"""Test upstream bridges' form field validator."""
|
||||
validator = forms.bridges_validator
|
||||
|
||||
@staticmethod
|
||||
def test_bridge_validator():
|
||||
"""Test upstream bridges' form field validator."""
|
||||
validator = forms.bridges_validator
|
||||
# Just IP:port
|
||||
validator('73.237.165.184:9001')
|
||||
validator('73.237.165.184')
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]')
|
||||
|
||||
# Just IP:port
|
||||
validator('73.237.165.184:9001')
|
||||
validator('73.237.165.184')
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]')
|
||||
# With fingerprint
|
||||
validator('73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
|
||||
|
||||
# With fingerprint
|
||||
validator('73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
|
||||
# With transport type
|
||||
validator('obfs4 73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
|
||||
|
||||
# With transport type
|
||||
validator('obfs4 73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8')
|
||||
# With transport type and extra options
|
||||
validator('obfs4 10.1.1.1:30000 '
|
||||
'0123456789ABCDEF0123456789ABCDEF01234567 '
|
||||
'cert=A/b+1 iat-mode=0')
|
||||
|
||||
# With transport type and extra options
|
||||
validator('obfs4 10.1.1.1:30000 '
|
||||
'0123456789ABCDEF0123456789ABCDEF01234567 '
|
||||
'cert=A/b+1 iat-mode=0')
|
||||
# Leading, trailing spaces and empty lines
|
||||
validator('\n'
|
||||
' \n'
|
||||
'73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
|
||||
' \n'
|
||||
'73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
|
||||
' \n'
|
||||
'\n')
|
||||
|
||||
# Leading, trailing spaces and empty lines
|
||||
validator('\n'
|
||||
' \n'
|
||||
'73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
|
||||
' \n'
|
||||
'73.237.165.184:9001 '
|
||||
'0D04F10F497E68D2AF32375BB763EC3458A908C8'
|
||||
' \n'
|
||||
'\n')
|
||||
# Invalid number for parts
|
||||
with pytest.raises(ValidationError):
|
||||
validator(' ')
|
||||
|
||||
# Invalid number for parts
|
||||
with pytest.raises(ValidationError):
|
||||
validator(' ')
|
||||
# Invalid IP address/port
|
||||
with pytest.raises(ValidationError):
|
||||
validator('73.237.165.384:9001')
|
||||
|
||||
# Invalid IP address/port
|
||||
with pytest.raises(ValidationError):
|
||||
validator('73.237.165.384:9001')
|
||||
with pytest.raises(ValidationError):
|
||||
validator('73.237.165.184:90001')
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validator('73.237.165.184:90001')
|
||||
with pytest.raises(ValidationError):
|
||||
validator('[a2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validator('[a2001:db8:85a3:8d3:1319:8a2e:370:7348]:443')
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:90443')
|
||||
with pytest.raises(ValidationError):
|
||||
validator('[2001:db8:85a3:8d3:1319:8a2e:370:7348]:90443')
|
||||
|
||||
@ -72,9 +72,9 @@ def make_request(request, view, as_admin=True, **kwargs):
|
||||
request.user = admin_user if as_admin else user
|
||||
|
||||
with patch('plinth.modules.users.forms.is_user_admin',
|
||||
return_value=as_admin),\
|
||||
return_value=as_admin), \
|
||||
patch('plinth.modules.users.views.is_user_admin',
|
||||
return_value=as_admin),\
|
||||
return_value=as_admin), \
|
||||
patch('plinth.modules.users.views.update_session_auth_hash'):
|
||||
|
||||
response = view(request, **kwargs)
|
||||
|
||||
@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, frontpage, menu
|
||||
from plinth.config import DropinConfigs
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.daemon import Daemon, SharedDaemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
@ -106,6 +106,9 @@ class WordPressApp(app_module.App):
|
||||
daemon = Daemon('daemon-wordpress', 'wordpress-freedombox.timer')
|
||||
self.add(daemon)
|
||||
|
||||
daemon = SharedDaemon('shared-daemon-wordpress-mysql', 'mysql')
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = WordPressBackupRestore('backup-restore-wordpress',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, frontpage, menu
|
||||
from plinth.config import DropinConfigs
|
||||
from plinth.daemon import SharedDaemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
@ -89,6 +90,9 @@ class ZophApp(app_module.App):
|
||||
webserver = Webserver('webserver-zoph-freedombox', 'zoph-freedombox')
|
||||
self.add(webserver)
|
||||
|
||||
daemon = SharedDaemon('shared-daemon-zoph-mysql', 'mysql')
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = ZophBackupRestore('backup-restore-zoph',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
@ -26,8 +26,6 @@ _is_first_setup = False
|
||||
is_first_setup_running = False
|
||||
_is_shutting_down = False
|
||||
|
||||
_force_upgrader = None
|
||||
|
||||
|
||||
def run_setup_on_app(app_id, allow_install=True, rerun=False):
|
||||
"""Execute the setup process in a thread."""
|
||||
@ -59,6 +57,15 @@ def _run_setup_on_app(app, current_version):
|
||||
message = None
|
||||
try:
|
||||
current_version = app.get_setup_version()
|
||||
|
||||
if current_version != 0:
|
||||
# Check if this app needs force_upgrade. If it is needed, but not
|
||||
# yet supported for the new version of the package, then an
|
||||
# exception will be raised, so that we do not run setup.
|
||||
package.refresh_package_lists()
|
||||
force_upgrader = ForceUpgrader.get_instance()
|
||||
force_upgrader.attempt_upgrade_for_app(app.app_id)
|
||||
|
||||
app.setup(old_version=current_version)
|
||||
app.set_setup_version(app.info.version)
|
||||
post_setup.send_robust(sender=app.__class__, module_name=app.app_id)
|
||||
@ -143,8 +150,8 @@ def stop():
|
||||
global _is_shutting_down
|
||||
_is_shutting_down = True
|
||||
|
||||
if _force_upgrader:
|
||||
_force_upgrader.shutdown()
|
||||
force_upgrader = ForceUpgrader.get_instance()
|
||||
force_upgrader.shutdown()
|
||||
|
||||
|
||||
def setup_apps(app_ids=None, essential=False, allow_install=True):
|
||||
@ -229,8 +236,8 @@ def _get_apps_for_regular_setup():
|
||||
1. essential apps that are not up-to-date
|
||||
2. non-essential app that are installed and need updates
|
||||
"""
|
||||
if (app.info.is_essential and
|
||||
app.get_setup_state() != app_module.App.SetupState.UP_TO_DATE):
|
||||
if (app.info.is_essential and app.get_setup_state()
|
||||
!= app_module.App.SetupState.UP_TO_DATE):
|
||||
return True
|
||||
|
||||
if app.get_setup_state() == app_module.App.SetupState.NEEDS_UPDATE:
|
||||
@ -335,6 +342,7 @@ class ForceUpgrader():
|
||||
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_run_lock = threading.Lock()
|
||||
_wait_event = threading.Event()
|
||||
|
||||
@ -349,6 +357,14 @@ class ForceUpgrader():
|
||||
"""Raised when upgrade fails and there is nothing more we wish to do.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Return a single instance of a the class."""
|
||||
if not cls._instance:
|
||||
cls._instance = ForceUpgrader()
|
||||
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the force upgrader."""
|
||||
if plinth.cfg.develop:
|
||||
@ -401,7 +417,7 @@ class ForceUpgrader():
|
||||
def shutdown(self):
|
||||
"""If we are sleeping for next attempt, cancel it.
|
||||
|
||||
If we are actually upgrading packages, don nothing.
|
||||
If we are actually upgrading packages, do nothing.
|
||||
"""
|
||||
self._wait_event.set()
|
||||
|
||||
@ -448,6 +464,50 @@ class ForceUpgrader():
|
||||
if need_retry:
|
||||
raise self.TemporaryFailure('Some apps failed to force upgrade.')
|
||||
|
||||
def attempt_upgrade_for_app(self, app_id):
|
||||
"""Attempt to perform an upgrade for specified app.
|
||||
|
||||
Raise TemporaryFailure if upgrade can't be performed now.
|
||||
|
||||
Raise PermanentFailure if upgrade can't be performed until something
|
||||
with the system state changes. We don't want to try again until
|
||||
notified of further package cache changes.
|
||||
|
||||
Return True if upgrade was performed successfully.
|
||||
|
||||
Return False if upgrade is not needed.
|
||||
|
||||
"""
|
||||
if _is_shutting_down:
|
||||
raise self.PermanentFailure('Service is shutting down')
|
||||
|
||||
if packages_privileged.is_package_manager_busy():
|
||||
raise self.TemporaryFailure('Package manager is busy')
|
||||
|
||||
apps = self._get_list_of_apps_to_force_upgrade()
|
||||
if app_id not in apps:
|
||||
logger.info('App %s does not need force upgrade', app_id)
|
||||
return False
|
||||
|
||||
packages = apps[app_id]
|
||||
app = app_module.App.get(app_id)
|
||||
try:
|
||||
logger.info('Force upgrading app: %s', app.info.name)
|
||||
if app.force_upgrade(packages):
|
||||
logger.info('Successfully force upgraded app: %s',
|
||||
app.info.name)
|
||||
return True
|
||||
else:
|
||||
logger.warning('Ignored force upgrade for app: %s',
|
||||
app.info.name)
|
||||
raise self.TemporaryFailure(
|
||||
'Force upgrade is needed, but not yet implemented for new '
|
||||
f'version of app: {app_id}')
|
||||
except Exception as exception:
|
||||
logger.exception('Error running force upgrade: %s', exception)
|
||||
raise self.TemporaryFailure(
|
||||
f'App {app_id} failed to force upgrade.')
|
||||
|
||||
def _run_force_upgrade_as_operation(self, app, packages):
|
||||
"""Start an operation for force upgrading."""
|
||||
name = gettext_noop('Updating app packages')
|
||||
@ -523,8 +583,5 @@ class ForceUpgrader():
|
||||
|
||||
def on_package_cache_updated():
|
||||
"""Called by D-Bus service when apt package cache is updated."""
|
||||
global _force_upgrader
|
||||
if not _force_upgrader:
|
||||
_force_upgrader = ForceUpgrader()
|
||||
|
||||
_force_upgrader.on_package_cache_updated()
|
||||
force_upgrader = ForceUpgrader.get_instance()
|
||||
force_upgrader.on_package_cache_updated()
|
||||
|
||||
@ -10,7 +10,7 @@ from unittest.mock import Mock, call, patch
|
||||
import pytest
|
||||
|
||||
from plinth.app import App, FollowerComponent, Info
|
||||
from plinth.daemon import (Daemon, RelatedDaemon, app_is_running,
|
||||
from plinth.daemon import (Daemon, RelatedDaemon, SharedDaemon, app_is_running,
|
||||
diagnose_netcat, diagnose_port_listening)
|
||||
from plinth.modules.diagnostics.check import DiagnosticCheck, Result
|
||||
|
||||
@ -77,10 +77,11 @@ def test_is_enabled(service_is_enabled, daemon):
|
||||
service_is_enabled.assert_has_calls([call('test-unit', strict_check=True)])
|
||||
|
||||
|
||||
@patch('plinth.app.apps_init')
|
||||
@patch('subprocess.run')
|
||||
@patch('subprocess.call')
|
||||
def test_enable(subprocess_call, subprocess_run, app_list, mock_privileged,
|
||||
daemon):
|
||||
def test_enable(subprocess_call, subprocess_run, apps_init, app_list,
|
||||
mock_privileged, daemon):
|
||||
"""Test that enabling the daemon works."""
|
||||
daemon.enable()
|
||||
subprocess_call.assert_has_calls(
|
||||
@ -101,9 +102,11 @@ def test_enable(subprocess_call, subprocess_run, app_list, mock_privileged,
|
||||
stdout=subprocess.DEVNULL, check=False)
|
||||
|
||||
|
||||
@patch('plinth.app.apps_init')
|
||||
@patch('subprocess.run')
|
||||
@patch('subprocess.call')
|
||||
def test_disable(subprocess_call, subprocess_run, mock_privileged, daemon):
|
||||
def test_disable(subprocess_call, subprocess_run, apps_init, app_list,
|
||||
mock_privileged, daemon):
|
||||
"""Test that disabling the daemon works."""
|
||||
daemon.disable()
|
||||
subprocess_call.assert_has_calls(
|
||||
@ -329,3 +332,56 @@ def test_related_daemon_initialization():
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
RelatedDaemon(None, 'test-daemon')
|
||||
|
||||
|
||||
def test_shared_daemon_leader():
|
||||
"""Test that shared daemon is not a leader component."""
|
||||
component1 = SharedDaemon('test-component1', 'test-daemon')
|
||||
assert not component1.is_leader
|
||||
|
||||
|
||||
@patch('plinth.action_utils.service_is_enabled')
|
||||
def test_shared_daemon_set_enabled(service_is_enabled):
|
||||
"""Test that enabled status is determined by unit status."""
|
||||
component = SharedDaemon('test-component', 'test-daemon')
|
||||
|
||||
service_is_enabled.return_value = False
|
||||
component.set_enabled(False)
|
||||
assert not component.is_enabled()
|
||||
component.set_enabled(True)
|
||||
assert not component.is_enabled()
|
||||
|
||||
service_is_enabled.return_value = True
|
||||
component.set_enabled(False)
|
||||
assert component.is_enabled()
|
||||
component.set_enabled(True)
|
||||
assert component.is_enabled()
|
||||
|
||||
|
||||
@patch('plinth.privileged.service.disable')
|
||||
def test_shared_daemon_disable(disable_method):
|
||||
"""Test that shared daemon disables service correctly."""
|
||||
|
||||
class AppTest2(App):
|
||||
"""Test application class."""
|
||||
app_id = 'test-app-2'
|
||||
|
||||
component1 = SharedDaemon('test-component1', 'test-daemon')
|
||||
app1 = AppTest()
|
||||
app1.add(component1)
|
||||
app1.is_enabled = Mock()
|
||||
|
||||
component2 = SharedDaemon('test-component2', 'test-daemon')
|
||||
app2 = AppTest2()
|
||||
app2.add(component2)
|
||||
|
||||
# When another app is enabled, service should not be disabled
|
||||
app1.is_enabled.return_value = True
|
||||
app2.disable()
|
||||
assert disable_method.mock_calls == []
|
||||
|
||||
# When all other apps are disabled, service should be disabled
|
||||
disable_method.reset_mock()
|
||||
app1.is_enabled.return_value = False
|
||||
app2.disable()
|
||||
assert disable_method.mock_calls == [call('test-daemon')]
|
||||
|
||||
@ -201,6 +201,9 @@ disable = [
|
||||
"too-many-ancestors", # Easy to hit when using Django
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
exclude = "build/"
|
||||
|
||||
# Ignore missing type stubs for some libraries. Try to keep this list minimal
|
||||
# and use type annotations where available.
|
||||
[[tool.mypy.overrides]]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user