1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.mediagoblin;
10
11 mkSubSectionKeyValue =
12 depth: k: v:
13 if lib.isAttrs v then
14 let
15 inherit (lib.strings) replicate;
16 in
17 "${replicate depth "["}${k}${replicate depth "]"}\n"
18 + lib.generators.toINIWithGlobalSection {
19 mkKeyValue = mkSubSectionKeyValue (depth + 1);
20 } { globalSection = v; }
21 else
22 lib.generators.mkKeyValueDefault {
23 mkValueString = v: if lib.isString v then ''"${v}"'' else lib.generators.mkValueStringDefault { } v;
24 } " = " k v;
25
26 iniFormat = pkgs.formats.ini { };
27
28 # we need to build our own GI_TYPELIB_PATH and GST_PLUGIN_PATH because celery, paster and gmg need this information and it cannot easily be re-wrapped
29 gst =
30 let
31 needsGst =
32 (cfg.settings.mediagoblin.plugins ? "mediagoblin.media_types.audio")
33 || (cfg.settings.mediagoblin.plugins ? "mediagoblin.media_types.video");
34 in
35 with pkgs.gst_all_1;
36 [
37 pkgs.glib
38 gst-plugins-base
39 gstreamer
40 ]
41 # audio and video share most dependencies, so we can just take audio
42 ++ lib.optionals needsGst cfg.package.optional-dependencies.audio;
43 GI_TYPELIB_PATH = lib.makeSearchPathOutput "out" "lib/girepository-1.0" gst;
44 GST_PLUGIN_PATH = lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" gst;
45
46 path =
47 lib.optionals (cfg.settings.mediagoblin.plugins ? "mediagoblin.media_types.stl") [ pkgs.blender ]
48 ++ lib.optionals (cfg.settings.mediagoblin.plugins ? "mediagoblin.media_types.pdf") (
49 with pkgs;
50 [
51 poppler-utils
52 unoconv
53 ]
54 );
55
56 finalPackage = cfg.package.python.buildEnv.override {
57 extraLibs =
58 with cfg.package.python.pkgs;
59 [
60 (toPythonModule cfg.package)
61 ]
62 ++ cfg.pluginPackages
63 # not documented in extras...
64 ++ lib.optional (lib.hasPrefix "postgresql://" cfg.settings.mediagoblin.sql_engine) psycopg2
65 ++ (
66 let
67 inherit (cfg.settings.mediagoblin) plugins;
68 in
69 with cfg.package.optional-dependencies;
70 lib.optionals (plugins ? "mediagoblin.media_types.audio") audio
71 ++ lib.optionals (plugins ? "mediagoblin.media_types.video") video
72 ++ lib.optionals (plugins ? "mediagoblin.media_types.raw_image") raw_image
73 ++ lib.optionals (plugins ? "mediagoblin.media_types.ascii") ascii
74 ++ lib.optionals (plugins ? "mediagoblin.plugins.ldap") ldap
75 );
76 };
77in
78{
79 options = {
80 services.mediagoblin = {
81 enable = lib.mkOption {
82 type = lib.types.bool;
83 default = false;
84 description = ''
85 Whether to enable MediaGoblin.
86
87 After the initial deployment, make sure to add an admin account:
88 ```
89 mediagoblin-gmg adduser --username admin --email admin@example.com
90 mediagoblin-gmg makeadmin admin
91 ```
92 '';
93 };
94
95 domain = lib.mkOption {
96 type = lib.types.str;
97 example = "mediagoblin.example.com";
98 description = "Domain under which mediagoblin will be served.";
99 };
100
101 createDatabaseLocally = lib.mkOption {
102 type = lib.types.bool;
103 default = true;
104 example = false;
105 description = "Whether to configure a local postgres database and connect to it.";
106 };
107
108 package = lib.mkPackageOption pkgs "mediagoblin" { };
109
110 pluginPackages = lib.mkOption {
111 type = with lib.types; listOf package;
112 default = [ ];
113 description = "Plugins to add to the environment of MediaGoblin. They still need to be enabled in the config.";
114 };
115
116 settings = lib.mkOption {
117 description = "Settings which are written into `mediagoblin.ini`.";
118 default = { };
119 type = lib.types.submodule {
120 freeformType = lib.types.anything;
121
122 options = {
123 mediagoblin = {
124 allow_registration = lib.mkOption {
125 type = lib.types.bool;
126 default = false;
127 description = ''
128 Whether to enable user self registration. This is generally not recommend due to spammers.
129 See [upstream FAQ](https://docs.mediagoblin.org/en/stable/siteadmin/production-deployments.html#should-i-keep-open-registration-enabled).
130 '';
131 };
132
133 email_debug_mode = lib.mkOption {
134 type = lib.types.bool;
135 default = true;
136 example = false;
137 description = ''
138 Disable email debug mode to start sending outgoing mails.
139 This requires configuring SMTP settings,
140 see the [upstream docs](https://docs.mediagoblin.org/en/stable/siteadmin/configuration.html#enabling-email-notifications)
141 for details.
142 '';
143 };
144
145 email_sender_address = lib.mkOption {
146 type = lib.types.str;
147 example = "noreply@example.org";
148 description = "Email address which notices are sent from.";
149 };
150
151 sql_engine = lib.mkOption {
152 type = lib.types.str;
153 default = "sqlite:///var/lib/mediagoblin/mediagoblin.db";
154 example = "postgresql:///mediagoblin";
155 description = "Database to use.";
156 };
157
158 plugins = lib.mkOption {
159 defaultText = ''
160 {
161 "mediagoblin.plugins.geolocation" = { };
162 "mediagoblin.plugins.processing_info" = { };
163 "mediagoblin.plugins.basic_auth" = { };
164 "mediagoblin.media_types.image" = { };
165 }
166 '';
167 description = ''
168 Plugins to enable. See [upstream docs](https://docs.mediagoblin.org/en/stable/siteadmin/plugins.html) for details.
169 Extra dependencies are automatically enabled.
170 '';
171 };
172 };
173 };
174 };
175 };
176
177 paste = {
178 port = lib.mkOption {
179 type = lib.types.port;
180 default = 6543;
181 description = "Port under which paste will listen.";
182 };
183
184 settings = lib.mkOption {
185 description = "Settings which are written into `paste.ini`.";
186 default = { };
187 type = lib.types.submodule {
188 freeformType = iniFormat.type;
189 };
190 };
191 };
192 };
193 };
194
195 config = lib.mkIf cfg.enable {
196 environment.systemPackages = [
197 (pkgs.writeShellScriptBin "mediagoblin-gmg" ''
198 sudo=exec
199 if [[ "$USER" != mediagoblin ]]; then
200 sudo='exec /run/wrappers/bin/sudo -u mediagoblin'
201 fi
202 $sudo sh -c "cd /var/lib/mediagoblin; env GI_TYPELIB_PATH=${GI_TYPELIB_PATH} GST_PLUGIN_PATH=${GST_PLUGIN_PATH} PATH=$PATH:${lib.makeBinPath path} ${lib.getExe' finalPackage "gmg"} $*"
203 '')
204 ];
205
206 services = {
207 mediagoblin.settings.mediagoblin = {
208 plugins = {
209 "mediagoblin.media_types.image" = { };
210 "mediagoblin.plugins.basic_auth" = { };
211 "mediagoblin.plugins.geolocation" = { };
212 "mediagoblin.plugins.processing_info" = { };
213 };
214 sql_engine = lib.mkIf cfg.createDatabaseLocally "postgresql:///mediagoblin";
215 };
216
217 nginx = {
218 enable = true;
219 recommendedGzipSettings = true;
220 recommendedProxySettings = true;
221 virtualHosts = {
222 # see https://git.sr.ht/~mediagoblin/mediagoblin/tree/bf61d38df21748aadb480c53fdd928647285e35f/item/nginx.conf.template
223 "${cfg.domain}" = {
224 forceSSL = true;
225 extraConfig = ''
226 # https://git.sr.ht/~mediagoblin/mediagoblin/tree/bf61d38df21748aadb480c53fdd928647285e35f/item/Dockerfile.nginx.in#L5
227 client_max_body_size 100M;
228
229 more_set_headers X-Content-Type-Options nosniff;
230 '';
231 locations = {
232 "/".proxyPass = "http://127.0.0.1:${toString cfg.paste.port}";
233 "/mgoblin_static/".alias =
234 "${finalPackage}/${finalPackage.python.sitePackages}/mediagoblin/static/";
235 "/mgoblin_media/".alias = "/var/lib/mediagoblin/user_dev/media/public/";
236 "/theme_static/".alias = "/var/lib/mediagoblin/user_dev/theme_static/";
237 "/plugin_static/".alias = "/var/lib/mediagoblin/user_dev/plugin_static/";
238 };
239 };
240 };
241 };
242
243 postgresql = lib.mkIf cfg.createDatabaseLocally {
244 enable = true;
245 ensureDatabases = [ "mediagoblin" ];
246 ensureUsers = [
247 {
248 name = "mediagoblin";
249 ensureDBOwnership = true;
250 }
251 ];
252 };
253
254 rabbitmq.enable = true;
255 };
256
257 systemd.services =
258 let
259 serviceDefaults = {
260 wantedBy = [ "multi-user.target" ];
261 inherit path;
262 serviceConfig = {
263 AmbientCapabilities = "";
264 CapabilityBoundingSet = [ "" ];
265 DevicePolicy = "closed";
266 Group = "mediagoblin";
267 LockPersonality = true;
268 MemoryDenyWriteExecute = true;
269 NoNewPrivileges = true;
270 PrivateDevices = true;
271 PrivateTmp = true;
272 ProcSubset = "pid";
273 ProtectControlGroups = true;
274 ProtectHome = true;
275 ProtectHostname = true;
276 ProtectKernelLogs = true;
277 ProtectKernelModules = true;
278 ProtectKernelTunables = true;
279 ProtectProc = "invisible";
280 ProtectSystem = "strict";
281 RestrictAddressFamilies = [
282 "AF_INET"
283 "AF_INET6"
284 "AF_UNIX"
285 ];
286 RemoveIPC = true;
287 StateDirectory = "mediagoblin";
288 StateDirectoryMode = "0750";
289 User = "mediagoblin";
290 WorkingDirectory = "/var/lib/mediagoblin/";
291 RestrictNamespaces = true;
292 RestrictRealtime = true;
293 RestrictSUIDSGID = true;
294 SystemCallArchitectures = "native";
295 SystemCallFilter = [
296 "@system-service"
297 "~@privileged"
298 "@chown"
299 ];
300 UMask = "0027";
301 };
302 };
303
304 generatedPasteConfig = iniFormat.generate "paste.ini" cfg.paste.settings;
305 pasteConfig = pkgs.runCommand "paste-combined.ini" { nativeBuildInputs = [ pkgs.crudini ]; } ''
306 cp ${cfg.package.src}/paste.ini $out
307 chmod +w $out
308 crudini --merge $out < ${generatedPasteConfig}
309 '';
310 in
311 {
312 mediagoblin-celeryd = lib.recursiveUpdate serviceDefaults {
313 # we cannot change DEFAULT.data_dir inside mediagoblin.ini because of an annoying bug
314 # https://todo.sr.ht/~mediagoblin/mediagoblin/57
315 preStart = ''
316 cp --remove-destination ${
317 pkgs.writeText "mediagoblin.ini" (
318 lib.generators.toINI { } (lib.filterAttrsRecursive (n: v: n != "plugins") cfg.settings)
319 + "\n"
320 + lib.generators.toINI { mkKeyValue = mkSubSectionKeyValue 2; } {
321 inherit (cfg.settings.mediagoblin) plugins;
322 }
323 )
324 } /var/lib/mediagoblin/mediagoblin.ini
325 '';
326 serviceConfig = {
327 Environment = [
328 "CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery"
329 "GI_TYPELIB_PATH=${GI_TYPELIB_PATH}"
330 "GST_PLUGIN_PATH=${GST_PLUGIN_PATH}"
331 "MEDIAGOBLIN_CONFIG=/var/lib/mediagoblin/mediagoblin.ini"
332 "PASTE_CONFIG=${pasteConfig}"
333 ];
334 ExecStart = "${lib.getExe' finalPackage "celery"} worker --loglevel=INFO";
335 };
336 unitConfig.Description = "MediaGoblin Celery";
337 };
338
339 mediagoblin-paster = lib.recursiveUpdate serviceDefaults {
340 after = [
341 "mediagoblin-celeryd.service"
342 "postgresql.target"
343 ];
344 requires = [
345 "mediagoblin-celeryd.service"
346 "postgresql.target"
347 ];
348 preStart = ''
349 cp --remove-destination ${pasteConfig} /var/lib/mediagoblin/paste.ini
350 ${lib.getExe' finalPackage "gmg"} dbupdate
351 '';
352 serviceConfig = {
353 Environment = [
354 "CELERY_ALWAYS_EAGER=false"
355 "GI_TYPELIB_PATH=${GI_TYPELIB_PATH}"
356 "GST_PLUGIN_PATH=${GST_PLUGIN_PATH}"
357 ];
358 ExecStart = "${lib.getExe' finalPackage "paster"} serve /var/lib/mediagoblin/paste.ini";
359 };
360 unitConfig.Description = "Mediagoblin";
361 };
362 };
363
364 systemd.tmpfiles.settings."mediagoblin"."/var/lib/mediagoblin/user_dev".d = {
365 group = "mediagoblin";
366 mode = "2750";
367 user = "mediagoblin";
368 };
369
370 users = {
371 groups.mediagoblin = { };
372 users = {
373 mediagoblin = {
374 group = "mediagoblin";
375 home = "/var/lib/mediagoblin";
376 isSystemUser = true;
377 };
378 nginx.extraGroups = [ "mediagoblin" ];
379 };
380 };
381 };
382}