...
 
Commits (59)
......@@ -7,7 +7,6 @@ before_script:
- apt-get install -y build-essential # Build dependencies
- apt-get install -y sshpass parted # Test dependencies
- apt-get install -y sudo
- apt-get remove -y python3.7* libpython3.7* # Use python3.6 when installing dependencies
- apt-mark hold fuse # not installable in CI environment
- apt-get install -y $(./run --list-dependencies) # Module dependencies
......@@ -18,7 +17,7 @@ stages:
run-unit-tests:
stage: test
script:
- apt-get install -y python3-coverage
- apt-get install -y python3-coverage libjs-jquery libjs-jquery-isonscreen libjs-jquery-tablesorter libjs-jquery-throttle-debounce
- adduser tester --gecos "First Last,RoomNumber,WorkPhone,HomePhone" --disabled-password
- echo "tester:password" | chpasswd
- cp -r . /home/tester/plinth
......@@ -33,7 +32,6 @@ run-unit-tests:
build-debian-package:
stage: package
script:
- apt-get build-dep -y . # Re-install python3.7 again
- DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -us -uc -b
- mkdir debian-package
- mv ../plinth*.deb debian-package
......
......@@ -13,7 +13,7 @@ Naming conventions:
* 'Code change', 'patch', and 'commit' are used interchangeably.
* 'Author' and 'contributor' are used interchangeably.
* Git 'log' and 'history' are used interchangeably.
* PR - pull request
* PR, MR - pull request and merge request, used interchangeably.
* 'Merging' often means 'applying a patch to git history' in a general sense,
not literal execution of the command `git merge`.
......@@ -49,6 +49,8 @@ Naming conventions:
to a [Developer Certificate of Origin](http://developercertificate.org/).
* If (part of) your code changes were inspired or plainly copied from another
source, please indicate this in the PR, so the reviewer can handle it.
* If your PR is not ready for merging yet, the title of your PR must start with
`WIP:`
* Have fun contributing :)
......@@ -82,6 +84,9 @@ Naming conventions:
* In case more fundamental changes are necessary, or if the contributor is new,
try to encourage them to make changes by giving appropriate feedback. This is
a major way how we mentor new contributors.
* Any PR whose title starts with `WIP:` cannot be merged. Communicate with the
author on what the pending changes are. Get the author to complete them or
complete them yourself in case of an emergency.
* Have fun reviewing :)
......
......@@ -47,10 +47,12 @@ $ sudo /vagrant/run --develop
Plinth will be available at https://localhost:4430/plinth (with
an invalid SSL certificate).
"
config.trigger.before :destroy do |trigger|
trigger.warn = "Dropping Plinth database"
trigger.run_remote = {
inline: "rm /vagrant/data/var/lib/plinth/plinth.sqlite3"
trigger.warn = "Performing cleanup steps"
trigger.run = {
path: "post-box-destroy.py"
}
end
config.vm.boot_timeout=1200
end
This diff is collapsed.
......@@ -154,6 +154,11 @@ def upgrade_config():
(not current_version or current_version > MOD_IRC_DEPRECATED_VERSION):
conf['modules'].pop('mod_irc')
# BOSH port was changed from 5280 to 5443.
for listen_port in conf['listen']:
if listen_port['port'] == 5280:
listen_port['port'] = 5443
# Write changes back to the file
with open(EJABBERD_CONFIG, 'w') as file_handle:
ruamel.yaml.round_trip_dump(conf, file_handle)
......
......@@ -22,6 +22,10 @@ Configuration helper for FreedomBox firewall interface.
import argparse
import subprocess
import augeas
from plinth import action_utils
def parse_arguments():
"""Return parsed command line arguments as dictionary"""
......@@ -68,9 +72,29 @@ def parse_arguments():
return parser.parse_args()
def set_firewall_backend(backend):
"""Set FirewallBackend attribute to the specified string."""
conf_file = '/etc/firewalld/firewalld.conf'
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# lens for shell-script config file
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
aug.set('/augeas/load/Shellvars/incl[last() + 1]', conf_file)
aug.load()
aug.set('/files/{}/FirewallBackend'.format(conf_file),
'{}'.format(backend))
aug.save()
action_utils.service_enable('firewalld')
action_utils.service_restart('firewalld')
def subcommand_setup(_):
"""Perform basic firewalld setup."""
subprocess.call(['firewall-cmd', '--set-default-zone=external'])
set_firewall_backend('nftables')
add_service('external', 'http')
add_service('internal', 'http')
......
......@@ -22,13 +22,14 @@ Configuration helper for OpenVPN server.
import argparse
import os
import subprocess
import augeas
from plinth import action_utils
from plinth import action_utils, utils
KEYS_DIRECTORY = '/etc/openvpn/freedombox-keys'
DH_KEY = '/etc/openvpn/freedombox-keys/dh4096.pem'
DH_KEY = '/etc/openvpn/freedombox-keys/pki/dh.pem'
OLD_SERVER_CONFIGURATION_PATH = '/etc/openvpn/freedombox.conf'
SERVER_CONFIGURATION_PATH = '/etc/openvpn/server/freedombox.conf'
......@@ -36,19 +37,21 @@ SERVER_CONFIGURATION_PATH = '/etc/openvpn/server/freedombox.conf'
OLD_SERVICE_NAME = 'openvpn@freedombox'
SERVICE_NAME = 'openvpn-server@freedombox'
CA_CERTIFICATE_PATH = KEYS_DIRECTORY + '/ca.crt'
USER_CERTIFICATE_PATH = KEYS_DIRECTORY + '/{username}.crt'
USER_KEY_PATH = KEYS_DIRECTORY + '/{username}.key'
ATTR_FILE = KEYS_DIRECTORY + '/index.txt.attr'
CA_CERTIFICATE_PATH = os.path.join(KEYS_DIRECTORY, 'pki', 'ca.crt')
USER_CERTIFICATE_PATH = os.path.join(KEYS_DIRECTORY, 'pki', 'issued',
'{username}.crt')
USER_KEY_PATH = os.path.join(KEYS_DIRECTORY, 'pki', 'private',
'{username}.key')
ATTR_FILE = os.path.join(KEYS_DIRECTORY, 'pki', 'index.txt.attr')
SERVER_CONFIGURATION = '''
port 1194
proto udp
dev tun
ca /etc/openvpn/freedombox-keys/ca.crt
cert /etc/openvpn/freedombox-keys/server.crt
key /etc/openvpn/freedombox-keys/server.key
dh /etc/openvpn/freedombox-keys/dh4096.pem
ca /etc/openvpn/freedombox-keys/pki/ca.crt
cert /etc/openvpn/freedombox-keys/pki/issued/server.crt
key /etc/openvpn/freedombox-keys/pki/private/server.key
dh /etc/openvpn/freedombox-keys/pki/dh.pem
server 10.91.0.0 255.255.255.0
keepalive 10 120
cipher AES-256-CBC
......@@ -75,19 +78,20 @@ verb 3
{key}</key>'''
CERTIFICATE_CONFIGURATION = {
'KEY_CONFIG': '/usr/share/easy-rsa/openssl-1.0.0.cnf',
'EASYRSA_BATCH': '1',
'EASYRSA_KEY_SIZE': '4096',
'KEY_CONFIG': '/usr/share/easy-rsa/openssl-easyrsa.cnf',
'KEY_DIR': KEYS_DIRECTORY,
'OPENSSL': 'openssl',
'KEY_SIZE': '4096',
'CA_EXPIRE': '3650',
'KEY_EXPIRE': '3650',
'KEY_COUNTRY': 'US',
'KEY_PROVINCE': 'NY',
'KEY_CITY': 'New York',
'KEY_ORG': 'FreedomBox',
'KEY_EMAIL': 'me@freedombox',
'KEY_OU': 'Home',
'KEY_NAME': 'FreedomBox'
'EASYRSA_OPENSSL': 'openssl',
'EASYRSA_CA_EXPIRE': '3650',
'EASYRSA_REQ_EXPIRE': '3650',
'EASYRSA_REQ_COUNTRY': 'US',
'EASYRSA_REQ_PROVINCE': 'NY',
'EASYRSA_REQ_CITY': 'New York',
'EASYRSA_REQ_ORG': 'FreedomBox',
'EASYRSA_REQ_EMAIL': 'me@freedombox',
'EASYRSA_REQ_OU': 'Home',
'EASYRSA_REQ_NAME': 'FreedomBox'
}
COMMON_ARGS = {'env': CERTIFICATE_CONFIGURATION, 'cwd': KEYS_DIRECTORY}
......@@ -116,7 +120,7 @@ def parse_arguments():
def subcommand_is_setup(_):
"""Return whether setup is complete."""
print('true' if os.path.isfile(DH_KEY) else 'false')
print('true' if utils.is_non_empty_file(DH_KEY) else 'false')
def subcommand_setup(_):
......@@ -143,9 +147,6 @@ def subcommand_upgrade(_):
def _create_server_config():
"""Write server configuration."""
if os.path.exists(SERVER_CONFIGURATION_PATH):
return
with open(SERVER_CONFIGURATION_PATH, 'w') as file_handle:
file_handle.write(SERVER_CONFIGURATION)
......@@ -167,12 +168,15 @@ def _create_certificates():
except FileExistsError:
pass
subprocess.check_call(['/usr/share/easy-rsa/clean-all'], **COMMON_ARGS)
subprocess.check_call(['/usr/share/easy-rsa/pkitool', '--initca'],
subprocess.check_call(['/usr/share/easy-rsa/easyrsa', 'init-pki'],
**COMMON_ARGS)
subprocess.check_call(
['/usr/share/easy-rsa/pkitool', '--server', 'server'], **COMMON_ARGS)
subprocess.check_call(['/usr/share/easy-rsa/build-dh'], **COMMON_ARGS)
['/usr/share/easy-rsa/easyrsa', 'build-ca', 'nopass'], **COMMON_ARGS)
subprocess.check_call([
'/usr/share/easy-rsa/easyrsa', 'build-server-full', 'server', 'nopass'
], **COMMON_ARGS)
subprocess.check_call(['/usr/share/easy-rsa/easyrsa', 'gen-dh'],
**COMMON_ARGS)
def subcommand_get_profile(arguments):
......@@ -189,8 +193,10 @@ def subcommand_get_profile(arguments):
if not _is_non_empty_file(user_certificate) or \
not _is_non_empty_file(user_key):
set_unique_subject('no') # Set unique subject in attribute file to no
subprocess.check_call(['/usr/share/easy-rsa/pkitool', username],
**COMMON_ARGS)
subprocess.check_call([
'/usr/share/easy-rsa/easyrsa', 'build-client-full', username,
'nopass'
], **COMMON_ARGS)
user_certificate_string = _read_file(user_certificate)
user_key_string = _read_file(user_key)
......@@ -223,8 +229,8 @@ def _is_non_empty_file(filepath):
def load_augeas():
"""Initialize Augeas."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
augeas.Augeas.NO_MODL_AUTOLOAD)
# shell-script config file lens
aug.set('/augeas/load/Simplevars/lens', 'Simplevars.lns')
......
......@@ -138,7 +138,7 @@ def subcommand_list(_):
'description')
snapshots = []
for line in lines[2:]:
parts = [part.strip() for part in line.split('|')]
parts = [part.strip('* ') for part in line.split('|')]
snapshots.append(dict(zip(keys, parts)))
default = _get_default_snapshot()
......
#!/usr/bin/python3
# -*- mode: python -*-
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Actions for sshfs.
"""
import argparse
import json
import os
import subprocess
import sys
TIMEOUT = 5
class AlreadyMountedError(Exception):
pass
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
mount = subparsers.add_parser('mount', help='mount an ssh filesystem')
mount.add_argument('--mountpoint', help='Local mountpoint', required=True)
mount.add_argument('--path', help='Remote ssh path to mount',
required=True)
mount.add_argument('--ssh-keyfile', help='Path of private ssh key',
default=None, required=False)
umount = subparsers.add_parser('umount',
help='unmount an ssh filesystem')
umount.add_argument('--mountpoint', help='Mountpoint to unmount',
required=True)
is_mounted = subparsers.add_parser(
'is-mounted', help='Check whether a mountpoint is mounted')
is_mounted.add_argument('--mountpoint', help='Mountpoint to check',
required=True)
subparsers.required = True
return parser.parse_args()
def subcommand_mount(arguments):
"""Show repository information."""
try:
validate_mountpoint(arguments.mountpoint)
except AlreadyMountedError:
return
remote_path = arguments.path
kwargs = {}
# the shell would expand ~/ to the local home directory
remote_path = remote_path.replace('~/', '').replace('~', '')
cmd = ['sshfs', remote_path, arguments.mountpoint, '-o',
'UserKnownHostsFile=/dev/null', '-o',
'StrictHostKeyChecking=no']
if arguments.ssh_keyfile:
cmd += ['-o', 'IdentityFile=$SSHKEY']
else:
password = read_password()
if not password:
raise ValueError('mount requires either a password or ssh_keyfile')
cmd += ['-o', 'password_stdin']
kwargs['input'] = password.encode()
subprocess.run(cmd, check=True, timeout=TIMEOUT, **kwargs)
def subcommand_umount(arguments):
"""Unmount a mountpoint."""
run(['umount', arguments.mountpoint])
def validate_mountpoint(mountpoint):
"""Check that the folder is empty, and create it if it doesn't exist"""
if os.path.exists(mountpoint):
if _is_mounted(mountpoint):
raise AlreadyMountedError('Mountpoint %s already mounted' %
mountpoint)
if os.listdir(mountpoint) or not os.path.isdir(mountpoint):
raise ValueError('Mountpoint %s is not an empty directory' %
mountpoint)
else:
os.makedirs(mountpoint)
def _is_mounted(mountpoint):
"""Return boolean whether a local directory is a mountpoint."""
cmd = ['mountpoint', '-q', mountpoint]
# mountpoint exits with status non-zero if it didn't find a mountpoint
try:
subprocess.run(cmd, check=True)
return True
except subprocess.CalledProcessError:
return False
def subcommand_is_mounted(arguments):
print(json.dumps(_is_mounted(arguments.mountpoint)))
def read_password():
"""Read the password from stdin."""
if sys.stdin.isatty():
return ''
else:
return ''.join(sys.stdin)
def run(cmd, env=None, check=True):
"""Wrap the command with ssh password or keyfile authentication"""
# Set a timeout to not get stuck if the remote server asks for a password.
subprocess.run(cmd, check=check, env=env, timeout=TIMEOUT)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()
# Proxy for BOSH server
ProxyPass /bosh/ http://localhost:5280/bosh/
ProxyPassReverse /bosh/ http://localhost:5280/bosh/
<Proxy http://localhost:5280/bosh/*>
ProxyPass /bosh/ http://localhost:5443/bosh/
ProxyPassReverse /bosh/ http://localhost:5443/bosh/
<Proxy http://localhost:5443/bosh/*>
Require all granted
</Proxy>
plinth (0.44.0) unstable; urgency=medium
[ Pavel Borecki ]
* Translated using Weblate (Czech)
[ Robert Martinez ]
* Add gray noise background
* Add white Card
* add footer padding
[ Allan Nordhøy ]
* Translated using Weblate (Norwegian Bokmål)
[ James Valleroy ]
* ejabberd: bosh port moved to 5443
* apache: Run setup again to reload
* ejabberd: Change BOSH port from 5280 to 5443
* Revert "ci: Use python3.6 when installing dependencies"
* ci: Install jquery packages for coverage
* functional_tests: Confirm when deleting all snapshots
* Translated using Weblate (Spanish)
* Update translation strings
[ Joseph Nuthalapati ]
* vagrant: clear logs and plinth database on destroying box
* minetest: Change list of mods to what's available in Debian
* Add instructions on how to use "WIP" in merge requests
* clients: Fix distortion of the client apps buttons
* snapshots: Fix default snapshot listing
* firewalld: Use nftables instead of iptables
* snapshots: Place the subsubmenu below the description
[ ssantos ]
* Translated using Weblate (German)
* Translated using Weblate (Portuguese)
[ Prachi Srivastava ]
* Changes delete all to delete selected in snapshot
* Adds toggle to select all for deletion
* Changes functional test to select All and delete snapshots
* Ignores warnings in pytest while running functional test
[ advocatux ]
* Translated using Weblate (Spanish)
[ Petter Reinholdtsen ]
* Translated using Weblate (Norwegian Bokmål)
-- James Valleroy <jvalleroy@mailbox.org> Mon, 03 Dec 2018 19:47:04 -0500
plinth (0.43.0) unstable; urgency=medium
[ Michael Pimmer ]
* Backups: export and download archives in one step
* Backups: uploading and import with temporarily stored file
* Backups: Restore directly from archive
* Backups: Don't fail when borg doesn't find files to extract
* Backups: clean up exporting archives functionality
* Backups: relative paths for borg extract in action script
* Backups: fix test
* Backups: clean up forms, names and templates
* Functional tests: minor documentation changes
* Backups: Stream archive downloads/exports
* Backups: do not hardcode uploaded backup file path
* Backups: minor cleanups
* Backups: show free disk space on upload+restore page
* Backups: functional test to download and restore an archive
* Backups: minor adaption of upload file size warning
* Backups: minor fixes of functional tests
* Functional tests: check that browser waits for redirects to finish
* Functional tests: fix waiting for redirects
* Functional tests: assert that module installation succeeded
* Cherrypy: Do not limit maximum upload size
* Backups: Make Manifest a dict instead of a list
[ James Valleroy ]
* functional_tests: Remove backup export steps
* functional_tests: Remove remaining backup export steps
* functional_tests: Add sso tags
* upgrades: Internationalize string and apply minor formatting
[ Anthony Stalker ]
* Translated using Weblate (Czech)
[ Joseph Nuthalapati ]
* vagrant: Destroy Plinth development database when box is destroyed
* sso: Make auth-pubtkt tickets valid for 12 hours
* openvpn: Migration from easy-rsa 2 to 3
* openvpn: is-setup checks for non-empty dh.pem file
* openvpn: Always write the latest server configuration on setup
[ ssantos ]
* Translated using Weblate (Portuguese)
[ Robert Martinez ]
* Update module terminology improvements
* Incorporate feedback from MR
-- James Valleroy <jvalleroy@mailbox.org> Mon, 19 Nov 2018 17:25:31 -0500
plinth (0.42.0) unstable; urgency=medium
[ Robert Martinez ]
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -2,3 +2,4 @@
url = https://localhost:4430
username = tester
password = testingtesting
delete_root_backup_archives = true
......@@ -67,6 +67,6 @@ Scenario: Download, upload and restore a backup
When I set bind forwarders to 1.1.1.1
And I create a backup of the bind app data
And I set bind forwarders to 1.0.0.1
And I download the bind app data backup
And I restore the downloaded bind app data backup
And I download the latest app data backup
And I restore the downloaded app data backup
Then bind forwarders should be 1.1.1.1
; Ignore warnings temporary fix
; TODO:// Remove when https://github.com/pytest-dev/pytest-splinter/issues/112 is resolved
[pytest]
addopts = -p no:warnings
\ No newline at end of file
......@@ -221,12 +221,12 @@ def set_mediawiki_admin_password(browser, password):
@when(parsers.parse('I enable message archive management'))
def set_mediawiki_admin_password(browser):
def mediawiki_enable_archive_management(browser):
application.enable_ejabberd_message_archive_management(browser)
@when(parsers.parse('I disable message archive management'))
def set_mediawiki_admin_password(browser):
def mediawiki_disable_archive_management(browser):
application.disable_ejabberd_message_archive_management(browser)
......
......@@ -21,7 +21,7 @@ import time
from pytest import fixture
from pytest_bdd import given, parsers, then, when
from support import system
from support import config, system
language_codes = {
'Deutsch': 'de',
......@@ -178,15 +178,14 @@ def dynamicdns_has_original_config(browser):
@when(parsers.parse('I create a backup of the {app_name:w} app data'))
def backup_create(browser, app_name):
if config.getboolean('DEFAULT', 'delete_root_backup_archives'):
system.backup_delete_root_archives(browser)
system.backup_create(browser, app_name)
@when(parsers.parse('I download the {app_name:w} app data backup'))
def backup_download(browser, app_name, downloaded_file_info):
url = '/plinth/sys/backups/export-and-download/_functional_test_%s/' % \
app_name
file_path = system.download_file_logged_in(browser, url, app_name,
suffix='.tar.gz')
@when(parsers.parse('I download the latest app data backup'))
def backup_download(browser, downloaded_file_info):
file_path = system.download_latest_backup(browser)
downloaded_file_info['path'] = file_path
......@@ -195,7 +194,7 @@ def backup_restore(browser, app_name):
system.backup_restore(browser, app_name)
@when(parsers.parse('I restore the downloaded {app_name:w} app data backup'))
@when(parsers.parse('I restore the downloaded app data backup'))
def backup_restore_from_upload(browser, app_name, downloaded_file_info):
path = downloaded_file_info["path"]
try:
......
......@@ -84,8 +84,11 @@ def check_language(browser, language_code):
def delete_all_snapshots(browser):
browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/all/delete')
submit(browser)
browser.visit(config['DEFAULT']['url'] + '/plinth/sys/snapshot/manage/')
browser.find_by_name('select_all').check()
submit(browser, browser.find_by_name('delete_selected'))
submit(browser, browser.find_by_name('delete_confirm'))
def create_snapshot(browser):
......@@ -184,17 +187,22 @@ def dynamicdns_change_config(browser):
submit(browser)
def backup_delete_root_archives(browser):
"""Delete all archives of the root borg repository"""
browser.visit(default_url + '/plinth/sys/backups/')
path = "//a[starts-with(@href,'/plinth/sys/backups/delete/root/')]"
while browser.find_by_xpath(path):
browser.find_by_xpath(path).first.click()
with wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
submit(browser)
def backup_create(browser, app_name):
browser.visit(default_url)
application.install(browser, 'backups')
delete = browser.find_link_by_href(
'/plinth/sys/backups/delete/_functional_test_' + app_name + '/')
if delete:
delete.first.click()
submit(browser)
browser.find_link_by_href('/plinth/sys/backups/create/').first.click()
browser.find_by_id('id_backups-name').fill('_functional_test_' + app_name)
for app in browser.find_by_css('input[type=checkbox]'):
app.uncheck()
......@@ -207,9 +215,9 @@ def backup_create(browser, app_name):
def backup_restore(browser, app_name):
browser.visit(default_url)
nav_to_module(browser, 'backups')
browser.find_link_by_href(
'/plinth/sys/backups/restore-archive/_functional_test_' +
app_name + '/').first.click()
path = "//a[starts-with(@href,'/plinth/sys/backups/restore-archive/root/')]"
# assume that want to restore the last (most recently created) backup
browser.find_by_xpath(path).last.click()
with wait_for_page_update(browser, expected_url='/plinth/sys/backups/'):
submit(browser)
......@@ -227,10 +235,20 @@ def backup_upload_and_restore(browser, app_name, downloaded_file_path):
submit(browser)
def download_file_logged_in(browser, url_path, app_names, suffix=''):
def download_latest_backup(browser):
nav_to_module(browser, 'backups')
path = "//a[starts-with(@href,'/plinth/sys/backups/download/root')]"
ele = browser.driver.find_elements_by_xpath(path)[0]
url = ele.get_attribute('href')
file_path = download_file_logged_in(browser, url, suffix='.tar.gz')
return file_path
def download_file_logged_in(browser, url, suffix=''):
"""Download a file from Plinth, pretend being logged in via cookies"""
current_url = urlparse(browser.url)
url = "%s://%s%s" % (current_url.scheme, current_url.netloc, url_path)
if not url.startswith("http"):
current_url = urlparse(browser.url)
url = "%s://%s%s" % (current_url.scheme, current_url.netloc, url)
cookies = browser.driver.get_cookies()
cookies = {cookie["name"]: cookie["value"] for cookie in cookies}
response = requests.get(url, verify=False, cookies=cookies)
......@@ -267,7 +285,7 @@ def pagekite_configure(browser, host, port, kite_name, kite_secret):
"""Configure pagekite basic parameters."""
nav_to_module(browser, 'pagekite')
browser.find_link_by_href('/plinth/sys/pagekite/configure/').first.click()
#time.sleep(0.250) # Wait for 200ms show animation to complete
# time.sleep(0.250) # Wait for 200ms show animation to complete
browser.fill('pagekite-server_domain', host)
browser.fill('pagekite-server_port', str(port))
browser.fill('pagekite-kite_name', kite_name)
......
......@@ -18,4 +18,4 @@
Package init file.
"""
__version__ = '0.42.0'
__version__ = '0.44.0'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -20,7 +20,7 @@ FreedomBox app for Apache server.
from plinth import actions
version = 2
version = 3
is_essential = True
......
This diff is collapsed.
......@@ -98,7 +98,7 @@ class BackupError:
class Packet:
"""Information passed to a handlers for backup/restore operations."""
def __init__(self, operation, scope, root, apps=None, label=None):
def __init__(self, operation, scope, root, apps=None, path=None):
"""Initialize the packet.
operation is either 'backup' or 'restore.
......@@ -110,8 +110,7 @@ class Packet:
All paths populated are relative to the 'root' path. The root path
itself must not be stored in the backup.
label is either an archive name (w/o path), or the full path of an
exported archive.
path is the full path of an (possibly exported) archive.
TODO: create two variables out of it as it's distinct information.
"""
......@@ -119,7 +118,7 @@ class Packet:
self.scope = scope
self.root = root
self.apps = apps
self.label = label
self.path = path
self.errors = []
self.directories = []
......@@ -136,7 +135,7 @@ class Packet:
self.files += app.manifest.get(section, {}).get('files', [])
def backup_full(backup_handler, label=None):
def backup_full(backup_handler, path=None):
"""Backup the entire system."""
if not _is_snapshot_available():
raise Exception('Full backup is not supported without snapshots.')
......@@ -144,7 +143,7 @@ def backup_full(backup_handler, label=None):
snapshot = _take_snapshot()
backup_root = snapshot['mount_path']
packet = Packet('backup', 'full', backup_root, label)
packet = Packet('backup', 'full', backup_root, path)
_run_operation(backup_handler, packet)
_delete_snapshot(snapshot)
......@@ -163,7 +162,8 @@ def restore_full(restore_handler):
_switch_to_subvolume(subvolume)
def backup_apps(backup_handler, app_names=None, label=None):
def backup_apps(backup_handler, path, app_names=None,
encryption_passphrase=None):
"""Backup data belonging to a set of applications."""
if not app_names:
apps = get_all_apps_for_backup()
......@@ -180,8 +180,9 @@ def backup_apps(backup_handler, app_names=None, label=None):
backup_root = '/'
snapshotted = False
packet = Packet('backup', 'apps', backup_root, apps, label)
_run_operation(backup_handler, packet)
packet = Packet('backup', 'apps', backup_root, apps, path)
_run_operation(backup_handler, packet,
encryption_passphrase=encryption_passphrase)
if snapshotted:
_delete_snapshot(snapshot)
......@@ -191,7 +192,7 @@ def backup_apps(backup_handler, app_names=None, label=None):
def restore_apps(restore_handler, app_names=None, create_subvolume=True,
backup_file=None):
backup_file=None, encryption_passphrase=None):
"""Restore data belonging to a set of applications."""
if not app_names:
apps = get_all_apps_for_backup()
......@@ -209,7 +210,8 @@ def restore_apps(restore_handler, app_names=None, create_subvolume=True,
subvolume = False
packet = Packet('restore', 'apps', restore_root, apps, backup_file)
_run_operation(restore_handler, packet)
_run_operation(restore_handler, packet,
encryption_passphrase=encryption_passphrase)
if subvolume:
_switch_to_subvolume(subvolume)
......@@ -480,8 +482,8 @@ def _run_hooks(hook, packet):
app.run_hook(hook, packet)
def _run_operation(handler, packet):
def _run_operation(handler, packet, encryption_passphrase=None):
"""Run handler and pre/post hooks for backup/restore operations."""
_run_hooks(packet.operation + '_pre', packet)
handler(packet)
handler(packet, encryption_passphrase=encryption_passphrase)
_run_hooks(packet.operation + '_post', packet)
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from plinth.errors import PlinthError
class BorgError(PlinthError):
"""Generic borg errors"""
pass
class BorgRepositoryDoesNotExistError(BorgError):
"""Borg access to a repository works but the repository does not exist"""
pass
class SshfsError(PlinthError):
"""Generic sshfs errors"""
pass
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.