mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-12 12:26:04 +00:00
freedombox Debian release 21.0
-----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAl/8/NkWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICF2pD/91D2fMUtEJhn3ZclPbQPpBtiPq O5HlUaGS10776Dipi4z6s3+bI+WtzdqlKx1Tp5G4SFcVpSUzJZPwz9E3INN+qcvM 5sorRxRmeMUlvxCsodG8YCd7Ub3VqkylkC9361FzhU6MBBZgwVG5w9jln3GrjsC9 rmt3E9FPslswsJkBCKyn9MycZkF3JyzbVZfo5uZnSrX3tFhj3Brf197q1sO7cwe5 QRpr4xlGeJYweltn4mvJm6CF65NhZXaoD0NBMSzE+mth/l7rqKg8YYdhgTIClJCn M/bq69OPe4hnfcIuk4mmJmi1/uAKttIpyKBh+vmAHphyallHNvqSvRmJ2KgHfh6c wYplnLO0+1pWBpR9YfaMmJBpHRZm6Hr8ABTkSF+SxQgSst7Xnzw0YQFdvPTv5qLe nY8gGCH2EjtjWHEULsB8UR3CpHiVcNmK7qMIj55sF7dwa6xROHWM3qdSlt5VJBrd 7Rqr5n0vSU3CbmcpoExbyHD62smAOj7PLOi6zFwA5W7xTkZMNhTxGavirdtu+fSo U4j5TEMvrSS1LHpaMAdMXmjNv5F+FZgeyLyGc/GhbGvRScPcEibqXR2bCiuePrDJ SeK3JOOZf7REpIZ8YFV+XwapA1reiYx/rt4WkoE6tbpOc+rJY5irTNnTKzAxUv9d 5Ex270lFAUxIy7juQg== =lUbE -----END PGP SIGNATURE----- gpgsig -----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmAA7cwWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICKy1D/sGlQjscBWwf+9S45aZ9GVtMj0t SyqWc7a0urZXrukqdxNYqLCNv6Q9oRpXPStrJ1ogexCJNxoCtoAERE0UfMxBre2b 8Au/1n2h1zrH8qxQycDl/qRF8JUga9kJgdn/FwvDDZh5BN/q/rG84PYT7wwk3yod fJsi5KKE5BZ8qfCuZLwsQFyCg7Wq9C3qnrI7FeXpeACozAYwcJ1vCqhSV/2YomH0 +oZm0o2rSVR7S7H4Gb/pVkf2Q1te5sS6Q+u31oJuJOt/ls2hniyndiV4fx8KrOon 4L+i4mWMEej4bWIIfEmx+7W0R9PK4ARhw9kfAKNBJRNrPoLB9lQeUdcurrDFuJ2Z Xn8Gb54n1U3qPPvfgKURkMBpYtOJAITVKTmX6QruD0DEu9rcdLXBQuz32xu42zU6 DavsY42VNuu5ZHhcqoVjuD+cruYzaKbhfJc/7y1JUktqbgIZfbvch52+RqANDPYc Dl6Hxvtcpg9eojSK7rQ8lbkvRmXAV1n70IL4287hjKN2cpICM2G1fKwGuU2Fdmxz WXSYABbAv1+AyMMVDwrHQJx/OFnAxouV/FQw0dX+GJUUe2q5XIS/D9py3ShF8jGo JTJPzI7eLxkBOeLXFQZLFDvgpMFvnLxxmXJp6EqscJmnbWyzFVMW3noEpqGNnmdV BKNA5LRDMoAn8fgTtg== =bO2i -----END PGP SIGNATURE----- Merge tag 'v21.0' into debian/buster-backports freedombox Debian release 21.0 Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
commit
b1782e4c13
@ -71,7 +71,13 @@ development environment inside a systemd-nspawn container.
|
||||
host$ ./container up
|
||||
```
|
||||
|
||||
4. SSH into the running container with the following command:
|
||||
4. To run unit and functional tests for an app:
|
||||
|
||||
```bash
|
||||
host$ ./container run-tests --pytest-args -v --include-functional --no-xvfb plinth/modules/{app name}
|
||||
```
|
||||
|
||||
5. SSH into the running container with the following command:
|
||||
|
||||
```bash
|
||||
host$ ./container ssh
|
||||
|
||||
114
actions/upgrades
114
actions/upgrades
@ -12,7 +12,8 @@ import subprocess
|
||||
import sys
|
||||
|
||||
from plinth.action_utils import (apt_hold, debconf_set_selections,
|
||||
run_apt_command, service_restart)
|
||||
run_apt_command, service_daemon_reload,
|
||||
service_restart)
|
||||
from plinth.modules.apache.components import check_url
|
||||
from plinth.modules.upgrades import (BACKPORTS_SOURCES_LIST, SOURCES_LIST,
|
||||
get_current_release, is_backports_current)
|
||||
@ -76,6 +77,20 @@ Pin: release a=buster-backports
|
||||
Pin-Priority: 500
|
||||
'''
|
||||
|
||||
DIST_UPGRADE_SERVICE = '''
|
||||
[Unit]
|
||||
Description=Upgrade to new stable Debian release
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/share/plinth/actions/upgrades dist-upgrade
|
||||
KillMode=process
|
||||
TimeoutSec=12hr
|
||||
'''
|
||||
|
||||
DIST_UPGRADE_SERVICE_PATH = \
|
||||
'/run/systemd/system/freedombox-dist-upgrade.service'
|
||||
|
||||
dist_upgrade_flag = pathlib.Path(
|
||||
'/var/lib/freedombox/dist-upgrade-in-progress')
|
||||
|
||||
@ -100,13 +115,12 @@ def parse_arguments():
|
||||
action='store_true',
|
||||
help='Development mode')
|
||||
|
||||
dist_upgrade = subparsers.add_parser(
|
||||
'dist-upgrade', help='Perform dist upgrade if possible')
|
||||
dist_upgrade.add_argument('--develop', required=False, default=False,
|
||||
action='store_true', help='Development mode')
|
||||
dist_upgrade.add_argument('--test', required=False, default=False,
|
||||
action='store_true',
|
||||
help='Test dist-upgrade from stable to testing')
|
||||
start_dist_upgrade = subparsers.add_parser(
|
||||
'start-dist-upgrade', help='Check and start dist upgrade process')
|
||||
start_dist_upgrade.add_argument(
|
||||
'--test', required=False, default=False, action='store_true',
|
||||
help='Test dist-upgrade from stable to testing')
|
||||
subparsers.add_parser('dist-upgrade', help='Perform dist upgrade')
|
||||
|
||||
subparsers.required = True
|
||||
return parser.parse_args()
|
||||
@ -294,26 +308,24 @@ def _add_apt_preferences():
|
||||
file_handle.write(APT_PREFERENCES_APPS)
|
||||
|
||||
|
||||
def _check_and_dist_upgrade(develop=False, test_upgrade=False):
|
||||
def _check_dist_upgrade(test_upgrade=False):
|
||||
"""Check for new stable release. If there is one, and updates are
|
||||
enabled, perform dist-upgrade.
|
||||
enabled, return True.
|
||||
|
||||
If develop is True, check for possible upgrade from stable to testing.
|
||||
If test_upgrade is True, also perform the upgrade to testing.
|
||||
If test_upgrade is True, also check for upgrade to testing.
|
||||
"""
|
||||
if dist_upgrade_flag.exists():
|
||||
print('Continuing previously interrupted dist-upgrade.')
|
||||
_perform_dist_upgrade()
|
||||
return
|
||||
print('Found previously interrupted dist-upgrade.')
|
||||
return True
|
||||
|
||||
release, dist = get_current_release()
|
||||
if release in ['unstable', 'testing']:
|
||||
print(f'System release is {release}. Skip checking for new stable '
|
||||
'release.')
|
||||
return
|
||||
return False
|
||||
|
||||
check_dists = ['stable']
|
||||
if develop:
|
||||
if test_upgrade:
|
||||
check_dists.append('testing')
|
||||
|
||||
codename = None
|
||||
@ -336,27 +348,27 @@ def _check_and_dist_upgrade(develop=False, test_upgrade=False):
|
||||
|
||||
if not codename:
|
||||
print('"Codename:" not found in release file.')
|
||||
return
|
||||
return False
|
||||
|
||||
if codename == dist:
|
||||
print(f'{dist} is already the latest release.')
|
||||
return
|
||||
return False
|
||||
|
||||
if not _check_auto():
|
||||
print('Automatic updates are not enabled.')
|
||||
return
|
||||
return False
|
||||
|
||||
if check_dist == 'testing' and not test_upgrade:
|
||||
print(f'Skipping dist-upgrade to {check_dist} since --test is not '
|
||||
'set.')
|
||||
return
|
||||
return False
|
||||
|
||||
output = subprocess.check_output(['df', '--output=avail,pcent', '/'])
|
||||
output = output.decode().split('\n')[1].split()
|
||||
free_space, free_percent = int(output[0]), int(output[1][:-1])
|
||||
if free_space < 1000000 or free_percent < 10:
|
||||
print('Not enough free space in /.')
|
||||
return
|
||||
return False
|
||||
|
||||
print(f'Upgrading from {dist} to {codename}...')
|
||||
with open(SOURCES_LIST, 'r') as sources_list:
|
||||
@ -378,7 +390,7 @@ def _check_and_dist_upgrade(develop=False, test_upgrade=False):
|
||||
|
||||
print('Dist upgrade in progress. Setting flag.')
|
||||
dist_upgrade_flag.touch(mode=0o660)
|
||||
_perform_dist_upgrade()
|
||||
return True
|
||||
|
||||
|
||||
def _perform_dist_upgrade():
|
||||
@ -391,7 +403,7 @@ def _perform_dist_upgrade():
|
||||
|
||||
print('Upgrading base-files and unattended-upgrades...', flush=True)
|
||||
run_apt_command(['install', 'base-files'])
|
||||
run_apt_command(['install', 'unattended-upgrades'])
|
||||
run_apt_command(['install', 'python3-systemd', 'unattended-upgrades'])
|
||||
|
||||
# Tell grub-pc to continue without installing grub again.
|
||||
print('Set grub-pc to not require re-installing grub...', flush=True)
|
||||
@ -413,27 +425,38 @@ def _perform_dist_upgrade():
|
||||
'Holding packages with conffile prompts: ' +
|
||||
', '.join(packages_with_prompts) + '...', flush=True)
|
||||
with apt_hold(packages_with_prompts):
|
||||
print('Running apt full-upgrade...', flush=True)
|
||||
run_apt_command(['full-upgrade'])
|
||||
print('Holding tt-rss package if available...', flush=True)
|
||||
with apt_hold(['tt-rss'], ignore_errors=True):
|
||||
print('Running apt full-upgrade...', flush=True)
|
||||
run_apt_command(['full-upgrade'])
|
||||
|
||||
# If searx is installed, update search engines list.
|
||||
if pathlib.Path('/etc/searx/settings.yml').exists():
|
||||
print('Updating searx search engines list...', flush=True)
|
||||
subprocess.run(['./searx', 'setup'], check=True)
|
||||
subprocess.run(['/usr/share/plinth/actions/searx', 'setup'],
|
||||
check=True)
|
||||
|
||||
print('Running apt autoremove...', flush=True)
|
||||
run_apt_command(['autoremove'])
|
||||
|
||||
# Run unattended-upgrade once more to handle upgrading the
|
||||
# freedombox package.
|
||||
print('Running unattended-upgrade...', flush=True)
|
||||
subprocess.run(['unattended-upgrade', '--verbose'])
|
||||
|
||||
# Restart FreedomBox service to ensure it is using the latest
|
||||
# dependencies.
|
||||
print('Restarting FreedomBox service...', flush=True)
|
||||
service_restart('plinth')
|
||||
|
||||
# Update apt cache again to trigger force_upgrades.
|
||||
print('Updating Apt cache...', flush=True)
|
||||
run_apt_command(['update'])
|
||||
|
||||
print('Dist upgrade complete. Removing flag.', flush=True)
|
||||
if dist_upgrade_flag.exists():
|
||||
dist_upgrade_flag.unlink()
|
||||
|
||||
# FreedomBox Service may have tried to restart several times
|
||||
# during the upgrade, but failed. It will stop trying after 5
|
||||
# retries. Restart it once more to ensure it is running.
|
||||
print('Restarting FreedomBox service...', flush=True)
|
||||
service_restart('plinth')
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
"""Setup apt preferences."""
|
||||
@ -450,14 +473,27 @@ def subcommand_activate_backports(arguments):
|
||||
_check_and_backports_sources(arguments.develop)
|
||||
|
||||
|
||||
def subcommand_dist_upgrade(arguments):
|
||||
"""Perform major distribution upgrade.
|
||||
|
||||
Check if a new stable release is available, and perform dist-upgrade if
|
||||
updates are enabled.
|
||||
def subcommand_start_dist_upgrade(arguments):
|
||||
"""Start dist upgrade process.
|
||||
|
||||
Check if a new stable release is available, and start dist-upgrade process
|
||||
if updates are enabled.
|
||||
"""
|
||||
_check_and_dist_upgrade(arguments.develop, arguments.test)
|
||||
if _check_dist_upgrade(arguments.test):
|
||||
with open(DIST_UPGRADE_SERVICE_PATH, 'w') as service_file:
|
||||
service_file.write(DIST_UPGRADE_SERVICE)
|
||||
|
||||
service_daemon_reload()
|
||||
subprocess.Popen(['systemctl', 'start', 'freedombox-dist-upgrade'],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, close_fds=True,
|
||||
start_new_session=True)
|
||||
|
||||
|
||||
def subcommand_dist_upgrade(_):
|
||||
"""Perform major distribution upgrade.
|
||||
"""
|
||||
_perform_dist_upgrade()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
127
container
127
container
@ -161,10 +161,84 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get install --no-upgrade --yes \
|
||||
$(sudo -u plinth /freedombox/run --develop --list-dependencies)
|
||||
sudo apt-mark unhold freedombox
|
||||
# Install ncurses-term
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes ncurses-term
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes ncurses-term sshpass
|
||||
|
||||
echo 'alias freedombox-develop="sudo -u plinth /freedombox/run --develop"' \
|
||||
>> /home/fbx/.bashrc
|
||||
|
||||
# Make some pytest related files and directories writable to the fbx user
|
||||
sudo touch geckodriver.log
|
||||
sudo chmod a+rw geckodriver.log
|
||||
sudo mkdir -p .pytest_cache/
|
||||
sudo chmod --recursive a+rw .pytest_cache/
|
||||
'''
|
||||
|
||||
SETUP_AND_RUN_TESTS_SCRIPT = '''
|
||||
BACKPORTS_SOURCES_LIST=/etc/apt/sources.list.d/freedombox2.list
|
||||
LDAPSCRIPTS_CONF=/etc/ldapscripts/freedombox-ldapscripts.conf
|
||||
|
||||
# Remount /freedoombox to be up to date
|
||||
echo "> In container: Remounting /freedombox"
|
||||
mount -o remount /freedombox
|
||||
|
||||
# Activate backports if Debian stable
|
||||
if [[ "{distribution}" == "stable" && ! -e $BACKPORTS_SOURCES_LIST ]]
|
||||
then
|
||||
echo "> In container: Enable backports"
|
||||
/freedombox/actions/upgrades activate-backports
|
||||
fi
|
||||
|
||||
echo "> In container: Upgrade packages"
|
||||
apt-get update
|
||||
apt-get -yq --with-new-pkgs upgrade
|
||||
|
||||
# Install requirements for tests if not already installed as root
|
||||
if ! [[ -e /usr/local/bin/geckodriver && -e /usr/local/bin/pytest-bdd ]]
|
||||
then
|
||||
# sshpass for Debian Buster
|
||||
apt-get install -yq --no-install-recommends sshpass
|
||||
/freedombox/plinth/tests/functional/install.sh
|
||||
fi
|
||||
|
||||
# Run the plinth server if functional tests are requested
|
||||
if [[ "{pytest_command}" =~ "--include-functional" ]]
|
||||
then
|
||||
is_plinth_running=0
|
||||
ps -ax -o cmd | grep -q "^sudo -u plinth /freedombox/run" && \
|
||||
is_plinth_running=1
|
||||
ps -ax -o cmd | grep -q "^/usr/bin/python3 /usr/bin/plinth" && \
|
||||
is_plinth_running=1
|
||||
|
||||
if [[ $is_plinth_running -eq 1 ]]
|
||||
then
|
||||
echo "> In container: Plinth is already running"
|
||||
else
|
||||
echo -n "> In container: Starting plinth ... "
|
||||
sudo -u plinth /freedombox/run --develop > plinth.log 2>&1 &
|
||||
while ! grep -q "Setup thread finished" plinth.log
|
||||
do
|
||||
sleep 1
|
||||
echo -n .
|
||||
done
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ "{pytest_command}" =~ "--no-xvfb" ]]
|
||||
then
|
||||
# Use the X11 authority file from the fbx user to run GUI programs
|
||||
xauth merge /home/fbx/.Xauthority
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run pytest
|
||||
cd /freedombox
|
||||
export FREEDOMBOX_URL=https://localhost
|
||||
export FREEDOMBOX_SSH_PORT=22
|
||||
export FREEDOMBOX_SAMBA_PORT=445
|
||||
{pytest_command}
|
||||
|
||||
# Make pytest cache files writable to the fbx user
|
||||
chmod --recursive --silent a+rw .pytest_cache/
|
||||
'''
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -185,15 +259,32 @@ def parse_arguments():
|
||||
subparser.add_argument(
|
||||
'--distribution', choices=distributions, default='testing',
|
||||
help='Distribution of the image to download and setup')
|
||||
subparser.add_argument('--image-size', default='8G',
|
||||
subparser.add_argument('--image-size', default='12G',
|
||||
help='Disk image size to resize to after download')
|
||||
|
||||
# Print IP address
|
||||
subparser = subparsers.add_parser(
|
||||
'ip', help='Print the IP address of the container.')
|
||||
subparser.add_argument(
|
||||
'--distribution', choices=distributions, default='testing',
|
||||
help='Distribution of the container to print IP address')
|
||||
|
||||
# ssh
|
||||
subparser = subparsers.add_parser('ssh', help='SSH into the container')
|
||||
subparser.add_argument('--distribution', choices=distributions,
|
||||
default='testing',
|
||||
help='Distribution of the container to SSH into')
|
||||
|
||||
# Run tests
|
||||
subparser = subparsers.add_parser('run-tests',
|
||||
help='Run tests in the container')
|
||||
subparser.add_argument('--distribution', choices=distributions,
|
||||
default='testing',
|
||||
help='Distribution of the container to run tests')
|
||||
subparser.add_argument(
|
||||
'--pytest-args', nargs='...',
|
||||
help='Additional arguments to pass to the pytest command')
|
||||
|
||||
# Stop
|
||||
subparser = subparsers.add_parser('stop', help='Stop the container')
|
||||
subparser.add_argument('--distribution', choices=distributions,
|
||||
@ -739,6 +830,7 @@ Folder overlay : (host, read-only){project_folder}
|
||||
-> (container)/freedombox
|
||||
|
||||
SSH easily : {script} ssh {options}
|
||||
Run tests : {script} run-tests {options} [ --pytest-args ... ]
|
||||
Run FreedomBox inside : freedombox-develop
|
||||
|
||||
Web access : https://{ip_address}/
|
||||
@ -779,9 +871,10 @@ def _get_ssh_command(ip_address, distribution):
|
||||
ip_address = f'{ip_address}%' + _get_interface_name(distribution)
|
||||
|
||||
return [
|
||||
'ssh', '-i',
|
||||
str(public_key), '-o', 'StrictHostKeyChecking=no', '-o',
|
||||
'UserKnownHostsFile=/dev/null', f'fbx@{ip_address}'
|
||||
'ssh', '-Y', '-C', '-t', '-i',
|
||||
str(public_key), '-o', 'LogLevel=error', '-o',
|
||||
'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null',
|
||||
f'fbx@{ip_address}'
|
||||
]
|
||||
|
||||
|
||||
@ -827,6 +920,7 @@ def subcommand_up(arguments):
|
||||
machine_name = f'fbx-{arguments.distribution}'
|
||||
if _get_machine_status(machine_name):
|
||||
logger.info('Container is already running')
|
||||
_print_banner(arguments.distribution)
|
||||
return
|
||||
|
||||
_verify_dependencies()
|
||||
@ -839,6 +933,11 @@ def subcommand_up(arguments):
|
||||
_print_banner(arguments.distribution)
|
||||
|
||||
|
||||
def subcommand_ip(arguments):
|
||||
"""Print the IP address of the container."""
|
||||
print(_get_ip_address(arguments.distribution) or '')
|
||||
|
||||
|
||||
def subcommand_ssh(arguments):
|
||||
"""Open an SSH shell into the container."""
|
||||
ip_address = _wait_for(lambda: _get_ip_address(arguments.distribution))
|
||||
@ -847,6 +946,24 @@ def subcommand_ssh(arguments):
|
||||
os.execlp('ssh', *command)
|
||||
|
||||
|
||||
def subcommand_run_tests(arguments):
|
||||
"""Run tests in the container."""
|
||||
distribution = arguments.distribution
|
||||
pytest_args = ' '.join(arguments.pytest_args or [])
|
||||
ip_address = _wait_for(lambda: _get_ip_address(distribution))
|
||||
ssh_command = _get_ssh_command(ip_address, distribution)
|
||||
|
||||
# Disable cache as root has no rights to overwrite files on /freedombox
|
||||
pytest_command = f'py.test-3 {pytest_args}'
|
||||
logger.info('Pytest command: %s', pytest_command)
|
||||
|
||||
test_script = SETUP_AND_RUN_TESTS_SCRIPT.format(
|
||||
pytest_command=pytest_command, distribution=distribution)
|
||||
setup_and_run_command = ['sudo', 'bash', '-c', f"'{test_script}'"]
|
||||
command = ssh_command + setup_and_run_command
|
||||
os.execlp('ssh', *command)
|
||||
|
||||
|
||||
def subcommand_stop(arguments):
|
||||
"""Stop the container."""
|
||||
_stop(arguments.distribution)
|
||||
|
||||
@ -46,7 +46,7 @@ RedirectMatch "^/freedombox" "/plinth"
|
||||
##
|
||||
<Location /plinth>
|
||||
Header set Referrer-Policy 'same-origin'
|
||||
Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-forms allow-same-origin; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;"
|
||||
Header set Content-Security-Policy "font-src 'self'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; default-src 'self'; base-uri 'none'; sandbox allow-scripts allow-popups allow-forms allow-same-origin allow-downloads; form-action 'self'; frame-ancestors 'none'; block-all-mixed-content;"
|
||||
Header set X-Content-Type-Options 'nosniff'
|
||||
</Location>
|
||||
|
||||
|
||||
77
debian/changelog
vendored
77
debian/changelog
vendored
@ -1,3 +1,80 @@
|
||||
freedombox (21.0) unstable; urgency=medium
|
||||
|
||||
[ Dietmar ]
|
||||
* Translated using Weblate (German)
|
||||
|
||||
[ ikmaak ]
|
||||
* Translated using Weblate (German)
|
||||
* Translated using Weblate (Dutch)
|
||||
* Translated using Weblate (Spanish)
|
||||
* Translated using Weblate (French)
|
||||
|
||||
[ Burak Yavuz ]
|
||||
* Translated using Weblate (Turkish)
|
||||
|
||||
[ Doma Gergő ]
|
||||
* Translated using Weblate (Hungarian)
|
||||
|
||||
[ Veiko Aasa ]
|
||||
* functional tests: Make tests compatible with pytest-bdd v4.0
|
||||
* ejabberd: functional tests: Wait until the jsxc buddy list is loaded
|
||||
* users: Skip action script tests if LDAP is not set up
|
||||
* functional-tests: Fix installation errors in install.sh script
|
||||
* dev-container: Add subcommand to run tests
|
||||
* gitweb: tests: functional: Fix test failures in localized environment
|
||||
* dev-container: 'up' command: Show banner also when container is already
|
||||
running
|
||||
* dev-container: Add command to print container IP address
|
||||
* tests: functional: Improve creating users in tests
|
||||
* gitweb: Add functional tests for git-access group
|
||||
* plinth: Fix daemon is enabled check when service alias is provided
|
||||
|
||||
[ ullli ]
|
||||
* mumble: Updated mumla and removed plumble from clients list
|
||||
|
||||
[ Johannes Keyser ]
|
||||
* Translated using Weblate (German)
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* apache2: Allow downloads in openvpn and backups with latest browsers
|
||||
* backups: Don't open a new window for downloading backups
|
||||
* openvpn: Don't show running status on download profile button
|
||||
* app: component: Add app_id and app properties
|
||||
* app: Add locked flag
|
||||
* backups: Add new component for backup and restore
|
||||
* backups: Use the backup component in all apps
|
||||
* doc: dev: Update documentation for using backup component
|
||||
* app: info: Move client validation to info component
|
||||
* doc: dev: Update documentation on calling clients validation
|
||||
* doc: dev: Update the tutorial to reflect latest API/code
|
||||
* radicale: Fix backup and restore of configuration
|
||||
|
||||
[ Michael Breidenbach ]
|
||||
* Translated using Weblate (Swedish)
|
||||
|
||||
[ James Valleroy ]
|
||||
* users: Avoid test error if ldapsearch is not available
|
||||
* upgrades: Ensure freedombox package is upgraded during dist upgrade
|
||||
* upgrades: Add service for dist upgrade
|
||||
* upgrades: Install python3-systemd for unattended-upgrades
|
||||
* upgrades: Don't allow needrestart to restart freedombox-dist-upgrade
|
||||
* upgrades: Check before starting dist upgrade process
|
||||
* upgrades: Write dist-upgrade service file in /run
|
||||
* upgrades: Restart FreedomBox service at end of dist-upgrade
|
||||
* upgrades: Use full path to searx action script
|
||||
* upgrades: Hold tt-rss during dist upgrade, if available
|
||||
* locale: Update translation strings
|
||||
* doc: Fetch latest manual
|
||||
|
||||
[ Stanisław Stefan Krukowski ]
|
||||
* Translated using Weblate (Polish)
|
||||
|
||||
[ Joseph Nuthalapati ]
|
||||
* transmission: Show port forwarding information
|
||||
* transmission: Update description
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Mon, 11 Jan 2021 19:57:44 -0500
|
||||
|
||||
freedombox (20.21~bpo10+1) buster-backports; urgency=medium
|
||||
|
||||
* Rebuild for buster-backports.
|
||||
|
||||
@ -26,7 +26,7 @@ External References
|
||||
|
||||
#. `systemd System and Service Manager <https://www.freedesktop.org/wiki/Software/systemd/>`_
|
||||
|
||||
#. `Bootstrap - CSS Library <http://getbootstrap.com/css/>`_
|
||||
#. `Bootstrap - CSS Library <http://getbootstrap.com/>`_
|
||||
|
||||
#. `FreedomBox User Manual <https://wiki.debian.org/FreedomBox/Manual>`_
|
||||
|
||||
|
||||
@ -6,17 +6,51 @@ 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>.init()
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Optional. This method is called by FreedomBox soon after all the applications
|
||||
are loaded. The ``init()`` call order guarantees that other applications that
|
||||
this application depends on will be initialized before this application is
|
||||
initialized.
|
||||
|
||||
<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 containing the
|
||||
full module load path. For example, ``names``.
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Optional. 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.
|
||||
|
||||
7
doc/dev/reference/components/backups.rst
Normal file
7
doc/dev/reference/components/backups.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
|
||||
Backups
|
||||
^^^^^^^
|
||||
|
||||
.. autoclass:: plinth.modules.backups.components.BackupRestore
|
||||
:members:
|
||||
@ -15,6 +15,7 @@ Components
|
||||
domain
|
||||
letsencrypt
|
||||
staticfiles
|
||||
backups
|
||||
|
||||
Base Classes
|
||||
^^^^^^^^^^^^
|
||||
|
||||
@ -5,7 +5,9 @@ Part 4: Components
|
||||
|
||||
Each :class:`~plinth.app.App` contains various :class:`~plinth.app.Component`
|
||||
components that each provide one small functionality needed by the app. Each of
|
||||
these components are instantiated and added to the app as children.
|
||||
these components are instantiated and added to the app as children. The
|
||||
:class:`~plinth.menu.Menu` object added in the previous step is one such
|
||||
component.
|
||||
|
||||
Providing basic information about the app
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -17,6 +19,8 @@ function normally.
|
||||
|
||||
from plinth import app as app_module
|
||||
|
||||
from . import manifest
|
||||
|
||||
class TransmissionApp(app_module.App):
|
||||
...
|
||||
|
||||
@ -28,15 +32,45 @@ function normally.
|
||||
icon_filename='transmission',
|
||||
short_description=_('BitTorrent Web Client'),
|
||||
description=description,
|
||||
manual_page='Transmission', clients=clients)
|
||||
manual_page='Transmission',
|
||||
clients=manifest.clients,
|
||||
donation_url='https://transmissionbt.com/donate/')
|
||||
self.add(info)
|
||||
|
||||
The first argument is app_id that is same as the ID for the app. The version is
|
||||
the version number for this app that must be incremented whenever setup() method
|
||||
needs to be called again. name, icon_filename, short_description, description,
|
||||
manual_page and clients provide information that is shown on the app's main
|
||||
page. More information the parameters is available in :class:`~plinth.app.Info`
|
||||
class documentation.
|
||||
page. The donation_url encourages our users to contribute to upstream projects
|
||||
in order ensure their long term sustainability. More information about the
|
||||
parameters is available in :class:`~plinth.app.Info` class documentation.
|
||||
|
||||
The description of app should provide basic information on what the app is about
|
||||
and how to use it. It is impractical, however, to explain everything about the
|
||||
app in a few short paragraphs. So, we need to write a page about the app in the
|
||||
FreedomBox manual. This page will be available to the users from within the
|
||||
FreedomBox web interface. To make this happen, let us write a `manual page entry
|
||||
<https://wiki.debian.org/FreedomBox/Manual/Transmission>`_ for our app in the
|
||||
`FreedomBox Wiki <https://wiki.debian.org/FreedomBox/Manual>`_ and then provide
|
||||
a link to it from app page.
|
||||
|
||||
It would be helpful to our users if we can show how they can use our app. If
|
||||
there are desktop and mobile clients that can used to access our service, we
|
||||
need to list them and present them. Let's add this information to
|
||||
``manifest.py``.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
clients = [{
|
||||
'name': _('Transmission'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/transmission'
|
||||
}]
|
||||
}]
|
||||
|
||||
Since our app is a simple web application with no clients needed, we just list
|
||||
that.
|
||||
|
||||
Managing a daemon
|
||||
^^^^^^^^^^^^^^^^^
|
||||
@ -229,3 +263,49 @@ a later section of this tutorial. The next parameter specifies whether anonymous
|
||||
users who are not logged into FreedomBox should be shown this shortcut. The
|
||||
final parameter further restricts to which group of users this shortcut must be
|
||||
shown.
|
||||
|
||||
Adding backup/restore functionality
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Each app in FreedomBox needs to provide the ability to backup and restore its
|
||||
configuration and data. Apart from providing durability to users' data, this
|
||||
allows the user to migrate from one machine to another. FreedomBox framework
|
||||
provides a component for handling these operations. Create the
|
||||
:class:`~plinth.modules.backups.components.BackupRestore` component during app
|
||||
initialization.
|
||||
|
||||
In ``__init__.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
|
||||
from . import manifest
|
||||
|
||||
class TransmissionApp(app_module.App):
|
||||
...
|
||||
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-transmission',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
In ``manifest.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
backup = {
|
||||
'data': {
|
||||
'directories': ['/var/lib/transmission-daemon/.config']
|
||||
},
|
||||
'secrets': {
|
||||
'files': ['/etc/transmission-daemon/settings.json']
|
||||
},
|
||||
'services': ['transmission-daemon']
|
||||
}
|
||||
|
||||
The data and secrets information specifies which list of files and directories
|
||||
FreedomBox framework needs to backup and restore. The list of services specifies
|
||||
which daemons should be stopped during the backup and restore process.
|
||||
|
||||
@ -64,18 +64,21 @@ provide options to the user. Add the following to ``forms.py``.
|
||||
from django import forms
|
||||
|
||||
|
||||
class TransmissionForm(forms.Form): # pylint: disable=W0232
|
||||
class TransmissionForm(DirectorySelectForm): # pylint: disable=W0232
|
||||
"""Transmission configuration form"""
|
||||
download_dir = forms.CharField(
|
||||
label='Download directory',
|
||||
help_text='Directory where downloads are saved. If you change the '
|
||||
'default directory, ensure that the new directory exists '
|
||||
'and is writable by "debian-transmission" user.')
|
||||
|
||||
This creates a Django form that shows a single option to set the download
|
||||
directory for our Transmission app. This is how a regular Django form is built.
|
||||
See :doc:`Django Forms documentation <django:topics/forms/index>` for more
|
||||
information.
|
||||
def __init__(self, *args, **kw):
|
||||
validator = DirectoryValidator(username=SYSTEM_USER,
|
||||
check_creatable=True)
|
||||
super(TransmissionForm,
|
||||
self).__init__(title=_('Download directory'),
|
||||
default='/var/lib/transmission-daemon/downloads',
|
||||
validator=validator, *args, **kw)
|
||||
|
||||
This uses a utility provided by the framework and creates a Django form that
|
||||
shows a single option to set the download directory for our Transmission app.
|
||||
This is similar to how a regular Django form is built. See :doc:`Django Forms
|
||||
documentation <django:topics/forms/index>` for more information.
|
||||
|
||||
.. tip: Too many options
|
||||
|
||||
@ -100,8 +103,9 @@ the user submits it. Let us implement that in ``views.py``.
|
||||
from .forms import TransmissionForm
|
||||
|
||||
class TransmissionAppView(views.AppView):
|
||||
...
|
||||
"""Serve configuration page."""
|
||||
form_class = TransmissionForm
|
||||
app_id = 'transmission'
|
||||
|
||||
def get_initial(self):
|
||||
"""Get the current settings from Transmission server."""
|
||||
@ -109,22 +113,18 @@ the user submits it. Let us implement that in ``views.py``.
|
||||
configuration = actions.superuser_run('transmission',
|
||||
['get-configuration'])
|
||||
configuration = json.loads(configuration)
|
||||
status.update({
|
||||
key.translate(str.maketrans({
|
||||
'-': '_'
|
||||
})): value
|
||||
for key, value in configuration.items()
|
||||
})
|
||||
status['storage_path'] = configuration['download-dir']
|
||||
status['hostname'] = socket.gethostname()
|
||||
|
||||
return status
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Apply the changes submitted in the form."""
|
||||
old_status = form.initial
|
||||
new_status = form.cleaned_data
|
||||
|
||||
if old_status['download_dir'] != new_status['download_dir']:
|
||||
if old_status['storage_path'] != new_status['storage_path']:
|
||||
new_configuration = {
|
||||
'download-dir': new_status['download_dir'],
|
||||
'download-dir': new_status['storage_path'],
|
||||
}
|
||||
|
||||
actions.superuser_run('transmission', ['merge-configuration'],
|
||||
|
||||
@ -32,9 +32,11 @@ Coding standards
|
||||
|
||||
For readability and easy collaboration it is important to follow common coding
|
||||
standards. FreedomBox uses the Python coding standards and uses the ``pylint``
|
||||
and ``flake8`` tools to check if the there are any violations. Run these tools
|
||||
on our application and fix any errors and warnings. Better yet, integrate these
|
||||
tools into your favorite IDE for on-the-fly checking.
|
||||
and ``flake8`` tools to check if the there are any violations. ``yapf`` and
|
||||
``isort`` tools are used to automatically format the code to ensure that all
|
||||
developers produce similarly formatted code. Run these tools on our application
|
||||
and fix any errors and warnings. Better yet, integrate these tools into your
|
||||
favorite IDE for on-the-fly checking.
|
||||
|
||||
For the most part, the code we have written so far, is already compliant with
|
||||
the coding standards. This includes variable/method naming, indentation,
|
||||
|
||||
@ -3,102 +3,6 @@
|
||||
Part 7: Other Changes
|
||||
---------------------
|
||||
|
||||
Showing information about app clients
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
It would be helpful to our users if we can show how they can use our app. If
|
||||
there are desktop and mobile clients that can used to access our service, we
|
||||
need to list them and present them. Let's add this information to
|
||||
``manifest.py``.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from plinth.clients import validate
|
||||
|
||||
clients = validate([{
|
||||
'name': _('Transmission'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/transmission'
|
||||
}]
|
||||
}])
|
||||
|
||||
Since our app is a simple web application with no clients needed, we just list
|
||||
that. We need to include this into the main app view. In ``__init__.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from .manifest import clients
|
||||
|
||||
clients = clients
|
||||
|
||||
In ``views.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from plinth.modules import transmission
|
||||
|
||||
class TransmissionAppView(views.AppView):
|
||||
...
|
||||
clients = transmission.clients
|
||||
|
||||
Writing a manual page
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The description of app should provide basic information on what the app is about
|
||||
and how to use it. It is impractical, however, to explain everything about the
|
||||
app in a few short paragraphs. So, we need to write a page about the app in the
|
||||
FreedomBox manual. This page will be available to the users from within the
|
||||
FreedomBox web interface. To make this happen, let us write a `manual page entry
|
||||
<https://wiki.debian.org/FreedomBox/Manual/Transmission>`_ for our app in the
|
||||
`FreedomBox Wiki <https://wiki.debian.org/FreedomBox/Manual>`_ and then provide
|
||||
a link to it from app page. In ``__init__.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
manual_page = 'Transmission'
|
||||
|
||||
Then, in ``views.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from plinth.modules import transmission
|
||||
|
||||
class TransmissionAppView(views.AppView):
|
||||
...
|
||||
manual_page = transmission.manual_page
|
||||
|
||||
Adding backup/restore functionality
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Each app in FreedomBox needs to provide the ability to backup its configuration
|
||||
and data. Apart from providing durability to users' data, this allows the user
|
||||
to migrate from one machine to another. FreedomBox framework provides a simple
|
||||
declarative mechanism to allow the app to be backed up and restored. In
|
||||
``manifest.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
backup = validate_backup({
|
||||
'data': {
|
||||
'directories': ['/var/lib/transmission-daemon/.config']
|
||||
},
|
||||
'secrets': {
|
||||
'files': ['/etc/transmission-daemon/settings.json']
|
||||
},
|
||||
'services': ['transmission-daemon']
|
||||
})
|
||||
|
||||
The data and secrets information specifies which list of files and directories
|
||||
FreedomBox framework needs to backup. The list of services specifies which
|
||||
daemons should be stopped during the backup process. In ``__init__.py``, add:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
from .manifest import backup
|
||||
|
||||
Creating diagnostics
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -174,16 +78,17 @@ the Django's localization methods to make that happen.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
name = _('Transmission')
|
||||
class TransmissionApp(app_module.App):
|
||||
...
|
||||
|
||||
short_description = _('BitTorrent Web Client')
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
description = [
|
||||
_('BitTorrent is a peer-to-peer file sharing protocol. '
|
||||
'Transmission daemon handles Bitorrent file sharing. Note that '
|
||||
'BitTorrent is not anonymous.'),
|
||||
_('Access the web interface at <a href="/transmission">/transmission</a>.')
|
||||
]
|
||||
info = app_module.Info(...
|
||||
name=_('Transmission'),
|
||||
...
|
||||
short_description=_('BitTorrent Web Client'),
|
||||
...)
|
||||
|
||||
Notice that the app's name, description, etc. are wrapped in the ``_()`` method
|
||||
call. This needs to be done for the rest of our app. We use the
|
||||
|
||||
@ -67,7 +67,7 @@ Creating the App class
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In the FreedomBox framework, each app must be a class derived from the
|
||||
:class:`plinth.app.App`. Let us to that in ``__init__.py``. We will fill up the
|
||||
:class:`plinth.app.App`. Let us do that in ``__init__.py``. We will fill up the
|
||||
class later.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@ -36,26 +36,15 @@ a link in FreedomBox web interface. Let us add a link in the apps list. In
|
||||
|
||||
from plinth.menu import main_menu
|
||||
|
||||
name = 'Transmission'
|
||||
|
||||
short_description = 'BitTorrent Web Client'
|
||||
|
||||
description = [
|
||||
'BitTorrent is a peer-to-peer file sharing protocol. '
|
||||
'Transmission daemon handles Bitorrent file sharing. Note that '
|
||||
'BitTorrent is not anonymous.',
|
||||
'Access the web interface at <a href="/transmission">/transmission</a>.'
|
||||
]
|
||||
|
||||
class TransmissionApp(app_module.App):
|
||||
...
|
||||
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
menu_item = menu.Menu('menu-transmission', name, short_description,
|
||||
'transmission', 'transmission:index',
|
||||
parent_url_name='apps')
|
||||
menu_item = menu.Menu('menu-transmission', 'Transmission',
|
||||
'BitTorrrent Web Client', 'transmission',
|
||||
'transmission:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
What this does is add a menu item component into the our app. In FreedomBox
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/Calibre|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[FreedomBox/Manual/Calibre|English]] - [[es/FreedomBox/Manual/Calibre|Español]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
@ -31,7 +33,7 @@ calibre can be accessed after installation through the web client at {{{https://
|
||||
|
||||
=== External links ===
|
||||
|
||||
* Official website <<BR>> https://calibre-ebook.com/
|
||||
* Official website <<BR>> https://calibre-ebook.com
|
||||
|
||||
## END_INCLUDE
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/Coturn|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[FreedomBox/Manual/Coturn|English]] - [[es/FreedomBox/Manual/Coturn|Español]] - [[DebianWiki/EditorGuide#translation|(+)]] -~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
@ -11,7 +13,7 @@
|
||||
|
||||
'''Available since''': version 20.8
|
||||
|
||||
Coturn is a server to facilitate audio/video calls and conferences by providing an implementation of TURN and STUN protocols. WebRTC, SIP and other communication servers can use it to establish a call between parties who are otherwise unable connect to each other.
|
||||
Coturn is a server to facilitate audio/video calls and conferences by providing an implementation of [[https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT|TURN]] and [[https://en.wikipedia.org/wiki/STUN|STUN]] protocols. WebRTC, SIP and other communication servers can use it to establish a call between parties who are otherwise unable connect to each other.
|
||||
|
||||
It is not meant to be used directly by users. Servers such as Matrix Synapse need to be configured with the details provided on the Coturn app page. Apart from Matrix Synapse, Jitsi, Ejabberd, Nextcloud Talk, etc. can use Coturn server for audio/video calls and conferences. There is no need for the servers to be running on the same machine as !FreedomBox and external servers can use Coturn running on !FreedomBox.
|
||||
|
||||
@ -21,9 +23,9 @@ Coturn is configured in !FreedomBox as an advanced app. This means that you need
|
||||
|
||||
When making an audio/video call, it is best to route the media streams between two peers directly. This will give the best possible latency (better signal quality) and avoid depending on a centralized server (privacy). It scales well because a simple chat server can host thousands of calls without involving the server in any way other than to setup the call. However, this approach does not work most of the time to due to how networks are configured. Most peers on the network do not have a unique IP address allocated to them. They work hidden behind a network device that performs "Network Address Translation" (NAT) for them. This means that the two peers have no way of reaching each other.
|
||||
|
||||
To address this problem, a simple technique known as STUN was introduced. With the help of a third party STUN server, the peers can trick the NAT devices, to carry the traffic between the two peers. Unfortunately, this trick only works about 80% of the time. So, if STUN fails, peers have no choice but to route their traffic through an intermediary server called TURN server. All the mechanism of trying out STUN first and then falling back to TURN is described in a protocol known as ICE.
|
||||
To address this problem, a simple technique known as STUN was introduced. With the help of a third party STUN server, the peers can trick the NAT devices, to carry the traffic between the two peers. Unfortunately, this trick only works about 80% of the time. So, if STUN fails, peers have no choice but to route their traffic through an intermediary server called TURN server. All the mechanism of trying out STUN first and then falling back to TURN is described in a protocol known as [[https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment|ICE]].
|
||||
|
||||
On !FreedomBox, Coturn provides both STUN and TURN servers. Both services are provided over TCP as well as UDP. They are provided on unencrypted as well as encrypted channels (with have a higher chance of success). Since STUN servers are very inexpensive and don't consume a lot of server resources, there is no authentication needed to use them. TURN servers on the other hand need authentication. This authentication is highly simplified and does not require maintaining a database of users. A server such as matrix-synapse which is about to setup an audio/video call between two peers will generate a username and password using a shared secret. When the peers use the TURN server, they will be validated using these credentials because the TURN server also knows the same secret.
|
||||
On !FreedomBox, Coturn provides both STUN and TURN servers. Both services are provided over TCP as well as UDP. They are provided on unencrypted as well as encrypted channels (which have a higher chance of success). Since STUN servers are very inexpensive and don't consume a lot of server resources, there is no authentication needed to use them. TURN servers on the other hand need authentication. This authentication is highly simplified and does not require maintaining a database of users. A server such as matrix-synapse which is about to setup an audio/video call between two peers will generate a username and password using a shared secret. When the peers use the TURN server, they will be validated using these credentials because the TURN server also knows the same secret.
|
||||
|
||||
In summary, a communication server needs to know the URLs of the STUN/TURN servers along with a shared authentication secret for TURN. After that, during audio/video call setup, they will correctly guide the peers to use STUN/TURN servers. Coturn app in !FreedomBox provides exactly this information. This information can be used to configure a communication server irrespective of whether it is running on the same !FreedomBox or on another server.
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/Deluge|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[de/FreedomBox/Manual/Deluge|Deutsch]] - [[FreedomBox/Manual/Deluge|English]] - [[es/FreedomBox/Manual/Deluge|Español]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#language en
|
||||
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~-[[FreedomBox/Manual/GettingHelp|English]] - [[es/FreedomBox/Manual/GettingHelp|Español]] - [[fr/FreedomBox/Manual/GettingHelp|Français]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
~-[[de/FreedomBox/Manual/GettingHelp|Deutsch]] - [[FreedomBox/Manual/GettingHelp|English]] - [[es/FreedomBox/Manual/GettingHelp|Español]] - [[fr/FreedomBox/Manual/GettingHelp|Français]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
## BEGIN_INCLUDE
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/GitWeb|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[FreedomBox/Manual/GitWeb|English]] - [[es/FreedomBox/Manual/GitWeb|Español]] - [[DebianWiki/EditorGuide#translation|(+)]] -~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
=== Using ===
|
||||
|
||||
* !FreedomBox Service comes installed with all !FreedomBox images. You can [[FreedomBox/Download|download]] !FreedomBox images and run on any of the supported hardware. Then, you can access !FreedomBox Service by visiting the URL [[http://freedombox/plinth]] or [[https://freedombox.local/plinth]].
|
||||
* !FreedomBox Service comes installed with all !FreedomBox images. You can [[FreedomBox/Download|download]] !FreedomBox images and run on any of the supported hardware. Then, to access !FreedomBox interface see [[FreedomBox/Manual/QuickStart|quick start]] instructions.
|
||||
|
||||
* If you are on a Debian box, you may install !FreedomBox Service from Debian package archive. Currently, only Buster (stable), Bullseye (testing), and Sid (unstable) are supported. To install !FreedomBox Service run:
|
||||
|
||||
|
||||
@ -10,6 +10,44 @@ 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.0 (2021-01-11) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* apache2: Allow downloads in openvpn and backups with latest browsers
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* locale: Update translations for Dutch, French, German, Hungarian, Polish, Spanish, Swedish, Turkish
|
||||
* app: Add locked flag
|
||||
* app: component: Add app_id and app properties
|
||||
* app: info: Move client validation to info component
|
||||
* backups: Add new component for backup and restore
|
||||
* backups: Don't open a new window for downloading backups
|
||||
* dev-container: 'up' command: Show banner also when container is already running
|
||||
* dev-container: Add command to print container IP address
|
||||
* dev-container: Add subcommand to run tests
|
||||
* doc: dev: Update the tutorial to reflect latest API/code
|
||||
* ejabberd: functional tests: Wait until the jsxc buddy list is loaded
|
||||
* functional tests: Make tests compatible with pytest-bdd v4.0
|
||||
* functional-tests: Fix installation errors in install.sh script
|
||||
* gitweb: Add functional tests for git-access group
|
||||
* gitweb: tests: functional: Fix test failures in localized environment
|
||||
* mumble: Updated mumla and removed plumble from clients list
|
||||
* openvpn: Don't show running status on download profile button
|
||||
* plinth: Fix daemon is enabled check when service alias is provided
|
||||
* radicale: Fix backup and restore of configuration
|
||||
* tests: functional: Improve creating users in tests
|
||||
* transmission: Show port forwarding information
|
||||
* transmission: Update description
|
||||
* upgrades: Add service for dist upgrade
|
||||
* upgrades: Ensure freedombox package is upgraded during dist upgrade
|
||||
* upgrades: Hold tt-rss during dist upgrade, if available
|
||||
* upgrades: Install python3-systemd for unattended-upgrades
|
||||
* upgrades: Restart !FreedomBox service at end of dist-upgrade
|
||||
* upgrades: Use full path to searx action script
|
||||
* users: Skip action script tests if LDAP is not set up
|
||||
|
||||
== FreedomBox 20.21 (2020-12-28) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/Tor|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[de/FreedomBox/Manual/Tor|Deutsch]] - [[FreedomBox/Manual/Tor|English]] - [[es/FreedomBox/Manual/Tor|Español]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
## page was renamed from FreedomBox/Manual/XMPP
|
||||
#language en
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: English - [[es/FreedomBox/Manual/ejabberd|Español]] -~
|
||||
##TAG:TRANSLATION-HEADER-START
|
||||
~- [[FreedomBox/Manual/ejabberd|English]] - [[es/FreedomBox/Manual/ejabberd|Español]] - [[DebianWiki/EditorGuide#translation|(+)]]-~
|
||||
##TAG:TRANSLATION-HEADER-END
|
||||
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/Calibre|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/Calibre, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/Coturn|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/Coturn, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/Deluge|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/Deluge, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/GitWeb|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/GitWeb, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -10,6 +10,44 @@ 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.0 (2021-01-11) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* apache2: Allow downloads in openvpn and backups with latest browsers
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* locale: Update translations for Dutch, French, German, Hungarian, Polish, Spanish, Swedish, Turkish
|
||||
* app: Add locked flag
|
||||
* app: component: Add app_id and app properties
|
||||
* app: info: Move client validation to info component
|
||||
* backups: Add new component for backup and restore
|
||||
* backups: Don't open a new window for downloading backups
|
||||
* dev-container: 'up' command: Show banner also when container is already running
|
||||
* dev-container: Add command to print container IP address
|
||||
* dev-container: Add subcommand to run tests
|
||||
* doc: dev: Update the tutorial to reflect latest API/code
|
||||
* ejabberd: functional tests: Wait until the jsxc buddy list is loaded
|
||||
* functional tests: Make tests compatible with pytest-bdd v4.0
|
||||
* functional-tests: Fix installation errors in install.sh script
|
||||
* gitweb: Add functional tests for git-access group
|
||||
* gitweb: tests: functional: Fix test failures in localized environment
|
||||
* mumble: Updated mumla and removed plumble from clients list
|
||||
* openvpn: Don't show running status on download profile button
|
||||
* plinth: Fix daemon is enabled check when service alias is provided
|
||||
* radicale: Fix backup and restore of configuration
|
||||
* tests: functional: Improve creating users in tests
|
||||
* transmission: Show port forwarding information
|
||||
* transmission: Update description
|
||||
* upgrades: Add service for dist upgrade
|
||||
* upgrades: Ensure freedombox package is upgraded during dist upgrade
|
||||
* upgrades: Hold tt-rss during dist upgrade, if available
|
||||
* upgrades: Install python3-systemd for unattended-upgrades
|
||||
* upgrades: Restart !FreedomBox service at end of dist-upgrade
|
||||
* upgrades: Use full path to searx action script
|
||||
* users: Skip action script tests if LDAP is not set up
|
||||
|
||||
== FreedomBox 20.21 (2020-12-28) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/Tor|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/Tor, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
## page was renamed from FreedomBox/Manual/XMPP
|
||||
#language es
|
||||
|
||||
~- [[DebianWiki/EditorGuide#translation|Translation(s)]]: [[FreedomBox/Manual/ejabberd|English]] - Español -~
|
||||
<<Include(FreedomBox/Manual/ejabberd, ,from="^##TAG:TRANSLATION-HEADER-START",to="^##TAG:TRANSLATION-HEADER-END")>>
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
Package init file.
|
||||
"""
|
||||
|
||||
__version__ = '20.21'
|
||||
__version__ = '21.0'
|
||||
|
||||
@ -413,7 +413,7 @@ def run_apt_command(arguments):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def apt_hold(packages=None):
|
||||
def apt_hold(packages=None, ignore_errors=False):
|
||||
"""Prevent packages from being removed during apt operations."""
|
||||
if not packages:
|
||||
packages = ['freedombox']
|
||||
@ -421,7 +421,8 @@ def apt_hold(packages=None):
|
||||
current_hold = subprocess.check_output(['apt-mark', 'showhold'] + packages)
|
||||
try:
|
||||
yield current_hold or subprocess.run(['apt-mark', 'hold'] + packages,
|
||||
check=True)
|
||||
check=not ignore_errors)
|
||||
finally:
|
||||
if not current_hold:
|
||||
subprocess.run(['apt-mark', 'unhold'] + packages, check=True)
|
||||
subprocess.run(['apt-mark', 'unhold'] + packages,
|
||||
check=not ignore_errors)
|
||||
|
||||
@ -5,6 +5,8 @@ Base class for all Freedombox applications.
|
||||
|
||||
import collections
|
||||
|
||||
from . import clients as clients_module
|
||||
|
||||
|
||||
class App:
|
||||
"""Implement common functionality for an app.
|
||||
@ -23,12 +25,19 @@ class App:
|
||||
the user. Enable/disable button for this app will not be shown. Default
|
||||
value is True, so the app can be disabled.
|
||||
|
||||
'locked' is a boolean indicating whether the user can perform operations on
|
||||
the app. This flag is currently set during backup and restore operations
|
||||
but UI changes are currently not implemented.
|
||||
|
||||
"""
|
||||
|
||||
app_id = None
|
||||
|
||||
can_be_disabled = True
|
||||
|
||||
locked = False # Whether user interaction with the app is allowed.
|
||||
# XXX: Lockdown the application UI by implementing a middleware
|
||||
|
||||
_all_apps = collections.OrderedDict()
|
||||
|
||||
def __init__(self):
|
||||
@ -53,12 +62,14 @@ class App:
|
||||
|
||||
def add(self, component):
|
||||
"""Add a component to an app."""
|
||||
component.app_id = self.app_id
|
||||
self.components[component.component_id] = component
|
||||
return self
|
||||
|
||||
def remove(self, component_id):
|
||||
"""Remove a component from the app."""
|
||||
component = self.components[component_id]
|
||||
component.app_id = None
|
||||
del self.components[component_id]
|
||||
return component
|
||||
|
||||
@ -167,7 +178,13 @@ class App:
|
||||
|
||||
|
||||
class Component:
|
||||
"""Interface for an app component."""
|
||||
"""Interface for an app component.
|
||||
|
||||
`app_id` is a string which is set to the value of the application's app_id
|
||||
to which this component belongs. It is set when the component is added to
|
||||
an app. When the component is removed from an app, it set to None.
|
||||
|
||||
"""
|
||||
|
||||
is_leader = False
|
||||
|
||||
@ -177,6 +194,16 @@ class Component:
|
||||
raise ValueError('Invalid component ID')
|
||||
|
||||
self.component_id = component_id
|
||||
self.app_id = None
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
"""Return the app this component is part of.
|
||||
|
||||
Raises KeyError if this component is not part of any app.
|
||||
|
||||
"""
|
||||
return App.get(self.app_id)
|
||||
|
||||
def enable(self):
|
||||
"""Run operations to enable the component."""
|
||||
@ -348,3 +375,5 @@ class Info(FollowerComponent):
|
||||
self.manual_page = manual_page
|
||||
self.clients = clients
|
||||
self.donation_url = donation_url
|
||||
if clients:
|
||||
clients_module.validate(clients)
|
||||
|
||||
@ -34,12 +34,12 @@ class Daemon(app.LeaderComponent):
|
||||
is renamed, the new unit file usually contains an Alias= setting in
|
||||
[Install] section with value of old unit name. When the unit is
|
||||
enabled, a symlink with the name of the alias is created. All
|
||||
operations such as is-enabled, is-running and disable work with the
|
||||
alias along with the primary unit name. However, for the case of
|
||||
enabling the unit file, the alias does not work. To be able to provide
|
||||
management for multiple versions of the unit file with different names,
|
||||
specify an alias. Both the names are taken into consideration when
|
||||
enabling the unit file.
|
||||
operations such as is-running and disable work with the alias along
|
||||
with the primary unit name. However, for the case of enabling the unit
|
||||
file or checking its enabled status, the alias does not work. To be
|
||||
able to provide management for multiple versions of the unit file with
|
||||
different names, specify an alias. Both the names are taken into
|
||||
consideration when enabling the unit file.
|
||||
|
||||
"""
|
||||
super().__init__(component_id)
|
||||
@ -51,6 +51,19 @@ class Daemon(app.LeaderComponent):
|
||||
|
||||
def is_enabled(self):
|
||||
"""Return if the daemon/unit is enabled."""
|
||||
if self.alias:
|
||||
# XXX: Handling alias should not be done here. service_is_enabled()
|
||||
# should return True even for an alias. Currently, in addition to
|
||||
# return code we are also checking the printed value. This makes
|
||||
# the implementation less future-proof as new values could printed
|
||||
# by the command. A fixed systemd bug
|
||||
# https://github.com/systemd/systemd/issues/18134 also currently
|
||||
# gives incorrect exit code for 'alias' case. See:
|
||||
# https://salsa.debian.org/freedombox-team/freedombox/-/merge_requests/1980
|
||||
if action_utils.service_is_enabled(self.alias,
|
||||
strict_check=self.strict_check):
|
||||
return True
|
||||
|
||||
return action_utils.service_is_enabled(self.unit,
|
||||
strict_check=self.strict_check)
|
||||
|
||||
|
||||
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
@ -9,13 +9,14 @@ from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.config import get_hostname
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.names.components import DomainType
|
||||
from plinth.signals import domain_added, domain_removed, post_hostname_change
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from .manifest import backup # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
# pylint: disable=C0103
|
||||
|
||||
@ -76,6 +77,10 @@ class AvahiApp(app_module.App):
|
||||
daemon = Daemon('daemon-avahi', managed_services[0])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-avahi',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
if self.is_enabled():
|
||||
domain_added.send_robust(sender='avahi',
|
||||
domain_type='domain-type-local',
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
Application manifest for avahi.
|
||||
"""
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
# Services that intend to make themselves discoverable will drop files into
|
||||
# /etc/avahi/services. Currently, we don't intend to make that customizable.
|
||||
# There is no necessity for backup and restore. This manifest will ensure that
|
||||
# avahi enable/disable setting is preserved.
|
||||
backup = validate_backup({})
|
||||
backup = {}
|
||||
|
||||
@ -75,10 +75,10 @@ def _backup_handler(packet, encryption_passphrase=None):
|
||||
get_valid_filename(packet.path) + '.json')
|
||||
manifests = {
|
||||
'apps': [{
|
||||
'name': app.name,
|
||||
'version': app.app.app.info.version,
|
||||
'backup': app.manifest
|
||||
} for app in packet.apps]
|
||||
'name': component.app.app_id,
|
||||
'version': component.app.info.version,
|
||||
'backup': component.manifest
|
||||
} for component in packet.components]
|
||||
}
|
||||
with open(manifest_path, 'w') as manifest_file:
|
||||
json.dump(manifests, manifest_file)
|
||||
@ -124,9 +124,9 @@ def restore_archive_handler(packet, encryption_passphrase=None):
|
||||
actions.superuser_run('backups', arguments, input=locations_data.encode())
|
||||
|
||||
|
||||
def restore_from_upload(path, apps=None):
|
||||
def restore_from_upload(path, app_ids=None):
|
||||
"""Restore files from an uploaded .tar.gz backup file"""
|
||||
api.restore_apps(_restore_exported_archive_handler, app_names=apps,
|
||||
api.restore_apps(_restore_exported_archive_handler, app_ids=app_ids,
|
||||
create_subvolume=False, backup_file=path)
|
||||
|
||||
|
||||
|
||||
@ -10,79 +10,38 @@ TODO:
|
||||
- Implement unit tests.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from plinth import actions, action_utils, module_loader, setup
|
||||
from plinth import action_utils, actions
|
||||
from plinth import app as app_module
|
||||
from plinth import setup
|
||||
|
||||
from .components import BackupRestore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate(backup):
|
||||
"""Validate the backup' information schema."""
|
||||
assert isinstance(backup, dict)
|
||||
|
||||
if 'config' in backup:
|
||||
assert isinstance(backup['config'], dict)
|
||||
_validate_directories_and_files(backup['config'])
|
||||
|
||||
if 'data' in backup:
|
||||
assert isinstance(backup['data'], dict)
|
||||
_validate_directories_and_files(backup['data'])
|
||||
|
||||
if 'secrets' in backup:
|
||||
assert isinstance(backup['secrets'], dict)
|
||||
_validate_directories_and_files(backup['secrets'])
|
||||
|
||||
if 'services' in backup:
|
||||
assert isinstance(backup['services'], list)
|
||||
for service in backup['services']:
|
||||
assert isinstance(service, (str, dict))
|
||||
if isinstance(service, dict):
|
||||
_validate_service(service)
|
||||
|
||||
return backup
|
||||
|
||||
|
||||
def _validate_directories_and_files(section):
|
||||
"""Validate directories and files keys in a section."""
|
||||
if 'directories' in section:
|
||||
assert isinstance(section['directories'], list)
|
||||
for directory in section['directories']:
|
||||
assert isinstance(directory, str)
|
||||
|
||||
if 'files' in section:
|
||||
assert isinstance(section['files'], list)
|
||||
for file_path in section['files']:
|
||||
assert isinstance(file_path, str)
|
||||
|
||||
|
||||
def _validate_service(service):
|
||||
"""Validate a service manifest provided as a dictionary."""
|
||||
assert isinstance(service['name'], str)
|
||||
assert isinstance(service['type'], str)
|
||||
assert service['type'] in ('apache', 'uwsgi', 'system')
|
||||
if service['type'] == 'apache':
|
||||
assert service['kind'] in ('config', 'site', 'module')
|
||||
|
||||
|
||||
class BackupError:
|
||||
"""Represent an backup/restore operation error."""
|
||||
def __init__(self, error_type, app, hook=None):
|
||||
|
||||
def __init__(self, error_type, component, hook=None):
|
||||
"""Initialize the error object."""
|
||||
self.error_type = error_type
|
||||
self.app = app
|
||||
self.component = component
|
||||
self.hook = hook
|
||||
|
||||
def __eq__(self, other_error):
|
||||
"""Compare to error objects."""
|
||||
return (self.error_type == other_error.error_type
|
||||
and self.app == other_error.app
|
||||
and self.component == other_error.component
|
||||
and self.hook == other_error.hook)
|
||||
|
||||
|
||||
class Packet:
|
||||
"""Information passed to a handlers for backup/restore operations."""
|
||||
def __init__(self, operation, scope, root, apps=None, path=None):
|
||||
|
||||
def __init__(self, operation, scope, root, components=None, path=None):
|
||||
"""Initialize the packet.
|
||||
|
||||
operation is either 'backup' or 'restore.
|
||||
@ -101,7 +60,7 @@ class Packet:
|
||||
self.operation = operation
|
||||
self.scope = scope
|
||||
self.root = root
|
||||
self.apps = apps
|
||||
self.components = components
|
||||
self.path = path
|
||||
self.errors = []
|
||||
|
||||
@ -112,11 +71,11 @@ class Packet:
|
||||
|
||||
def _process_manifests(self):
|
||||
"""Look at manifests and fill up the list of directories/files."""
|
||||
for app in self.apps:
|
||||
for component in self.components:
|
||||
for section in ['config', 'data', 'secrets']:
|
||||
self.directories += app.manifest.get(section, {}).get(
|
||||
'directories', [])
|
||||
self.files += app.manifest.get(section, {}).get('files', [])
|
||||
section = getattr(component, section)
|
||||
self.directories += section.get('directories', [])
|
||||
self.files += section.get('files', [])
|
||||
|
||||
|
||||
def backup_full(backup_handler, path=None):
|
||||
@ -146,25 +105,25 @@ def restore_full(restore_handler):
|
||||
_switch_to_subvolume(subvolume)
|
||||
|
||||
|
||||
def backup_apps(backup_handler, path, app_names=None,
|
||||
def backup_apps(backup_handler, path, app_ids=None,
|
||||
encryption_passphrase=None):
|
||||
"""Backup data belonging to a set of applications."""
|
||||
if not app_names:
|
||||
apps = get_all_apps_for_backup()
|
||||
if not app_ids:
|
||||
components = get_all_components_for_backup()
|
||||
else:
|
||||
apps = get_apps_in_order(app_names)
|
||||
components = get_components_in_order(app_ids)
|
||||
|
||||
if _is_snapshot_available():
|
||||
snapshot = _take_snapshot()
|
||||
backup_root = snapshot['mount_path']
|
||||
snapshotted = True
|
||||
else:
|
||||
_lockdown_apps(apps, lockdown=True)
|
||||
original_state = _shutdown_services(apps)
|
||||
_lockdown_apps(components, lockdown=True)
|
||||
original_state = _shutdown_services(components)
|
||||
backup_root = '/'
|
||||
snapshotted = False
|
||||
|
||||
packet = Packet('backup', 'apps', backup_root, apps, path)
|
||||
packet = Packet('backup', 'apps', backup_root, components, path)
|
||||
_run_operation(backup_handler, packet,
|
||||
encryption_passphrase=encryption_passphrase)
|
||||
|
||||
@ -172,29 +131,29 @@ def backup_apps(backup_handler, path, app_names=None,
|
||||
_delete_snapshot(snapshot)
|
||||
else:
|
||||
_restore_services(original_state)
|
||||
_lockdown_apps(apps, lockdown=False)
|
||||
_lockdown_apps(components, lockdown=False)
|
||||
|
||||
|
||||
def restore_apps(restore_handler, app_names=None, create_subvolume=True,
|
||||
def restore_apps(restore_handler, app_ids=None, create_subvolume=True,
|
||||
backup_file=None, encryption_passphrase=None):
|
||||
"""Restore data belonging to a set of applications."""
|
||||
if not app_names:
|
||||
apps = get_all_apps_for_backup()
|
||||
if not app_ids:
|
||||
components = get_all_components_for_backup()
|
||||
else:
|
||||
apps = get_apps_in_order(app_names)
|
||||
components = get_components_in_order(app_ids)
|
||||
|
||||
_install_apps_before_restore(apps)
|
||||
_install_apps_before_restore(components)
|
||||
|
||||
if _is_snapshot_available() and create_subvolume:
|
||||
subvolume = _create_subvolume(empty=False)
|
||||
restore_root = subvolume['mount_path']
|
||||
else:
|
||||
_lockdown_apps(apps, lockdown=True)
|
||||
original_state = _shutdown_services(apps)
|
||||
_lockdown_apps(components, lockdown=True)
|
||||
original_state = _shutdown_services(components)
|
||||
restore_root = '/'
|
||||
subvolume = False
|
||||
|
||||
packet = Packet('restore', 'apps', restore_root, apps, backup_file)
|
||||
packet = Packet('restore', 'apps', restore_root, components, backup_file)
|
||||
_run_operation(restore_handler, packet,
|
||||
encryption_passphrase=encryption_passphrase)
|
||||
|
||||
@ -202,10 +161,10 @@ def restore_apps(restore_handler, app_names=None, create_subvolume=True,
|
||||
_switch_to_subvolume(subvolume)
|
||||
else:
|
||||
_restore_services(original_state)
|
||||
_lockdown_apps(apps, lockdown=False)
|
||||
_lockdown_apps(components, lockdown=False)
|
||||
|
||||
|
||||
def _install_apps_before_restore(apps):
|
||||
def _install_apps_before_restore(components):
|
||||
"""Install/upgrade apps needed before restoring a backup.
|
||||
|
||||
Upgrading apps to latest version before backups reduces the chance of newer
|
||||
@ -213,92 +172,57 @@ def _install_apps_before_restore(apps):
|
||||
|
||||
"""
|
||||
modules_to_setup = []
|
||||
for backup_app in apps:
|
||||
if backup_app.app.setup_helper.get_state() in ('needs-setup',
|
||||
'needs-update'):
|
||||
modules_to_setup.append(backup_app.name)
|
||||
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)
|
||||
|
||||
setup.run_setup_on_modules(modules_to_setup)
|
||||
|
||||
|
||||
class BackupApp:
|
||||
"""A application that can be backed up and its manifest."""
|
||||
def __init__(self, name, app):
|
||||
"""Initialize object and load manfiest."""
|
||||
self.name = name
|
||||
self.app = app
|
||||
def _get_backup_restore_component(app):
|
||||
"""Return the backup/restore component of the app."""
|
||||
for component in app.components.values():
|
||||
if isinstance(component, BackupRestore):
|
||||
return component
|
||||
|
||||
# Has no backup related meta data
|
||||
raise TypeError
|
||||
|
||||
|
||||
def get_all_components_for_backup():
|
||||
"""Return a list of all components that can be backed up."""
|
||||
components = []
|
||||
|
||||
for app_ in app_module.App.list():
|
||||
try:
|
||||
self.manifest = app.backup
|
||||
except AttributeError:
|
||||
raise TypeError
|
||||
|
||||
self.has_data = bool(app.backup)
|
||||
|
||||
def __eq__(self, other_app):
|
||||
"""Check if this app is same as another."""
|
||||
return self.name == other_app.name and \
|
||||
self.app == other_app.app and \
|
||||
self.manifest == other_app.manifest and \
|
||||
self.has_data == other_app.has_data
|
||||
|
||||
def is_installed(self):
|
||||
"""Return whether app is installed.
|
||||
|
||||
Return true even if the app needs update.
|
||||
"""
|
||||
return self.app.setup_helper.get_state() != 'needs-setup'
|
||||
|
||||
def run_hook(self, hook, packet):
|
||||
"""Run a hook inside an application."""
|
||||
if not hasattr(self.app, hook):
|
||||
return
|
||||
|
||||
try:
|
||||
getattr(self.app, hook)(packet)
|
||||
except Exception as exception:
|
||||
logger.exception(
|
||||
'Error running backup/restore hook for app %s: %s', self.name,
|
||||
exception)
|
||||
packet.errors.append(BackupError('hook', self.app, hook=hook))
|
||||
|
||||
|
||||
def get_all_apps_for_backup():
|
||||
"""Return a list of all applications that can be backed up."""
|
||||
apps = []
|
||||
for module_name, module in module_loader.loaded_modules.items():
|
||||
try:
|
||||
backup_app = BackupApp(module_name, module)
|
||||
if backup_app.is_installed():
|
||||
apps.append(backup_app)
|
||||
module = importlib.import_module(app_.__class__.__module__)
|
||||
if module.setup_helper.get_state() != 'needs-setup':
|
||||
components.append(_get_backup_restore_component(app_))
|
||||
except TypeError: # Application not available for backup/restore
|
||||
pass
|
||||
|
||||
return apps
|
||||
return components
|
||||
|
||||
|
||||
def get_apps_in_order(app_names):
|
||||
"""Return a list of app modules in order of dependency."""
|
||||
apps = []
|
||||
for module_name, module in module_loader.loaded_modules.items():
|
||||
if module_name in app_names:
|
||||
apps.append(BackupApp(module_name, module))
|
||||
def get_components_in_order(app_ids):
|
||||
"""Return a list of backup components in order of app dependencies."""
|
||||
components = []
|
||||
for app_ in app_module.App.list():
|
||||
if app_.app_id in app_ids:
|
||||
components.append(_get_backup_restore_component(app_))
|
||||
|
||||
return apps
|
||||
return components
|
||||
|
||||
|
||||
def _lockdown_apps(apps, lockdown):
|
||||
def _lockdown_apps(components, lockdown):
|
||||
"""Mark apps as in/out of lockdown mode and disable all user interaction.
|
||||
|
||||
This is a flag in the app module. It will enforced by a middleware that
|
||||
will intercept all interaction and show a lockdown message.
|
||||
|
||||
"""
|
||||
for app in apps:
|
||||
app.app.locked = lockdown
|
||||
|
||||
# XXX: Lockdown the application UI by implementing a middleware
|
||||
for component in components:
|
||||
component.app.locked = lockdown
|
||||
|
||||
|
||||
def _is_snapshot_available():
|
||||
@ -347,6 +271,7 @@ def _switch_to_subvolume(subvolume):
|
||||
|
||||
class ServiceHandler:
|
||||
"""Abstraction to help with service shutdown/restart."""
|
||||
|
||||
@staticmethod
|
||||
def create(backup_app, service):
|
||||
service_type = 'system'
|
||||
@ -381,6 +306,7 @@ class ServiceHandler:
|
||||
|
||||
class SystemServiceHandler(ServiceHandler):
|
||||
"""Handle starting and stopping of system services for backup."""
|
||||
|
||||
def __init__(self, backup_app, service):
|
||||
"""Initialize the object."""
|
||||
super().__init__(backup_app, service)
|
||||
@ -400,6 +326,7 @@ class SystemServiceHandler(ServiceHandler):
|
||||
|
||||
class ApacheServiceHandler(ServiceHandler):
|
||||
"""Handle starting and stopping of Apache services for backup."""
|
||||
|
||||
def __init__(self, backup_app, service):
|
||||
"""Initialize the object."""
|
||||
super().__init__(backup_app, service)
|
||||
@ -424,18 +351,19 @@ class ApacheServiceHandler(ServiceHandler):
|
||||
['enable', '--name', self.web_name, '--kind', self.kind])
|
||||
|
||||
|
||||
def _shutdown_services(apps):
|
||||
"""Shutdown all services specified by manifests.
|
||||
def _shutdown_services(components):
|
||||
"""Shutdown all services specified by backup manifests.
|
||||
|
||||
- Services are shutdown in the reverse order of the apps listing.
|
||||
- Services are shutdown in the reverse order of the components listing.
|
||||
|
||||
Return the current state of the services so they can be restored
|
||||
accurately.
|
||||
|
||||
"""
|
||||
state = []
|
||||
for app in apps:
|
||||
for service in app.manifest.get('services', []):
|
||||
state.append(ServiceHandler.create(app, service))
|
||||
for component in components:
|
||||
for service in component.services:
|
||||
state.append(ServiceHandler.create(component, service))
|
||||
|
||||
for service in reversed(state):
|
||||
service.stop()
|
||||
@ -480,8 +408,14 @@ def _run_hooks(hook, packet):
|
||||
|
||||
"""
|
||||
logger.info('Running %s hooks', hook)
|
||||
for app in packet.apps:
|
||||
app.run_hook(hook, packet)
|
||||
for component in packet.components:
|
||||
try:
|
||||
getattr(component, hook)(packet)
|
||||
except Exception as exception:
|
||||
logger.exception(
|
||||
'Error running backup/restore hook for app %s: %s',
|
||||
component.app.app_id, exception)
|
||||
packet.errors.append(BackupError('hook', component, hook=hook))
|
||||
|
||||
|
||||
def _run_operation(handler, packet, encryption_passphrase=None):
|
||||
|
||||
99
plinth/modules/backups/components.py
Normal file
99
plinth/modules/backups/components.py
Normal file
@ -0,0 +1,99 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
App component for other apps to use backup/restore functionality.
|
||||
"""
|
||||
|
||||
from plinth import app
|
||||
|
||||
|
||||
def _validate_directories_and_files(section):
|
||||
"""Validate directories and files keys in a section."""
|
||||
if not section:
|
||||
return
|
||||
|
||||
assert isinstance(section, dict)
|
||||
|
||||
if 'directories' in section:
|
||||
assert isinstance(section['directories'], list)
|
||||
for directory in section['directories']:
|
||||
assert isinstance(directory, str)
|
||||
|
||||
if 'files' in section:
|
||||
assert isinstance(section['files'], list)
|
||||
for file_path in section['files']:
|
||||
assert isinstance(file_path, str)
|
||||
|
||||
|
||||
def _validate_services(services):
|
||||
"""Validate services manifest provided as list."""
|
||||
if not services:
|
||||
return
|
||||
|
||||
assert isinstance(services, list)
|
||||
for service in services:
|
||||
assert isinstance(service, (str, dict))
|
||||
if isinstance(service, dict):
|
||||
_validate_service(service)
|
||||
|
||||
|
||||
def _validate_service(service):
|
||||
"""Validate a service manifest provided as a dictionary."""
|
||||
assert isinstance(service['name'], str)
|
||||
assert isinstance(service['type'], str)
|
||||
assert service['type'] in ('apache', 'uwsgi', 'system')
|
||||
if service['type'] == 'apache':
|
||||
assert service['kind'] in ('config', 'site', 'module')
|
||||
|
||||
|
||||
class BackupRestore(app.FollowerComponent):
|
||||
"""Component to backup/restore an app."""
|
||||
|
||||
def __init__(self, component_id, config=None, data=None, secrets=None,
|
||||
services=None):
|
||||
"""Initialize the backup/restore component."""
|
||||
super().__init__(component_id)
|
||||
|
||||
_validate_directories_and_files(config)
|
||||
self.config = config or {}
|
||||
_validate_directories_and_files(data)
|
||||
self.data = data or {}
|
||||
_validate_directories_and_files(secrets)
|
||||
self.secrets = secrets or {}
|
||||
_validate_services(services)
|
||||
self.services = services or []
|
||||
|
||||
self.has_data = bool(config) or bool(data) or bool(secrets)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Check if this component is same as another."""
|
||||
return self.component_id == other.component_id
|
||||
|
||||
@property
|
||||
def manifest(self):
|
||||
"""Return the backup details as a dictionary."""
|
||||
manifest = {}
|
||||
if self.config:
|
||||
manifest['config'] = self.config
|
||||
|
||||
if self.secrets:
|
||||
manifest['secrets'] = self.secrets
|
||||
|
||||
if self.data:
|
||||
manifest['data'] = self.data
|
||||
|
||||
if self.services:
|
||||
manifest['services'] = self.services
|
||||
|
||||
return manifest
|
||||
|
||||
def backup_pre(self, packet):
|
||||
"""Perform any special operations before backup."""
|
||||
|
||||
def backup_post(self, packet):
|
||||
"""Perform any special operations after backup."""
|
||||
|
||||
def restore_pre(self, packet):
|
||||
"""Perform any special operations before restore."""
|
||||
|
||||
def restore_post(self, packet):
|
||||
"""Perform any special operations after restore."""
|
||||
@ -24,16 +24,16 @@ from .repository import get_repositories
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_app_choices(apps):
|
||||
"""Return a list of check box multiple choices from list of apps."""
|
||||
def _get_app_choices(components):
|
||||
"""Return a list of check box multiple choices from list of components."""
|
||||
choices = []
|
||||
for app in apps:
|
||||
name = app.app.app.info.name
|
||||
if not app.has_data:
|
||||
for component in components:
|
||||
name = component.app.info.name
|
||||
if not component.has_data:
|
||||
name = ugettext('{app} (No data to backup)').format(
|
||||
app=app.app.app.info.name)
|
||||
app=component.app.info.name)
|
||||
|
||||
choices.append((app.name, name))
|
||||
choices.append((component.app_id, name))
|
||||
|
||||
return choices
|
||||
|
||||
@ -59,9 +59,12 @@ class CreateArchiveForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the form with selectable apps."""
|
||||
super().__init__(*args, **kwargs)
|
||||
apps = api.get_all_apps_for_backup()
|
||||
self.fields['selected_apps'].choices = _get_app_choices(apps)
|
||||
self.fields['selected_apps'].initial = [app.name for app in apps]
|
||||
components = api.get_all_components_for_backup()
|
||||
choices = _get_app_choices(components)
|
||||
self.fields['selected_apps'].choices = choices
|
||||
self.fields['selected_apps'].initial = [
|
||||
choice[0] for choice in choices
|
||||
]
|
||||
self.fields['repository'].choices = _get_repository_choices()
|
||||
|
||||
|
||||
@ -72,10 +75,13 @@ class RestoreForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the form with selectable apps."""
|
||||
apps = kwargs.pop('apps')
|
||||
components = kwargs.pop('components')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['selected_apps'].choices = _get_app_choices(apps)
|
||||
self.fields['selected_apps'].initial = [app.name for app in apps]
|
||||
choices = _get_app_choices(components)
|
||||
self.fields['selected_apps'].choices = choices
|
||||
self.fields['selected_apps'].initial = [
|
||||
choice[0] for choice in choices
|
||||
]
|
||||
|
||||
|
||||
class UploadForm(forms.Form):
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
Application manifest for backups.
|
||||
"""
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
# Currently, backup application does not have any settings. However, settings
|
||||
# such as scheduler settings, backup location, secrets to connect to remove
|
||||
# servers need to be backed up.
|
||||
backup = validate_backup({})
|
||||
backup = {}
|
||||
|
||||
@ -158,6 +158,7 @@ class BaseBorgRepository(abc.ABC):
|
||||
|
||||
def remove(self):
|
||||
"""Remove a borg repository"""
|
||||
|
||||
def list_archives(self):
|
||||
"""Return list of archives in this repository."""
|
||||
output = self.run(['list-repo', '--path', self.borg_path])
|
||||
@ -165,12 +166,12 @@ class BaseBorgRepository(abc.ABC):
|
||||
return sorted(archives, key=lambda archive: archive['start'],
|
||||
reverse=True)
|
||||
|
||||
def create_archive(self, archive_name, app_names):
|
||||
def create_archive(self, archive_name, app_ids):
|
||||
"""Create a new archive in this repository with given name."""
|
||||
archive_path = self._get_archive_path(archive_name)
|
||||
passphrase = self.credentials.get('encryption_passphrase', None)
|
||||
api.backup_apps(_backup_handler, path=archive_path,
|
||||
app_names=app_names, encryption_passphrase=passphrase)
|
||||
api.backup_apps(_backup_handler, path=archive_path, app_ids=app_ids,
|
||||
encryption_passphrase=passphrase)
|
||||
|
||||
def delete_archive(self, archive_name):
|
||||
"""Delete an archive with given name from this repository."""
|
||||
@ -222,6 +223,7 @@ class BaseBorgRepository(abc.ABC):
|
||||
|
||||
def get_download_stream(self, archive_name):
|
||||
"""Return an stream of .tar.gz binary data for a backup archive."""
|
||||
|
||||
class BufferedReader(io.BufferedReader):
|
||||
"""Improve performance of buffered binary streaming.
|
||||
|
||||
@ -235,6 +237,7 @@ class BaseBorgRepository(abc.ABC):
|
||||
binary data.
|
||||
|
||||
"""
|
||||
|
||||
def __next__(self):
|
||||
"""Override to call read() instead of readline()."""
|
||||
chunk = self.read(io.DEFAULT_BUFFER_SIZE)
|
||||
@ -279,11 +282,11 @@ class BaseBorgRepository(abc.ABC):
|
||||
output = self.run(['get-archive-apps', '--path', archive_path])
|
||||
return output.splitlines()
|
||||
|
||||
def restore_archive(self, archive_name, apps=None):
|
||||
def restore_archive(self, archive_name, app_ids=None):
|
||||
"""Restore an archive from this repository to the system."""
|
||||
archive_path = self._get_archive_path(archive_name)
|
||||
passphrase = self.credentials.get('encryption_passphrase', None)
|
||||
api.restore_apps(restore_archive_handler, app_names=apps,
|
||||
api.restore_apps(restore_archive_handler, app_ids=app_ids,
|
||||
create_subvolume=False, backup_file=archive_path,
|
||||
encryption_passphrase=passphrase)
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
<tr id="archive-{{ archive.name }}" class="archive">
|
||||
<td class="archive-name">{{ archive.name }}</td>
|
||||
<td class="archive-operations">
|
||||
<a class="archive-export btn btn-sm btn-default" target="_blank"
|
||||
<a class="archive-export btn btn-sm btn-default"
|
||||
href="{% url 'backups:download' uuid archive.name %}">
|
||||
{% trans "Download" %}
|
||||
</a>
|
||||
|
||||
@ -8,13 +8,18 @@ from unittest.mock import MagicMock, call, patch
|
||||
import pytest
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
from plinth.app import App
|
||||
|
||||
from .. import api, forms, repository
|
||||
from ..components import BackupRestore
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
setup_helper = MagicMock()
|
||||
|
||||
|
||||
def _get_test_manifest(name):
|
||||
return api.validate({
|
||||
return {
|
||||
'config': {
|
||||
'directories': ['/etc/' + name + '/config.d/'],
|
||||
'files': ['/etc/' + name + '/config'],
|
||||
@ -32,50 +37,42 @@ def _get_test_manifest(name):
|
||||
'name': name,
|
||||
'kind': 'site'
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def _get_backup_app(name):
|
||||
"""Return a dummy BackupApp object."""
|
||||
return api.BackupApp(name, MagicMock(backup=_get_test_manifest(name)))
|
||||
def _get_backup_component(name):
|
||||
"""Return a BackupRestore component."""
|
||||
return BackupRestore(name, **_get_test_manifest(name))
|
||||
|
||||
|
||||
class TestBackupApp:
|
||||
"""Test the BackupApp class."""
|
||||
@staticmethod
|
||||
def test_run_hook():
|
||||
"""Test running a hook on an application."""
|
||||
packet = api.Packet('backup', 'apps', '/', [])
|
||||
hook = 'testhook_pre'
|
||||
app = MagicMock()
|
||||
backup_app = api.BackupApp('app_name', app)
|
||||
backup_app.run_hook(hook, packet)
|
||||
class AppTest(App):
|
||||
"""Sample App for testing."""
|
||||
app_id = 'test-app'
|
||||
|
||||
app.testhook_pre.assert_has_calls([call(packet)])
|
||||
assert not packet.errors
|
||||
|
||||
app.testhook_pre.reset_mock()
|
||||
app.testhook_pre.side_effect = Exception()
|
||||
backup_app.run_hook(hook, packet)
|
||||
assert packet.errors == [api.BackupError('hook', app, hook=hook)]
|
||||
|
||||
del app.testhook_pre
|
||||
backup_app.run_hook(hook, packet)
|
||||
def _get_test_app(name):
|
||||
"""Return an App."""
|
||||
app = AppTest()
|
||||
app.app_id = name
|
||||
app._all_apps[name] = app
|
||||
app.add(_get_backup_component(name + '-component'))
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('load_cfg')
|
||||
class TestBackupProcesses:
|
||||
"""Test cases for backup processes"""
|
||||
|
||||
@staticmethod
|
||||
def test_packet_process_manifests():
|
||||
def test_packet_collected_files_directories():
|
||||
"""Test that directories/files are collected from manifests."""
|
||||
apps = [_get_backup_app('a'), _get_backup_app('b')]
|
||||
packet = api.Packet('backup', 'apps', '/', apps)
|
||||
for app in apps:
|
||||
components = [_get_backup_component('a'), _get_backup_component('b')]
|
||||
packet = api.Packet('backup', 'apps', '/', components)
|
||||
for component in components:
|
||||
for section in ['config', 'data', 'secrets']:
|
||||
for directory in app.manifest[section]['directories']:
|
||||
for directory in getattr(component, section)['directories']:
|
||||
assert directory in packet.directories
|
||||
for file_path in app.manifest[section]['files']:
|
||||
for file_path in getattr(component, section)['files']:
|
||||
assert file_path in packet.files
|
||||
|
||||
@staticmethod
|
||||
@ -97,50 +94,56 @@ class TestBackupProcesses:
|
||||
restore_handler.assert_called_once()
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.module_loader.loaded_modules.items')
|
||||
def test_get_all_apps_for_backup(modules):
|
||||
"""Test listing apps supporting backup and needing backup."""
|
||||
apps = [
|
||||
('a', MagicMock(backup=_get_test_manifest('a'))),
|
||||
('b', MagicMock(backup=_get_test_manifest('b'))),
|
||||
('c', MagicMock(backup=None)),
|
||||
('d', MagicMock()),
|
||||
]
|
||||
del apps[3][1].backup
|
||||
modules.return_value = apps
|
||||
@patch('importlib.import_module')
|
||||
@patch('plinth.app.App.list')
|
||||
def test_get_all_components_for_backup(apps_list, import_module):
|
||||
"""Test listing components supporting backup and needing backup."""
|
||||
modules = [MagicMock(), MagicMock(), MagicMock()]
|
||||
import_module.side_effect = modules
|
||||
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_apps = api.get_all_apps_for_backup()
|
||||
expected_apps = [
|
||||
api.BackupApp('a', apps[0][1]),
|
||||
api.BackupApp('b', apps[1][1]),
|
||||
api.BackupApp('c', apps[2][1])
|
||||
returned_components = api.get_all_components_for_backup()
|
||||
expected_components = [
|
||||
apps[0].components['a-component'],
|
||||
apps[2].components['c-component']
|
||||
]
|
||||
assert returned_apps == expected_apps
|
||||
assert returned_components == expected_components
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.module_loader.loaded_modules.items')
|
||||
def test_get_apps_in_order(modules):
|
||||
"""Test that apps are listed in correct dependency order."""
|
||||
@patch('plinth.app.App.list')
|
||||
def test_get_components_in_order(apps_list):
|
||||
"""Test that components are listed in correct dependency order."""
|
||||
apps = [
|
||||
('names', MagicMock(backup=_get_test_manifest('names'))),
|
||||
('config', MagicMock(backup=_get_test_manifest('config'))),
|
||||
_get_test_app('names'),
|
||||
_get_test_app('other'),
|
||||
_get_test_app('config')
|
||||
]
|
||||
modules.return_value = apps
|
||||
apps_list.return_value = apps
|
||||
|
||||
app_names = ['config', 'names']
|
||||
apps = api.get_apps_in_order(app_names)
|
||||
assert apps[0].name == 'names'
|
||||
assert apps[1].name == 'config'
|
||||
app_ids = ['config', 'names']
|
||||
components = api.get_components_in_order(app_ids)
|
||||
assert len(components) == 2
|
||||
assert components[0].app_id == 'names'
|
||||
assert components[1].app_id == 'config'
|
||||
|
||||
@staticmethod
|
||||
def test__lockdown_apps():
|
||||
"""Test that locked flag is set for each app."""
|
||||
app_a = MagicMock(locked=False)
|
||||
app_b = MagicMock(locked=None)
|
||||
apps = [MagicMock(app=app_a), MagicMock(app=app_b)]
|
||||
api._lockdown_apps(apps, True)
|
||||
assert app_a.locked is True
|
||||
assert app_b.locked is True
|
||||
apps = [_get_test_app('test-app-1'), _get_test_app('test-app-2')]
|
||||
components = [
|
||||
apps[0].components['test-app-1-component'],
|
||||
apps[1].components['test-app-2-component']
|
||||
]
|
||||
|
||||
api._lockdown_apps(components, True)
|
||||
assert apps[0].locked
|
||||
assert apps[1].locked
|
||||
|
||||
api._lockdown_apps(components, False)
|
||||
assert not apps[0].locked
|
||||
assert not apps[1].locked
|
||||
|
||||
@staticmethod
|
||||
@patch('plinth.action_utils.webserver_is_enabled')
|
||||
@ -148,19 +151,20 @@ class TestBackupProcesses:
|
||||
@patch('plinth.actions.superuser_run')
|
||||
def test__shutdown_services(run, service_is_running, webserver_is_enabled):
|
||||
"""Test that services are stopped in correct order."""
|
||||
apps = [_get_backup_app('a'), _get_backup_app('b')]
|
||||
components = [_get_backup_component('a'), _get_backup_component('b')]
|
||||
service_is_running.return_value = True
|
||||
webserver_is_enabled.return_value = True
|
||||
state = api._shutdown_services(apps)
|
||||
state = api._shutdown_services(components)
|
||||
|
||||
expected_state = [
|
||||
api.ServiceHandler.create(apps[0],
|
||||
apps[0].manifest['services'][0]),
|
||||
api.ServiceHandler.create(apps[0],
|
||||
apps[0].manifest['services'][1]),
|
||||
api.ServiceHandler.create(apps[1],
|
||||
apps[1].manifest['services'][0]),
|
||||
api.ServiceHandler.create(apps[1], apps[1].manifest['services'][1])
|
||||
api.ServiceHandler.create(components[0],
|
||||
components[0].services[0]),
|
||||
api.ServiceHandler.create(components[0],
|
||||
components[0].services[1]),
|
||||
api.ServiceHandler.create(components[1],
|
||||
components[1].services[0]),
|
||||
api.ServiceHandler.create(components[1],
|
||||
components[1].services[1]),
|
||||
]
|
||||
assert state == expected_state
|
||||
|
||||
@ -207,21 +211,26 @@ class TestBackupProcesses:
|
||||
@staticmethod
|
||||
def test__run_operation():
|
||||
"""Test that operation runs handler and app hooks."""
|
||||
apps = [_get_backup_app('a'), _get_backup_app('b')]
|
||||
packet = api.Packet('backup', 'apps', '/', apps)
|
||||
packet.apps[0].run_hook = MagicMock()
|
||||
packet.apps[1].run_hook = MagicMock()
|
||||
components = [_get_backup_component('a'), _get_backup_component('b')]
|
||||
packet = api.Packet('backup', 'apps', '/', components)
|
||||
packet.components[0].backup_pre = MagicMock()
|
||||
packet.components[0].backup_post = MagicMock()
|
||||
packet.components[1].backup_pre = MagicMock()
|
||||
packet.components[1].backup_post = MagicMock()
|
||||
handler = MagicMock()
|
||||
api._run_operation(handler, packet)
|
||||
handler.assert_has_calls([call(packet, encryption_passphrase=None)])
|
||||
|
||||
calls = [call('backup_pre', packet), call('backup_post', packet)]
|
||||
packet.apps[0].run_hook.assert_has_calls(calls)
|
||||
packet.apps[1].run_hook.assert_has_calls(calls)
|
||||
calls = [call(packet)]
|
||||
packet.components[0].backup_pre.assert_has_calls(calls)
|
||||
packet.components[0].backup_post.assert_has_calls(calls)
|
||||
packet.components[1].backup_pre.assert_has_calls(calls)
|
||||
packet.components[1].backup_post.assert_has_calls(calls)
|
||||
|
||||
|
||||
class TestBackupModule:
|
||||
"""Tests of the backups django module, like views or forms."""
|
||||
|
||||
@staticmethod
|
||||
def test_file_upload():
|
||||
# posting a video should fail
|
||||
|
||||
212
plinth/modules/backups/tests/test_components.py
Normal file
212
plinth/modules/backups/tests/test_components.py
Normal file
@ -0,0 +1,212 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Test the App components provides by backups app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from .. import components
|
||||
from ..components import BackupRestore
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.fixture(name='backup_restore')
|
||||
def fixture_backup_restore():
|
||||
"""Fixture to create a domain type after clearing all existing ones."""
|
||||
value = {'files': ['a', 'b'], 'directories': ['a', 'b']}
|
||||
services = ['service-1', {'type': 'system', 'name': 'service-2'}]
|
||||
return BackupRestore('test-backup-restore', config=value, data=value,
|
||||
secrets=value, services=services)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('section', [
|
||||
None,
|
||||
{
|
||||
'directories': ['a', 'b']
|
||||
},
|
||||
{
|
||||
'files': ['a', 'b']
|
||||
},
|
||||
{
|
||||
'directories': ['a'],
|
||||
'files': ['a']
|
||||
},
|
||||
{
|
||||
'extra': 'value'
|
||||
},
|
||||
])
|
||||
def test_valid_directories_and_files(section):
|
||||
"""Test that valid values of files and directories."""
|
||||
components._validate_directories_and_files(section)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('section', [
|
||||
'invalid',
|
||||
10,
|
||||
['invalid'],
|
||||
{
|
||||
'files': None
|
||||
},
|
||||
{
|
||||
'files': 10
|
||||
},
|
||||
{
|
||||
'files': {}
|
||||
},
|
||||
{
|
||||
'files': [10],
|
||||
},
|
||||
{
|
||||
'files': [None],
|
||||
},
|
||||
{
|
||||
'files': [[]],
|
||||
},
|
||||
{
|
||||
'directories': None
|
||||
},
|
||||
{
|
||||
'directories': [10],
|
||||
},
|
||||
])
|
||||
def test_invalid_directories_and_files(section):
|
||||
"""Test that invalid values of files and directories."""
|
||||
with pytest.raises(AssertionError):
|
||||
components._validate_directories_and_files(section)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('services', [
|
||||
None,
|
||||
[],
|
||||
['service'],
|
||||
[{
|
||||
'type': 'uwsgi',
|
||||
'name': 'service'
|
||||
}],
|
||||
[{
|
||||
'type': 'system',
|
||||
'name': 'service'
|
||||
}],
|
||||
[{
|
||||
'type': 'apache',
|
||||
'name': 'service',
|
||||
'kind': 'config'
|
||||
}],
|
||||
[{
|
||||
'type': 'apache',
|
||||
'name': 'service',
|
||||
'kind': 'site'
|
||||
}],
|
||||
[{
|
||||
'type': 'apache',
|
||||
'name': 'service',
|
||||
'kind': 'module'
|
||||
}],
|
||||
])
|
||||
def test_valid_services(services):
|
||||
"""Test that valid values of services."""
|
||||
components._validate_services(services)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('services', [
|
||||
10,
|
||||
'invalid',
|
||||
[10],
|
||||
[[]],
|
||||
[{}],
|
||||
[{
|
||||
'type': 'invalid',
|
||||
'name': 'service'
|
||||
}],
|
||||
[{
|
||||
'type': 10,
|
||||
'name': 'service'
|
||||
}],
|
||||
[{
|
||||
'type': 'system',
|
||||
'name': 10
|
||||
}],
|
||||
[{
|
||||
'type': 'system',
|
||||
'name': None
|
||||
}],
|
||||
[{
|
||||
'type': 'system',
|
||||
'name': []
|
||||
}],
|
||||
[{
|
||||
'type': 'apache',
|
||||
'name': 'service'
|
||||
}],
|
||||
[{
|
||||
'type': 'apache',
|
||||
'name': 'service',
|
||||
'kind': 'invalid-kind'
|
||||
}],
|
||||
])
|
||||
def test_invalid_services(services):
|
||||
"""Test that invalid values of services."""
|
||||
with pytest.raises((AssertionError, KeyError)):
|
||||
components._validate_services(services)
|
||||
|
||||
|
||||
def test_backup_restore_init_default_arguments():
|
||||
"""Test initialization of the backup restore object."""
|
||||
component = BackupRestore('test-backup-restore')
|
||||
assert component.component_id == 'test-backup-restore'
|
||||
assert component.config == {}
|
||||
assert component.data == {}
|
||||
assert component.secrets == {}
|
||||
assert component.services == []
|
||||
assert not component.has_data
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key', ['config', 'data', 'secrets'])
|
||||
def test_backup_restore_init(key):
|
||||
"""Test initialization of the backup restore object."""
|
||||
with pytest.raises(AssertionError):
|
||||
BackupRestore('test-backup-restore', **{key: 'invalid-value'})
|
||||
|
||||
value = {'files': ['a', 'b'], 'directories': ['a', 'b']}
|
||||
component = BackupRestore('test-backup-restore', **{key: value})
|
||||
assert getattr(component, key) == value
|
||||
assert component.has_data
|
||||
|
||||
|
||||
def test_backup_restore_init_services():
|
||||
"""Test initialization of the backup restore object."""
|
||||
with pytest.raises(AssertionError):
|
||||
BackupRestore('test-backup-restore', services='invalid-value')
|
||||
|
||||
services = ['service-1', {'type': 'system', 'name': 'service-2'}]
|
||||
component = BackupRestore('test-backup-restore', services=services)
|
||||
assert component.services == services
|
||||
assert not component.has_data
|
||||
|
||||
|
||||
def test_backup_restore_equal(backup_restore):
|
||||
"""Test equality operator on the backup restore object."""
|
||||
assert backup_restore == BackupRestore('test-backup-restore')
|
||||
assert backup_restore != BackupRestore('test-different')
|
||||
|
||||
|
||||
def test_backup_restore_manifest(backup_restore):
|
||||
"""Test manifest retrieval from backup restore object."""
|
||||
manifest = backup_restore.manifest
|
||||
assert isinstance(manifest, dict)
|
||||
assert manifest['config'] == backup_restore.config
|
||||
assert manifest['data'] == backup_restore.data
|
||||
assert manifest['secrets'] == backup_restore.secrets
|
||||
assert manifest['services'] == backup_restore.services
|
||||
|
||||
assert BackupRestore('test-backup-restore').manifest == {}
|
||||
|
||||
|
||||
def test_backup_restore_hooks(backup_restore):
|
||||
"""Test running hooks on backup restore object."""
|
||||
packet = None
|
||||
backup_restore.backup_pre(packet)
|
||||
backup_restore.backup_post(packet)
|
||||
backup_restore.restore_pre(packet)
|
||||
backup_restore.restore_post(packet)
|
||||
@ -147,7 +147,7 @@ class BaseRestoreView(SuccessMessageMixin, FormView):
|
||||
"""Pass additional keyword args for instantiating the form."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
included_apps = self._get_included_apps()
|
||||
kwargs['apps'] = api.get_apps_in_order(included_apps)
|
||||
kwargs['components'] = api.get_components_in_order(included_apps)
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@ -11,9 +11,10 @@ from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import frontpage, menu
|
||||
from plinth.modules.apache.components import Uwsgi, Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
|
||||
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 2
|
||||
|
||||
@ -64,7 +65,7 @@ class BepastyApp(app_module.App):
|
||||
icon_filename='bepasty',
|
||||
short_description=_('File & Snippet Sharing'),
|
||||
description=_description, manual_page='bepasty',
|
||||
clients=clients)
|
||||
clients=manifest.clients)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-bepasty', info.name,
|
||||
@ -75,7 +76,7 @@ class BepastyApp(app_module.App):
|
||||
shortcut = frontpage.Shortcut('shortcut-bepasty', info.name,
|
||||
info.short_description,
|
||||
info.icon_filename, '/bepasty',
|
||||
clients=clients)
|
||||
clients=manifest.clients)
|
||||
self.add(shortcut)
|
||||
|
||||
firewall = Firewall('firewall-bepasty', info.name,
|
||||
@ -89,6 +90,10 @@ class BepastyApp(app_module.App):
|
||||
urls=['https://{host}/bepasty/'])
|
||||
self.add(webserver)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-bepasty',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
|
||||
@ -2,18 +2,15 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.clients import validate
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
clients = validate([{
|
||||
clients = [{
|
||||
'name': _('bepasty'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/bepasty'
|
||||
}]
|
||||
}])
|
||||
}]
|
||||
|
||||
backup = validate_backup({
|
||||
backup = {
|
||||
'config': {
|
||||
'files': ['/etc/bepasty-freedombox.conf']
|
||||
},
|
||||
@ -21,4 +18,4 @@ backup = validate_backup({
|
||||
'directories': ['/var/lib/bepasty']
|
||||
},
|
||||
'services': ['uwsgi'],
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,10 +14,11 @@ from plinth import actions
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from .manifest import backup # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 2
|
||||
|
||||
@ -96,6 +97,10 @@ class BindApp(app_module.App):
|
||||
alias=managed_services[1])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-bind',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
|
||||
@ -3,11 +3,9 @@
|
||||
Application manifest for bind.
|
||||
"""
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
backup = validate_backup({
|
||||
backup = {
|
||||
'config': {
|
||||
'files': ['/etc/bind/named.conf.options']
|
||||
},
|
||||
'services': ['bind9']
|
||||
})
|
||||
}
|
||||
|
||||
@ -13,11 +13,12 @@ from plinth import app as app_module
|
||||
from plinth import cfg, frontpage, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 1
|
||||
|
||||
@ -60,7 +61,7 @@ class CalibreApp(app_module.App):
|
||||
name=_('calibre'), icon_filename='calibre',
|
||||
short_description=_('E-book Library'),
|
||||
description=_description, manual_page='Calibre',
|
||||
clients=clients,
|
||||
clients=manifest.clients,
|
||||
donation_url='https://calibre-ebook.com/donate')
|
||||
self.add(info)
|
||||
|
||||
@ -94,6 +95,10 @@ class CalibreApp(app_module.App):
|
||||
groups=groups)
|
||||
self.add(users_and_groups)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-calibre',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
|
||||
@ -2,20 +2,17 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.clients import validate
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
clients = validate([{
|
||||
clients = [{
|
||||
'name': _('calibre'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/calibre/'
|
||||
}]
|
||||
}])
|
||||
}]
|
||||
|
||||
backup = validate_backup({
|
||||
backup = {
|
||||
'data': {
|
||||
'directories': ['/var/lib/private/calibre-server-freedombox/']
|
||||
},
|
||||
'services': ['calibre-server-freedombox']
|
||||
})
|
||||
}
|
||||
|
||||
@ -12,12 +12,12 @@ from plinth import cfg, frontpage, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules import names
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.signals import domain_added, domain_removed
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import utils
|
||||
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
||||
from . import manifest, utils
|
||||
|
||||
version = 1
|
||||
|
||||
@ -65,7 +65,7 @@ class CockpitApp(app_module.App):
|
||||
icon='fa-wrench', icon_filename='cockpit',
|
||||
short_description=_('Server Administration'),
|
||||
description=_description, manual_page='Cockpit',
|
||||
clients=clients)
|
||||
clients=manifest.clients)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-cockpit', info.name,
|
||||
@ -92,6 +92,10 @@ class CockpitApp(app_module.App):
|
||||
daemon = Daemon('daemon-cockpit', managed_services[0])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-cockpit',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
domain_added.connect(on_domain_added)
|
||||
domain_removed.connect(on_domain_removed)
|
||||
|
||||
|
||||
@ -5,19 +5,16 @@ Application manifest for cockpit.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
from plinth.clients import validate
|
||||
|
||||
clients = validate([{
|
||||
clients = [{
|
||||
'name': _('Cockpit'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/_cockpit/'
|
||||
}]
|
||||
}])
|
||||
}]
|
||||
|
||||
# cockpit.conf need not be backed up because add/remove domain signals are
|
||||
# triggered on every Plinth domain change (and cockpit application install) and
|
||||
# will set the value of allowed domains correctly. This is the only key the is
|
||||
# customized in cockpit.conf.
|
||||
backup = validate_backup({})
|
||||
backup = {}
|
||||
|
||||
@ -13,11 +13,12 @@ from plinth import app as app_module
|
||||
from plinth import menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules import names
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
|
||||
from .manifest import backup # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 1
|
||||
|
||||
@ -86,6 +87,10 @@ class CoturnApp(app_module.App):
|
||||
reserved_usernames=['turnserver'])
|
||||
self.add(users_and_groups)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-coturn',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
|
||||
@ -1,10 +1,3 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
backup = validate_backup({
|
||||
'secrets': {
|
||||
'directories': ['/etc/coturn']
|
||||
},
|
||||
'services': ['coturn']
|
||||
})
|
||||
backup = {'secrets': {'directories': ['/etc/coturn']}, 'services': ['coturn']}
|
||||
|
||||
@ -10,8 +10,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from plinth import app as app_module
|
||||
from plinth import menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
|
||||
from .manifest import backup # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 2
|
||||
|
||||
@ -79,6 +80,10 @@ class DateTimeApp(app_module.App):
|
||||
daemon = Daemon('daemon-datetime', managed_services[0])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-datetime',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
def diagnose(self):
|
||||
"""Run diagnostics and return the results."""
|
||||
results = super().diagnose()
|
||||
|
||||
@ -3,6 +3,4 @@
|
||||
Application manifest for datetime.
|
||||
"""
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
backup = validate_backup({'data': {'files': ['/etc/timezone']}})
|
||||
backup = {'data': {'files': ['/etc/timezone']}}
|
||||
|
||||
@ -10,11 +10,12 @@ from plinth import app as app_module
|
||||
from plinth import frontpage, menu
|
||||
from plinth.daemon import Daemon
|
||||
from plinth.modules.apache.components import Webserver
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.firewall.components import Firewall
|
||||
from plinth.modules.users import add_user_to_share_group
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
|
||||
from .manifest import backup, clients # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 6
|
||||
|
||||
@ -50,7 +51,8 @@ class DelugeApp(app_module.App):
|
||||
app_id=self.app_id, version=version, name=_('Deluge'),
|
||||
icon_filename='deluge',
|
||||
short_description=_('BitTorrent Web Client'),
|
||||
description=_description, manual_page='Deluge', clients=clients,
|
||||
description=_description, manual_page='Deluge',
|
||||
clients=manifest.clients,
|
||||
donation_url='https://www.patreon.com/deluge_cas')
|
||||
self.add(info)
|
||||
|
||||
@ -88,6 +90,10 @@ class DelugeApp(app_module.App):
|
||||
groups=groups)
|
||||
self.add(users_and_groups)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-deluge',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
|
||||
@ -2,21 +2,18 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.clients import validate
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
clients = validate([{
|
||||
clients = [{
|
||||
'name': _('Deluge'),
|
||||
'description': _('Bittorrent client written in Python/PyGTK'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/deluge'
|
||||
}]
|
||||
}])
|
||||
}]
|
||||
|
||||
backup = validate_backup({
|
||||
backup = {
|
||||
'config': {
|
||||
'directories': ['/var/lib/deluged/.config']
|
||||
},
|
||||
'services': ['deluged', 'deluge-web']
|
||||
})
|
||||
}
|
||||
|
||||
@ -15,14 +15,14 @@ Scenario: Enable deluge application
|
||||
|
||||
Scenario: User of 'bit-torrent' group
|
||||
Given the deluge application is enabled
|
||||
When I create a user named delugeuser with password deluge&456 in group bit-torrent
|
||||
And I'm logged in as the user delugeuser with password deluge&456
|
||||
And the user delugeuser in group bit-torrent exists
|
||||
When I'm logged in as the user delugeuser
|
||||
Then the deluge site should be available
|
||||
|
||||
Scenario: User not of 'bit-torrent' group
|
||||
Given the deluge application is enabled
|
||||
When I create a user named nogroupuser with password somep@ssw6
|
||||
And I'm logged in as the user nogroupuser with password somep@ssw6
|
||||
And the user nogroupuser exists
|
||||
When I'm logged in as the user nogroupuser
|
||||
Then the deluge site should not be available
|
||||
|
||||
Scenario: Upload a torrent to deluge
|
||||
|
||||
@ -16,8 +16,9 @@ from django.utils.translation import ugettext_noop
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, daemon, glib, menu
|
||||
from plinth.modules.apache.components import diagnose_url_on_all
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
|
||||
from .manifest import backup # noqa, pylint: disable=unused-import
|
||||
from . import manifest
|
||||
|
||||
version = 1
|
||||
|
||||
@ -57,6 +58,10 @@ class DiagnosticsApp(app_module.App):
|
||||
'diagnostics:index', parent_url_name='system')
|
||||
self.add(menu_item)
|
||||
|
||||
backup_restore = BackupRestore('backup-restore-diagnostics',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
# Check periodically for low RAM space
|
||||
interval = 180 if cfg.develop else 3600
|
||||
glib.schedule(interval, _warn_about_low_ram_space)
|
||||
|
||||
@ -3,6 +3,4 @@
|
||||
Application manifest for diagnostics.
|
||||
"""
|
||||
|
||||
from plinth.modules.backups.api import validate as validate_backup
|
||||
|
||||
backup = validate_backup({})
|
||||
backup = {}
|
||||
|
||||
@ -52,8 +52,6 @@ _description = [
|
||||
' federate with other diaspora* pods.')
|
||||
]
|
||||
|
||||
from .manifest import clients # noqa pylint:disable=E402 isort:skip
|
||||
|
||||
app = None
|
||||
|
||||
|
||||
@ -65,10 +63,12 @@ class DiasporaApp(app_module.App):
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
from . import manifest
|
||||
info = app_module.Info(app_id=self.app_id, version=version,
|
||||
name=_('diaspora*'), icon_filename='diaspora',
|
||||
short_description=_('Federated Social Network'),
|
||||
description=_description, clients=clients)
|
||||
description=_description,
|
||||
clients=manifest.clients)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-diaspora', info.name,
|
||||
|
||||
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