nixos/seafile: init service

This commit is contained in:
Greizgh 2021-04-20 20:06:57 +02:00
parent 22ecb14d70
commit 7b7f3dfbe4
No known key found for this signature in database
GPG Key ID: 68B0B42612A108B2
5 changed files with 424 additions and 0 deletions

View File

@ -357,6 +357,14 @@
<link linkend="opt-services.multipath.enable">services.multipath</link>. <link linkend="opt-services.multipath.enable">services.multipath</link>.
</para> </para>
</listitem> </listitem>
<listitem>
<para>
<link xlink:href="https://www.seafile.com/en/home/">seafile</link>,
an open source file syncing &amp; sharing software. Available
as
<link xlink:href="options.html#opt-services.seafile.enable">services.seafile</link>.
</para>
</listitem>
</itemizedlist> </itemizedlist>
</section> </section>
<section xml:id="sec-release-21.11-incompatibilities"> <section xml:id="sec-release-21.11-incompatibilities">

View File

@ -110,6 +110,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [multipath](https://github.com/opensvc/multipath-tools), the device mapper multipath (DM-MP) daemon. Available as [services.multipath](#opt-services.multipath.enable). - [multipath](https://github.com/opensvc/multipath-tools), the device mapper multipath (DM-MP) daemon. Available as [services.multipath](#opt-services.multipath.enable).
- [seafile](https://www.seafile.com/en/home/), an open source file syncing & sharing software. Available as [services.seafile](options.html#opt-services.seafile.enable).
## Backward Incompatibilities {#sec-release-21.11-incompatibilities} ## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
- The `services.wakeonlan` option was removed, and replaced with `networking.interfaces.<name>.wakeOnLan`. - The `services.wakeonlan` option was removed, and replaced with `networking.interfaces.<name>.wakeOnLan`.

View File

@ -836,6 +836,7 @@
./services/networking/rpcbind.nix ./services/networking/rpcbind.nix
./services/networking/rxe.nix ./services/networking/rxe.nix
./services/networking/sabnzbd.nix ./services/networking/sabnzbd.nix
./services/networking/seafile.nix
./services/networking/searx.nix ./services/networking/searx.nix
./services/networking/skydns.nix ./services/networking/skydns.nix
./services/networking/shadowsocks.nix ./services/networking/shadowsocks.nix

View File

@ -0,0 +1,290 @@
{ config, lib, pkgs, ... }:
with lib;
let
python = pkgs.python3Packages.python;
cfg = config.services.seafile;
settingsFormat = pkgs.formats.ini { };
ccnetConf = settingsFormat.generate "ccnet.conf" cfg.ccnetSettings;
seafileConf = settingsFormat.generate "seafile.conf" cfg.seafileSettings;
seahubSettings = pkgs.writeText "seahub_settings.py" ''
FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '${seahubDir}/seahub.db',
}
}
MEDIA_ROOT = '${seahubDir}/media/'
THUMBNAIL_ROOT = '${seahubDir}/thumbnail/'
with open('${seafRoot}/.seahubSecret') as f:
SECRET_KEY = f.readline().rstrip()
${cfg.seahubExtraConf}
'';
seafRoot = "/var/lib/seafile"; # hardcode it due to dynamicuser
ccnetDir = "${seafRoot}/ccnet";
dataDir = "${seafRoot}/data";
seahubDir = "${seafRoot}/seahub";
in {
###### Interface
options.services.seafile = {
enable = mkEnableOption "Seafile server";
ccnetSettings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
General = {
SERVICE_URL = mkOption {
type = types.str;
example = "https://www.example.com";
description = ''
Seahub public URL.
'';
};
};
};
};
default = { };
description = ''
Configuration for ccnet, see
<link xlink:href="https://manual.seafile.com/config/ccnet-conf/"/>
for supported values.
'';
};
seafileSettings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
fileserver = {
port = mkOption {
type = types.port;
default = 8082;
description = ''
The tcp port used by seafile fileserver.
'';
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
example = "0.0.0.0";
description = ''
The binding address used by seafile fileserver.
'';
};
};
};
};
default = { };
description = ''
Configuration for seafile-server, see
<link xlink:href="https://manual.seafile.com/config/seafile-conf/"/>
for supported values.
'';
};
workers = mkOption {
type = types.int;
default = 4;
example = 10;
description = ''
The number of gunicorn worker processes for handling requests.
'';
};
adminEmail = mkOption {
example = "john@example.com";
type = types.str;
description = ''
Seafile Seahub Admin Account Email.
'';
};
initialAdminPassword = mkOption {
example = "someStrongPass";
type = types.str;
description = ''
Seafile Seahub Admin Account initial password.
Should be change via Seahub web front-end.
'';
};
seafilePackage = mkOption {
type = types.package;
description = "Which package to use for the seafile server.";
default = pkgs.seafile-server;
};
seahubExtraConf = mkOption {
default = "";
type = types.lines;
description = ''
Extra config to append to `seahub_settings.py` file.
Refer to <link xlink:href="https://manual.seafile.com/config/seahub_settings_py/" />
for all available options.
'';
};
};
###### Implementation
config = mkIf cfg.enable {
environment.etc."seafile/ccnet.conf".source = ccnetConf;
environment.etc."seafile/seafile.conf".source = seafileConf;
environment.etc."seafile/seahub_settings.py".source = seahubSettings;
systemd.targets.seafile = {
wantedBy = [ "multi-user.target" ];
description = "Seafile components";
};
systemd.services = let
securityOptions = {
ProtectHome = true;
PrivateUsers = true;
PrivateDevices = true;
ProtectClock = true;
ProtectHostname = true;
ProtectProc = "invisible";
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" ];
};
in {
seaf-server = {
description = "Seafile server";
partOf = [ "seafile.target" ];
after = [ "network.target" ];
wantedBy = [ "seafile.target" ];
restartTriggers = [ ccnetConf seafileConf ];
serviceConfig = securityOptions // {
User = "seafile";
Group = "seafile";
DynamicUser = true;
StateDirectory = "seafile";
RuntimeDirectory = "seafile";
LogsDirectory = "seafile";
ConfigurationDirectory = "seafile";
ExecStart = ''
${cfg.seafilePackage}/bin/seaf-server \
--foreground \
-F /etc/seafile \
-c ${ccnetDir} \
-d ${dataDir} \
-l /var/log/seafile/server.log \
-P /run/seafile/server.pid \
-p /run/seafile
'';
};
preStart = ''
if [ ! -f "${seafRoot}/server-setup" ]; then
mkdir -p ${dataDir}/library-template
mkdir -p ${ccnetDir}/{GroupMgr,misc,OrgMgr,PeerMgr}
${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/GroupMgr/groupmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/groupmgr.sql"
${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/misc/config.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/config.sql"
${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/OrgMgr/orgmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/org.sql"
${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/PeerMgr/usermgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/user.sql"
${pkgs.sqlite}/bin/sqlite3 ${dataDir}/seafile.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/seafile.sql"
echo "${cfg.seafilePackage.version}-sqlite" > "${seafRoot}"/server-setup
fi
# checking for upgrades and handling them
# WARNING: needs to be extended to actually handle major version migrations
installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
pkgMajor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f1)
pkgMinor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f2)
if [ $installedMajor != $pkgMajor ] || [ $installedMinor != $pkgMinor ]; then
echo "Unsupported upgrade" >&2
exit 1
fi
'';
};
seahub = let
penv = (pkgs.python3.withPackages (ps: with ps; [ gunicorn seahub ]));
in {
description = "Seafile Server Web Frontend";
wantedBy = [ "seafile.target" ];
partOf = [ "seafile.target" ];
after = [ "network.target" "seaf-server.service" ];
requires = [ "seaf-server.service" ];
restartTriggers = [ seahubSettings ];
environment = {
PYTHONPATH =
"${pkgs.python3Packages.seahub}/thirdpart:${pkgs.python3Packages.seahub}:${penv}/${python.sitePackages}";
DJANGO_SETTINGS_MODULE = "seahub.settings";
CCNET_CONF_DIR = ccnetDir;
SEAFILE_CONF_DIR = dataDir;
SEAFILE_CENTRAL_CONF_DIR = "/etc/seafile";
SEAFILE_RPC_PIPE_PATH = "/run/seafile";
SEAHUB_LOG_DIR = "/var/log/seafile";
};
serviceConfig = securityOptions // {
User = "seafile";
Group = "seafile";
DynamicUser = true;
RuntimeDirectory = "seahub";
StateDirectory = "seafile";
LogsDirectory = "seafile";
ConfigurationDirectory = "seafile";
ExecStart = ''
${penv}/bin/gunicorn seahub.wsgi:application \
--name seahub \
--workers ${toString cfg.workers} \
--log-level=info \
--preload \
--timeout=1200 \
--limit-request-line=8190 \
--bind unix:/run/seahub/gunicorn.sock
'';
};
preStart = ''
mkdir -p ${seahubDir}/media
# Link all media except avatars
for m in `find ${pkgs.python3Packages.seahub}/media/ -maxdepth 1 -not -name "avatars"`; do
ln -sf $m ${seahubDir}/media/
done
if [ ! -e "${seafRoot}/.seahubSecret" ]; then
${penv}/bin/python ${pkgs.python3Packages.seahub}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
chmod 400 ${seafRoot}/.seahubSecret
fi
if [ ! -f "${seafRoot}/seahub-setup" ]; then
# avatars directory should be writable
install -D -t ${seahubDir}/media/avatars/ ${pkgs.python3Packages.seahub}/media/avatars/default.png
install -D -t ${seahubDir}/media/avatars/groups ${pkgs.python3Packages.seahub}/media/avatars/groups/default.png
# init database
${pkgs.python3Packages.seahub}/manage.py migrate
# create admin account
${pkgs.expect}/bin/expect -c 'spawn ${pkgs.python3Packages.seahub}/manage.py createsuperuser --email=${cfg.adminEmail}; expect "Password: "; send "${cfg.initialAdminPassword}\r"; expect "Password (again): "; send "${cfg.initialAdminPassword}\r"; expect "Superuser created successfully."'
echo "${pkgs.python3Packages.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
fi
if [ $(cat "${seafRoot}/seahub-setup" | cut -d"-" -f1) != "${pkgs.python3Packages.seahub.version}" ]; then
# update database
${pkgs.python3Packages.seahub}/manage.py migrate
echo "${pkgs.python3Packages.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
fi
'';
};
};
};
}

