mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-22 10:01:45 +00:00
nextcloud: Add new app based on podman container
Nextcloud has long been a desired app for FreedomBox, however, due to difficulties around Debian packaging, it hasn't yet been implemented. This branch creates an app for Nextcloud with the help of Podman. Podman is a containarization technology, like Docker, but with some extra features that make its integration into FreedomBox convenient. If the podman approach turns out to be favorable, we should consider writing a podman specific module. How does this work? The app installs the podman and default-mysql-server Debian packages. In other apps, such as MediaWiki, FreedomBox chooses SQLite which eases maintenance and backup/restore. However, this would bring a significant performance degrade for Nextcloud, hence the choice of Mysql. Other apps, like Wordpress already use Mysql, so it is installed on the host as opposed to installing it in a sepatate container. A firewalld rich rule is created, so the Nextcloud container can communicate with the db and OpenLDAP. The podman package comes with a systemd service and timer for automatically upgrading containers that are labeled as "io.containers.autoupdate=registry". podman-auto-update.timer is managed by FreedomBox. We might add a drop-in file for the timer to make it more consistent with unattended-upgrades. Podman natively supports creating systemd services for individual containers. The generated systemd service is then managed by FreedomBox. The current container image is based on Debian and runs apache inside the container. To avoid running apache redundantly (both on the host and inside the container) it would be preferable to use the nextcloud:stable-fpm image instead, which seems to require creating a new virtual host. Configure /.well-known URIs to redirect to /nextcloud. There is a conflict with Radicale if both apps are running. Put the podman1 interface into the trusted firewalld zone. This results in the container gaining Internet access which is necessary to downloading Nextcloud applications and federating with other Nextcloud instances. After applying the patches, I opened my instance to the Internet to make sure this configuration doesn't accidentally expose services. I scanned TCP ports 3306 and 6379 (after installing and binding redis-server to the bridge interface). After that, I installed the Tor app and put the default WAN interface to the external zone to confirm that port 9050/TCP does not get exposed through the bridge interface. To-do: * test the fpm image * test running the container in rootless mode for better security Signed-off-by: Benedek Nagy <contact@nbenedek.me> [sunil: Add missing docstrings] [sunil: Make some methods private to module] [sunil: Run yapf for formatting] [sunil: Remove a comment to hide form when app is disabled] [sunil: Update form labels] [sunil: I18N for client names] [sunil: Reduce number success messages in form for easy i18n and consistency] [sunil: Reorganize patch series, squash fixes] [sunil: Tweak auto update daemon component's ID] [sunil: Add blank lines for formatting] [sunil: Minor refactoring for _run_occ method] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
c169537975
commit
3d8967a20a
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -175,6 +175,12 @@ Comment: https://github.com/mumble-voip/mumble/blob/master/icons/mumble.svg
|
||||
https://github.com/mumble-voip/mumble/blob/master/LICENSE
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: plinth/modules/nextcloud/static/icons/nextcloud.png
|
||||
plinth/modules/nextcloud/static/icons/nextcloud.svg
|
||||
Copyright: 2016 Nextcloud GmbH.
|
||||
Comment: https://nextcloud.com/trademarks/
|
||||
License: AGPL-3+
|
||||
|
||||
Files: plinth/modules/openvpn/static/icons/openvpn.png
|
||||
Copyright: 2017 Kishan Raval
|
||||
Comment: https://github.com/thekishanraval/Logos
|
||||
|
||||
123
plinth/modules/nextcloud/__init__.py
Normal file
123
plinth/modules/nextcloud/__init__.py
Normal file
@ -0,0 +1,123 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to configure Nextcloud."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import frontpage, menu
|
||||
from plinth.config import DropinConfigs
|
||||
from plinth.daemon import Daemon, SharedDaemon
|
||||
from plinth.modules.apache.components import Webserver, diagnose_url
|
||||
from plinth.modules.firewall.components import (Firewall,
|
||||
FirewallLocalProtection)
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import manifest, privileged
|
||||
|
||||
_description = [
|
||||
_('Nextcloud is a self-hosted productivity platform which provides '
|
||||
'private and secure functions for file sharing, collaborative work, '
|
||||
'and more. Nextcloud includes the Nextcloud server, client applications '
|
||||
'for desktop computers, and mobile clients. The Nextcloud server '
|
||||
'provides a well integrated web interface.'),
|
||||
_('All users of FreedomBox can use Nextcloud.'),
|
||||
_('To perform administrative actions, use the '
|
||||
f'<strong>"{privileged.GUI_ADMIN}"</strong> user.'),
|
||||
_('You can set a new password in the "Configuration" section below.'),
|
||||
_('Please note, that Nextcloud isn\'t maintained by Debian, which means '
|
||||
'security and feature updates are applied independently.')
|
||||
]
|
||||
|
||||
|
||||
class NextcloudApp(app_module.App):
|
||||
"""FreedomBox app for Nextcloud."""
|
||||
|
||||
app_id = 'nextcloud'
|
||||
|
||||
_version = 1
|
||||
|
||||
configure_when_disabled = False
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(
|
||||
app_id=self.app_id, version=self._version, name=_('Nextcloud'),
|
||||
icon_filename='nextcloud',
|
||||
short_description=_('File Storage & Collaboration'),
|
||||
description=_description, manual_page='Nextcloud',
|
||||
clients=manifest.clients)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-nextcloud', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'nextcloud:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-nextcloud', info.name,
|
||||
short_description=info.short_description,
|
||||
icon=info.icon_filename,
|
||||
url='/nextcloud/', clients=info.clients,
|
||||
login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-nextcloud',
|
||||
['podman', 'default-mysql-server'],
|
||||
conflicts=['libpam-tmpdir'],
|
||||
conflicts_action=Packages.ConflictsAction.REMOVE)
|
||||
self.add(packages)
|
||||
|
||||
dropin_configs = DropinConfigs('dropin-configs-nextcloud', [
|
||||
'/etc/apache2/conf-available/nextcloud-freedombox.conf',
|
||||
'/etc/fail2ban/jail.d/nextcloud-freedombox.conf',
|
||||
'/etc/fail2ban/filter.d/nextcloud-freedombox.conf',
|
||||
])
|
||||
self.add(dropin_configs)
|
||||
|
||||
firewall = Firewall('firewall-nextcloud', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
firewall_local_protection = FirewallLocalProtection(
|
||||
'firewall-local-protection-nextcloud', ['8181'])
|
||||
self.add(firewall_local_protection)
|
||||
|
||||
webserver = Webserver('webserver-nextcloud', 'nextcloud-freedombox',
|
||||
urls=['https://{host}/nextcloud/login'])
|
||||
self.add(webserver)
|
||||
|
||||
daemon = Daemon('daemon-nextcloud', 'nextcloud-fbx')
|
||||
self.add(daemon)
|
||||
|
||||
daemon = Daemon('daemon-nextcloud-timer', 'nextcloud-cron-fbx.timer')
|
||||
self.add(daemon)
|
||||
|
||||
daemon = SharedDaemon('shared-daemon-podman-auto-update',
|
||||
'podman-auto-update.timer')
|
||||
self.add(daemon)
|
||||
|
||||
daemon = SharedDaemon('shared-daemon-nextcloud-redis', 'redis-server',
|
||||
listen_ports=[(6379, 'tcp4')])
|
||||
self.add(daemon)
|
||||
|
||||
daemon = SharedDaemon('shared-daemon-nextcloud-mysql', 'mysql')
|
||||
self.add(daemon)
|
||||
|
||||
def setup(self, old_version):
|
||||
"""Install and configure the app."""
|
||||
super().setup(old_version)
|
||||
privileged.setup()
|
||||
if not old_version:
|
||||
self.enable()
|
||||
|
||||
def uninstall(self):
|
||||
"""De-configure and uninstall the app."""
|
||||
privileged.uninstall()
|
||||
super().uninstall()
|
||||
|
||||
def diagnose(self):
|
||||
"""Run diagnostics and return the results."""
|
||||
results = super().diagnose()
|
||||
results.append(diagnose_url('docker.com'))
|
||||
return results
|
||||
@ -0,0 +1,25 @@
|
||||
##
|
||||
## On all sites, provide Nextcloud on a default path: /nextcloud
|
||||
##
|
||||
## Requires the following Apache modules to be enabled:
|
||||
## mod_headers
|
||||
## mod_proxy
|
||||
## mod_proxy_http
|
||||
##
|
||||
|
||||
# Redirect .well-known URLs on the server to Nextcloud to enable auto-discovery
|
||||
# of calendars, contacts, etc. without having to provide full server URLs. If
|
||||
# another app providing similar functionality is enabled, only one of them will
|
||||
# work based on the sort order of Apache configuration files.
|
||||
Redirect 301 /.well-known/carddav /nextcloud/remote.php/dav
|
||||
Redirect 301 /.well-known/caldav /nextcloud/remote.php/dav
|
||||
Redirect 301 /.well-known/webfinger /nextcloud/index.php/.well-known/webfinger
|
||||
Redirect 301 /.well-known/nodeinfo /nextcloud/index.php/.well-known/nodeinfo
|
||||
|
||||
<Location /nextcloud>
|
||||
ProxyPass http://127.0.0.1:8181
|
||||
|
||||
## Send the scheme from user's request to enable Nextcloud to redirect URLs,
|
||||
## set cookies, set absolute URLs (if any) properly.
|
||||
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
|
||||
</Location>
|
||||
@ -0,0 +1,7 @@
|
||||
[INCLUDES]
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
_daemon = apache-access
|
||||
prefregex = %(__prefix_line)s
|
||||
failregex = \S+ <HOST>(?::\d+)? - \S+ \[[^\]]*\] "GET /nextcloud/login\?direct=1&user=\S+ HTTP/\S+" 200
|
||||
@ -0,0 +1,4 @@
|
||||
[nextcloud-freedombox]
|
||||
enabled = true
|
||||
filter = nextcloud-freedombox
|
||||
journalmatch = SYSLOG_IDENTIFIER=apache-access
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.nextcloud
|
||||
21
plinth/modules/nextcloud/forms.py
Normal file
21
plinth/modules/nextcloud/forms.py
Normal file
@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Nextcloud configuration form."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class NextcloudForm(forms.Form):
|
||||
"""Nextcloud configuration form."""
|
||||
|
||||
domain = forms.CharField(
|
||||
label=_('Domain'), required=False, help_text=_(
|
||||
'Examples: "myfreedombox.example.org" or "example.onion".'))
|
||||
|
||||
admin_password = forms.CharField(
|
||||
label=_('Administrator password'), help_text=_(
|
||||
'Optional. Set a new password for Nextcloud\'s administrator '
|
||||
'account (nextcloud-admin). The password cannot be a common one '
|
||||
'and the minimum required length is <strong>10 characters'
|
||||
'</strong>. Leave this field blank to keep the current password.'),
|
||||
required=False, widget=forms.PasswordInput, min_length=10)
|
||||
49
plinth/modules/nextcloud/manifest.py
Normal file
49
plinth/modules/nextcloud/manifest.py
Normal file
@ -0,0 +1,49 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Application manifest for Nextcloud."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.clients import store_url
|
||||
|
||||
_nextcloud_android_package_id = 'com.nextcloud.client'
|
||||
|
||||
clients = [{
|
||||
'name': _('Nextcloud'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/nextcloud/'
|
||||
}]
|
||||
}, {
|
||||
'name':
|
||||
_('Nextcloud'),
|
||||
'platforms': [{
|
||||
'type': 'download',
|
||||
'os': 'gnu-linux',
|
||||
'url': 'https://nextcloud.com/install/#install-clients'
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'macos',
|
||||
'url': 'https://nextcloud.com/install/#install-clients'
|
||||
}, {
|
||||
'type': 'download',
|
||||
'os': 'windows',
|
||||
'url': 'https://nextcloud.com/install/#install-clients'
|
||||
}, {
|
||||
'type': 'store',
|
||||
'os': 'ios',
|
||||
'store_name': 'app-store',
|
||||
'url': 'https://itunes.apple.com/us/app/nextcloud/id1125420102?mt=8'
|
||||
}, {
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'google-play',
|
||||
'url': store_url('google-play', _nextcloud_android_package_id)
|
||||
}, {
|
||||
'type': 'store',
|
||||
'os': 'android',
|
||||
'store_name': 'f-droid',
|
||||
'url': store_url('f-droid', _nextcloud_android_package_id)
|
||||
}]
|
||||
}]
|
||||
|
||||
backup = {}
|
||||
274
plinth/modules/nextcloud/privileged.py
Normal file
274
plinth/modules/nextcloud/privileged.py
Normal file
@ -0,0 +1,274 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Configure Nextcloud."""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth.actions import privileged
|
||||
|
||||
NETWORK_NAME = 'nextcloud-fbx'
|
||||
BRIDGE_IP = '172.16.16.1'
|
||||
CONTAINER_IP = '172.16.16.2'
|
||||
CONTAINER_NAME = 'nextcloud-fbx'
|
||||
VOLUME_NAME = 'nextcloud-volume-fbx'
|
||||
IMAGE_NAME = 'docker.io/library/nextcloud:stable-apache'
|
||||
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'nextcloud_fbx'
|
||||
DB_USER = 'nextcloud_fbx'
|
||||
GUI_ADMIN = 'nextcloud-admin'
|
||||
SOCKET_CONFIG_FILE = pathlib.Path('/etc/mysql/mariadb.conf.d/'
|
||||
'99-freedombox.cnf')
|
||||
SYSTEMD_LOCATION = '/etc/systemd/system/'
|
||||
NEXTCLOUD_CONTAINER_SYSTEMD_FILE = pathlib.Path(
|
||||
f'{SYSTEMD_LOCATION}{CONTAINER_NAME}.service')
|
||||
NEXTCLOUD_CRON_SERVICE_FILE = pathlib.Path(
|
||||
f'{SYSTEMD_LOCATION}nextcloud-cron-fbx.service')
|
||||
NEXTCLOUD_CRON_TIMER_FILE = pathlib.Path(
|
||||
f'{SYSTEMD_LOCATION}nextcloud-cron-fbx.timer')
|
||||
|
||||
|
||||
@privileged
|
||||
def setup():
|
||||
"""Setup Nextcloud configuration."""
|
||||
database_password = _generate_secret_key(16)
|
||||
administrator_password = _generate_secret_key(16)
|
||||
_configure_db_socket()
|
||||
_configure_firewall(action='add', interface_name=NETWORK_NAME)
|
||||
_create_database(database_password)
|
||||
action_utils.podman_run(
|
||||
network_name=NETWORK_NAME, subnet='172.16.16.0/24',
|
||||
bridge_ip=BRIDGE_IP, host_port='8181', container_port='80',
|
||||
container_ip=CONTAINER_IP, volume_name=VOLUME_NAME,
|
||||
container_name=CONTAINER_NAME, image_name=IMAGE_NAME,
|
||||
extra_run_options=[
|
||||
'--env=TRUSTED_PROXIES={BRIDGE_IP}',
|
||||
'--env=OVERWRITEWEBROOT=/nextcloud'
|
||||
])
|
||||
# OCC isn't immediately available after the container is spun up.
|
||||
# Wait until CAN_INSTALL file is available.
|
||||
timeout = 300
|
||||
while timeout > 0:
|
||||
if os.path.exists('/var/lib/containers/storage/volumes/'
|
||||
'nextcloud-volume-fbx/_data/config/CAN_INSTALL'):
|
||||
break
|
||||
timeout = timeout - 1
|
||||
time.sleep(1)
|
||||
|
||||
_nextcloud_setup_wizard(database_password, administrator_password)
|
||||
# Check if LDAP has already been configured. This is necessary because
|
||||
# if the setup proccess is rerun when updating the FredomBox app another
|
||||
# redundant LDAP config would be created.
|
||||
is_ldap_configured = _run_occ('ldap:test-config', 's01',
|
||||
capture_output=True)
|
||||
if is_ldap_configured != ('The configuration is valid and the connection '
|
||||
'could be established!'):
|
||||
_configure_ldap()
|
||||
|
||||
_configure_systemd()
|
||||
|
||||
|
||||
def _run_occ(*args, capture_output: bool = False):
|
||||
"""Run the Nextcloud occ command inside the container."""
|
||||
occ = [
|
||||
'podman', 'exec', '--user', 'www-data', CONTAINER_NAME, 'php', 'occ'
|
||||
] + list(args)
|
||||
return subprocess.run(occ, capture_output=capture_output, check=False)
|
||||
|
||||
|
||||
@privileged
|
||||
def get_domain():
|
||||
"""Return domain name set in Nextcloud."""
|
||||
try:
|
||||
domain = _run_occ('config:system:get', 'overwritehost',
|
||||
capture_output=True)
|
||||
return domain.stdout.decode().strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
|
||||
@privileged
|
||||
def set_domain(domain_name: str):
|
||||
"""Set Nextcloud domain name."""
|
||||
protocol = 'https'
|
||||
if domain_name.endswith('.onion'):
|
||||
protocol = 'http'
|
||||
|
||||
if domain_name:
|
||||
_run_occ('config:system:set', 'overwritehost', '--value', domain_name)
|
||||
|
||||
_run_occ('config:system:set', 'overwrite.cli.url', '--value',
|
||||
f'{protocol}://{domain_name}/nextcloud')
|
||||
|
||||
_run_occ('config:system:set', 'overwriteprotocol', '--value', protocol)
|
||||
|
||||
# Restart to apply changes immediately
|
||||
action_utils.service_restart('nextcloud-fbx')
|
||||
|
||||
|
||||
@privileged
|
||||
def set_admin_password(password: str):
|
||||
"""Set password for owncloud-admin"""
|
||||
subprocess.run([
|
||||
'podman', 'exec', '--user', 'www-data', f'--env=OC_PASS={password}',
|
||||
'-it', CONTAINER_NAME, 'sh', '-c',
|
||||
("/var/www/html/occ "
|
||||
f"user:resetpassword --password-from-env {GUI_ADMIN}")
|
||||
], check=True)
|
||||
|
||||
|
||||
def _configure_firewall(action, interface_name):
|
||||
subprocess.run([
|
||||
'firewall-cmd', '--permanent', '--zone=trusted',
|
||||
f'--{action}-interface={interface_name}'
|
||||
], check=True)
|
||||
action_utils.service_restart('firewalld')
|
||||
|
||||
|
||||
def _configure_db_socket():
|
||||
file_content = f'''## This file is automatically generated by FreedomBox
|
||||
## Enable database to create a socket for podman's bridge network
|
||||
[mysqld]
|
||||
bind-address = {BRIDGE_IP}
|
||||
'''
|
||||
SOCKET_CONFIG_FILE.write_text(file_content, encoding='utf-8')
|
||||
action_utils.service_restart('mariadb')
|
||||
|
||||
|
||||
def _create_database(db_password):
|
||||
"""Create an empty MySQL database for Nextcloud."""
|
||||
# SQL injection is avoided due to known input.
|
||||
_db_file_path = pathlib.Path('/var/lib/mysql/nextcloud_fbx')
|
||||
if _db_file_path.exists():
|
||||
return
|
||||
query = f'''CREATE USER '{DB_USER}'@'{CONTAINER_IP}'
|
||||
IDENTIFIED BY'{db_password}';
|
||||
CREATE DATABASE {DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
GRANT ALL PRIVILEGES ON {DB_NAME}.* TO '{DB_USER}'@'{CONTAINER_IP}';
|
||||
FLUSH PRIVILEGES;'''
|
||||
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
|
||||
check=True)
|
||||
|
||||
|
||||
def _nextcloud_setup_wizard(db_password, admin_password):
|
||||
admin_data_dir = pathlib.Path(
|
||||
'/var/lib/containers/storage/volumes/nextcloud-volume-fbx/'
|
||||
f'_data/data/{GUI_ADMIN}')
|
||||
if not admin_data_dir.exists():
|
||||
_run_occ('maintenance:install', '--database=mysql',
|
||||
f'--database-name={DB_NAME}', f'--database-host={BRIDGE_IP}',
|
||||
'--database-port=3306', f'--database-user={DB_USER}',
|
||||
f'--database-pass={db_password}', f'--admin-user={GUI_ADMIN}',
|
||||
f'--admin-pass={admin_password}')
|
||||
# For the server to work properly, it's important to configure background
|
||||
# jobs correctly. Cron is the recommended setting.
|
||||
_run_occ('background:cron')
|
||||
|
||||
|
||||
def _configure_ldap():
|
||||
_run_occ('app:enable', 'user_ldap')
|
||||
_run_occ('ldap:create-empty-config')
|
||||
|
||||
ldap_settings = {
|
||||
'ldapBase': 'dc=thisbox',
|
||||
'ldapBaseGroups': 'dc=thisbox',
|
||||
'ldapBaseUsers': 'dc=thisbox',
|
||||
'ldapConfigurationActive': '1',
|
||||
'ldapGroupDisplayName': 'cn',
|
||||
'ldapGroupFilter': '(&(|(objectclass=posixGroup)))',
|
||||
'ldapGroupFilterMode': '0',
|
||||
'ldapGroupFilterObjectclass': 'posixGroup',
|
||||
'ldapGroupMemberAssocAttr': 'memberUid',
|
||||
'ldapHost': BRIDGE_IP,
|
||||
'ldapLoginFilter': '(&(|(objectclass=posixAccount))(uid=%uid))',
|
||||
'ldapLoginFilterEmail': '0',
|
||||
'ldapLoginFilterMode': '0',
|
||||
'ldapLoginFilterUsername': '1',
|
||||
'ldapNestedGroups': '0',
|
||||
'ldapPort': '389',
|
||||
'ldapTLS': '0',
|
||||
'ldapUserDisplayName': 'cn',
|
||||
'ldapUserFilter': '(|(objectclass=posixAccount))',
|
||||
'ldapUserFilterMode': '0',
|
||||
'ldapUserFilterObjectclass': 'account',
|
||||
'ldapUuidGroupAttribute': 'auto',
|
||||
'ldapUuidUserAttribute': 'auto',
|
||||
'turnOffCertCheck': '0',
|
||||
'turnOnPasswordChange': '0',
|
||||
'useMemberOfToDetectMembership': '0'
|
||||
}
|
||||
|
||||
for key, value in ldap_settings.items():
|
||||
_run_occ('ldap:set-config', 's01', key, value)
|
||||
|
||||
|
||||
def _configure_systemd():
|
||||
systemd_content = subprocess.run(
|
||||
['podman', 'generate', 'systemd', '--new', CONTAINER_NAME],
|
||||
capture_output=True, check=True).stdout.decode()
|
||||
# Create service and timer for running periodic php jobs.
|
||||
NEXTCLOUD_CONTAINER_SYSTEMD_FILE.write_text(systemd_content,
|
||||
encoding='utf-8')
|
||||
nextcloud_cron_service_content = '''
|
||||
[Unit]
|
||||
Description=Nextcloud cron.php job
|
||||
|
||||
[Service]
|
||||
ExecCondition=/usr/bin/podman exec --user www-data nextcloud-fbx php occ status -e
|
||||
ExecStart=/usr/bin/podman exec --user www-data nextcloud-fbx php /var/www/html/cron.php
|
||||
KillMode=process
|
||||
''' # noqa: E501
|
||||
nextcloud_cron_timer_content = '''[Unit]
|
||||
Description=Run Nextcloud cron.php every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=5min
|
||||
Unit=nextcloud-cron-fbx.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
'''
|
||||
NEXTCLOUD_CRON_SERVICE_FILE.write_text(nextcloud_cron_service_content)
|
||||
NEXTCLOUD_CRON_TIMER_FILE.write_text(nextcloud_cron_timer_content)
|
||||
action_utils.service_daemon_reload()
|
||||
|
||||
|
||||
@privileged
|
||||
def uninstall():
|
||||
"""Uninstall Nextcloud"""
|
||||
_drop_database()
|
||||
_remove_db_socket()
|
||||
_configure_firewall(action='remove', interface_name=NETWORK_NAME)
|
||||
action_utils.podman_uninstall(container_name=CONTAINER_NAME,
|
||||
network_name=NETWORK_NAME,
|
||||
volume_name=VOLUME_NAME,
|
||||
image_name=IMAGE_NAME)
|
||||
files = [NEXTCLOUD_CRON_SERVICE_FILE, NEXTCLOUD_CRON_TIMER_FILE]
|
||||
for file in files:
|
||||
file.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _remove_db_socket():
|
||||
SOCKET_CONFIG_FILE.unlink(missing_ok=True)
|
||||
action_utils.service_restart('mariadb')
|
||||
|
||||
|
||||
def _drop_database():
|
||||
"""Drop the mysql database that was created during install."""
|
||||
query = f'''DROP DATABASE {DB_NAME};
|
||||
DROP User '{DB_USER}'@'{CONTAINER_IP}';'''
|
||||
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
|
||||
check=True)
|
||||
|
||||
|
||||
def _generate_secret_key(length=64, chars=None):
|
||||
"""Generate a new random secret key for use with Nextcloud."""
|
||||
chars = chars or (string.ascii_letters + string.digits)
|
||||
rand = random.SystemRandom()
|
||||
return ''.join(rand.choice(chars) for _ in range(length))
|
||||
BIN
plinth/modules/nextcloud/static/icons/nextcloud.png
Normal file
BIN
plinth/modules/nextcloud/static/icons/nextcloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
55
plinth/modules/nextcloud/static/icons/nextcloud.svg
Normal file
55
plinth/modules/nextcloud/static/icons/nextcloud.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.4 KiB |
10
plinth/modules/nextcloud/urls.py
Normal file
10
plinth/modules/nextcloud/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""URLs for the Nextcloud module."""
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
from .views import NextcloudAppView
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^apps/nextcloud/$', NextcloudAppView.as_view(), name='index')
|
||||
]
|
||||
55
plinth/modules/nextcloud/views.py
Normal file
55
plinth/modules/nextcloud/views.py
Normal file
@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Django views for Nextcloud app."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth.modules.nextcloud.forms import NextcloudForm
|
||||
from plinth.views import AppView
|
||||
|
||||
from . import privileged
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NextcloudAppView(AppView):
|
||||
"""Show Nextcloud app main view."""
|
||||
|
||||
app_id = 'nextcloud'
|
||||
form_class = NextcloudForm
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the values to fill in the form."""
|
||||
initial = super().get_initial()
|
||||
initial.update({'domain': privileged.get_domain()})
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Apply the changes submitted in the form."""
|
||||
old_config = self.get_initial()
|
||||
new_config = form.cleaned_data
|
||||
|
||||
is_changed = False
|
||||
|
||||
def _value_changed(key):
|
||||
return old_config.get(key) != new_config.get(key)
|
||||
|
||||
if _value_changed('domain'):
|
||||
privileged.set_domain(new_config['domain'])
|
||||
is_changed = True
|
||||
|
||||
if new_config['admin_password']:
|
||||
try:
|
||||
privileged.set_admin_password(new_config['admin_password'])
|
||||
is_changed = True
|
||||
except Exception:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Password update failed. Please choose a stronger '
|
||||
'password.'))
|
||||
if is_changed:
|
||||
messages.success(self.request, _('Configuration updated.'))
|
||||
|
||||
return super().form_valid(form)
|
||||
Loading…
x
Reference in New Issue
Block a user