123
nixos/tests/seafile.nix Normal file
View File

@ -0,0 +1,123 @@
import ./make-test-python.nix ({ pkgs, ... }:
let
client = { config, pkgs, ... }: {
virtualisation.memorySize = 256;
environment.systemPackages = [ pkgs.seafile-shared pkgs.curl ];
};
in {
name = "seafile";
meta = with pkgs.stdenv.lib.maintainers; {
maintainers = [ kampfschlaefer schmittlauch ];
};
nodes = {
server = { config, pkgs, ... }: {
virtualisation.memorySize = 512;
services.seafile = {
enable = true;
ccnetSettings.General.SERVICE_URL = "http://server";
adminEmail = "admin@example.com";
initialAdminPassword = "seafile_password";
};
services.nginx = {
enable = true;
virtualHosts."server" = {
locations."/".proxyPass = "http://unix:/run/seahub/gunicorn.sock";
locations."/seafhttp" = {
proxyPass = "http://127.0.0.1:8082";
extraConfig = ''
rewrite ^/seafhttp(.*)$ $1 break;
client_max_body_size 0;
proxy_connect_timeout 36000s;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
send_timeout 36000s;
proxy_http_version 1.1;
'';
};
};
};
networking.firewall = { allowedTCPPorts = [ 80 ]; };
};
client1 = client pkgs;
client2 = client pkgs;
};
testScript = ''
start_all()
with subtest("start seaf-server"):
server.wait_for_unit("seaf-server.service")
server.wait_for_file("/run/seafile/seafile.sock")
with subtest("start seahub"):
server.wait_for_unit("seahub.service")
server.wait_for_unit("nginx.service")
server.wait_for_file("/run/seahub/gunicorn.sock")
with subtest("client1 fetch seahub page"):
client1.succeed("curl -L http://server | grep 'Log In' >&2")
with subtest("client1 connect"):
client1.wait_for_unit("default.target")
client1.succeed("seaf-cli init -d . >&2")
client1.succeed("seaf-cli start >&2")
client1.succeed(
"seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
)
libid = client1.succeed(
'seaf-cli create -s http://server -n test01 -u admin\@example.com -p seafile_password -t "first test library"'
).strip()
client1.succeed(
"seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01"
)
client1.fail(
"seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test02"
)
client1.succeed(
f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
)
client1.sleep(3)
client1.succeed("seaf-cli status |grep synchronized >&2")
client1.succeed("ls -la >&2")
client1.succeed("ls -la test01 >&2")
client1.execute("echo bla > test01/first_file")
client1.sleep(2)
client1.succeed("seaf-cli status |grep synchronized >&2")
with subtest("client2 sync"):
client2.wait_for_unit("default.target")
client2.succeed("seaf-cli init -d . >&2")
client2.succeed("seaf-cli start >&2")
client2.succeed(
"seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
)
libid = client2.succeed(
"seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01 |cut -d' ' -f 2"
).strip()
client2.succeed(
f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
)
client2.sleep(3)
client2.succeed("seaf-cli status |grep synchronized >&2")
client2.succeed("ls -la test01 >&2")
client2.succeed('[ `cat test01/first_file` = "bla" ]')
'';
})