diff --git a/hosts/temmie/services/userweb/default.nix b/hosts/temmie/services/userweb/default.nix index a24e55f..0bfe1b5 100644 --- a/hosts/temmie/services/userweb/default.nix +++ b/hosts/temmie/services/userweb/default.nix @@ -1,412 +1,9 @@ -{ config, lib, pkgs, ... }: -let - cfg = config.services.httpd; - - # NOTE Enable this if you want to strace stuff in the sandbox... - debug = false; - - homeLetters = [ "a" "b" "c" "d" "h" "i" "j" "k" "l" "m" "z" ]; - - phpOptions = lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}"){ - display_errors = "Off"; - display_startup_errors = "Off"; - post_max_size = "40M"; - upload_max_filesize = "40M"; - }); - - # https://nixos.org/manual/nixpkgs/stable/#ssec-php-user-guide-installing-with-extensions - phpEnv = pkgs.php.buildEnv { - extensions = { all, ... }: with all; [ - bz2 - curl - decimal - gd - imagick - mysqli - mysqlnd - pgsql - posix - protobuf sqlite3 - uuid - xml - xsl - zlib - zstd - - pdo - pdo_mysql - pdo_pgsql - pdo_sqlite - ]; - - extraConfig = phpOptions; - }; - - perlEnv = (pkgs.perl.withPackages (ps: with ps; [ - pkgs.exiftool - pkgs.ikiwiki - pkgs.irssi - pkgs.nix.libs.nix-perl-bindings - - CGI - DBDPg - DBDSQLite - DBDmysql - DBI - Git - ImageMagick - JSON - TemplateToolkit - ])).overrideAttrs (prev: { - # NOTE: `pkgs.perl.propagatedBuildInputs` don't actually propagate through the - # wrapper derivation created by `withPackages`. This should compensate - # for that. - postBuild = prev.postBuild + '' - cp -r '${pkgs.perl}/nix-support' "$out"/nix-support - ''; - }); - - # https://nixos.org/manual/nixpkgs/stable/#python.buildenv-function - pythonEnv = pkgs.python3.buildEnv.override { - extraLibs = with pkgs.python3Packages; [ - legacy-cgi - - matplotlib - requests - ]; - ignoreCollisions = true; - }; - - # https://nixos.org/manual/nixpkgs/stable/#sec-building-environment - fhsEnv = pkgs.buildEnv { - name = "userweb-env"; - ignoreCollisions = true; - paths = with pkgs; [ - bash - - config.services.bro.instances.userweb-sendmail.client.package - - perlEnv - pythonEnv - phpEnv - ] - ++ (with phpEnv.packages; [ - # composer - ]) - ++ [ - # Useful packages for homepages - exiftool - gnuplot - ikiwiki-full - imagemagick - jhead - ruby - sbcl - sourceHighlight - - # Missing packages from tom - # blosxom - # pyblosxom - # mediawiki (TODO: do people host their own mediawikis in userweb?) - # nanoblogger - - # Version control - cvs - rcs - git - - # Compression/Archival - bzip2 - gnutar - gzip - lz4 - unzip - xz - zip - zstd - - # Other tools you might expect to find on a normal system - acl - coreutils-full - curl - diffutils - file - findutils - gawk - gnugrep - gnumake - gnupg - gnused - less - man - util-linux - vim - wget - which - xdg-utils - ] ++ lib.optionals debug [ - glibc.getent - strace - systemd - ]; - - extraOutputsToInstall = [ - "man" - "doc" - ]; - }; -in { imports = [ - ./mail.nix + ./httpd.nix ./log-processor.nix + ./mail.nix + ./module.nix + ./packages.nix ]; - - services.httpd = { - enable = true; - adminAddr = "drift@pvv.ntnu.no"; - - # TODO: consider upstreaming systemd support - # TODO: mod_log_journald in v2.5 - package = pkgs.apacheHttpd.overrideAttrs (prev: { - nativeBuildInputs = prev.nativeBuildInputs ++ [ pkgs.pkg-config ]; - buildInputs = prev.buildInputs ++ [ pkgs.systemdLibs ]; - configureFlags = prev.configureFlags ++ [ "--enable-systemd" ]; - }); - - enablePHP = true; - phpPackage = phpEnv; - inherit phpOptions; - - enablePerl = true; - - # TODO: mod_log_journald in v2.5 - extraModules = [ - "systemd" - "userdir" - { - name = "perl"; - path = let - mod_perl = pkgs.symlinkJoin { - name = "userweb_modperl_with_custom_perl_env"; - ignoreCollisions = true; - paths = [ - (pkgs.apacheHttpdPackages.mod_perl.override { - apacheHttpd = cfg.package.out; - }) - perlEnv - ]; - }; - in "${mod_perl}/modules/mod_perl.so"; - } - ]; - - logPerVirtualHost = false; - - extraConfig = '' - TraceEnable on - LogLevel warn rewrite:trace3 - - - SetHandler application/x-httpd-php - - - SetHandler application/x-httpd-php-source - Require all denied - - - SetHandler modperl - PerlResponseHandler ModPerl::Registry - Options +ExecCGI - - ''; - - virtualHosts."temmie.pvv.ntnu.no" = { - forceSSL = true; - enableACME = true; - - serverAliases = [ - "www2.pvv.ntnu.no" - ]; - - extraConfig = '' - CustomLog "${cfg.logDir}/access.log" combined - CustomLog "/run/httpd-log-processor-access.fifo" combined - ErrorLog "/run/httpd-log-processor-error.fifo" - ScriptLog "${cfg.logDir}/cgi.log" - - UserDir ${lib.concatMapStringsSep " " (l: "/home/pvv/${l}/*/web-docs") homeLetters} - UserDir disabled root - AddHandler cgi-script .cgi - DirectoryIndex index.html index.html.var index.php index.php3 index.cgi index.phtml index.shtml meg.html - SetEnvIf Request_URI "^/~([^/]+)" USERDIR_USER=$1 - - - Options MultiViews Indexes SymLinksIfOwnerMatch ExecCGI IncludesNoExec - AllowOverride All - Require all granted - - - - AllowOverride All - Require all denied - - ''; - }; - }; - - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; - - # socket activation comes in v2.5 - # systemd.sockets.httpd = { - # wantedBy = [ "sockets.target" ]; - # description = "HTTPD socket"; - # listenStreams = [ - # "0.0.0.0:80" - # "0.0.0.0:443" - # ]; - # }; - - # NOTE: 54 -> 33, this is the UID/GID we used for www-data on tom in the past. - # Any files accessed by or created by httpd will do so over NFS with this - # UID/GID pair as its credentials. - # This overlaps with the hardcoded `disnix` uid in nixpkgs, but we *probably* - # won't be using that for the foreseeable future. - users.users."wwwrun".uid = lib.mkForce 33; - users.groups."wwwrun".gid = lib.mkForce 33; - - systemd.targets.userweb = { - description = "PVV HTTPD UserWeb"; - }; - - systemd.slices.system-userweb = { - description = "PVV HTTPD UserWeb"; - }; - - systemd.services.httpd = { - after = [ - "pvv-homedirs.target" - "httpd-log-processor@access.socket" - "httpd-log-processor@error.socket" - ]; - requires = [ - "pvv-homedirs.target" - "httpd-log-processor@access.socket" - "httpd-log-processor@error.socket" - ]; - requiredBy = [ "userweb.target" ]; - - environment = { - PATH = lib.mkForce "/usr/bin"; - }; - - serviceConfig = { - Type = lib.mkForce "notify"; - ExecStart = lib.mkForce "${cfg.package}/bin/httpd -D FOREGROUND -f /etc/httpd/httpd.conf -k start"; - ExecReload = lib.mkForce "${cfg.package}/bin/httpd -f /etc/httpd/httpd.conf -k graceful"; - ExecStop = lib.mkForce ""; - KillMode = "mixed"; - Slice = "system-userweb.slice"; - - ConfigurationDirectory = [ "httpd" ]; - LogsDirectory = [ "httpd" ]; - LogsDirectoryMode = "0700"; - - AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals debug [ "CAP_SYS_PTRACE" ]; - CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals debug [ "CAP_SYS_PTRACE" ]; - LockPersonality = !debug; - PrivateDevices = true; - PrivateTmp = true; - # NOTE: this removes CAP_NET_BIND_SERVICE... - # PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = "tmpfs"; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectSystem = true; - RemoveIPC = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - "AF_UNIX" - "AF_NETLINK" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SocketBindDeny = "any"; - SocketBindAllow = [ - "tcp:80" - "tcp:443" - ]; - SystemCallArchitectures = "native"; - SystemCallFilter = lib.mkIf (!debug) [ "@system-service" ]; - UMask = "0077"; - - RuntimeDirectoryMode = "0750"; - RuntimeDirectory = [ "httpd/root-mnt" ]; - RootDirectory = "/run/httpd/root-mnt"; - MountAPIVFS = true; - BindReadOnlyPaths = [ - builtins.storeDir - "/etc" - "/dev/null" - "/var/lib/acme" - "/var/run/nscd" - "${fhsEnv}/bin:/bin" - "${fhsEnv}/sbin:/sbin" - "${fhsEnv}/lib:/lib" - "${fhsEnv}/share:/share" - ] ++ (lib.mapCartesianProduct ({ parent, child }: "${fhsEnv}${child}:${parent}${child}") { - parent = [ - "/local" - "/opt" - "/opt/local" - "/store" - "/store/gnu" - "/usr" - "/usr/local" - "/run/current-system/sw" - ]; - child = [ - "/bin" - "/sbin" - "/lib" - "/libexec" - "/include" - "/share" - ]; - }); - BindPaths = (lib.mapCartesianProduct ({ directoryFn, letter }: "/run/pvv-home-mounts/${letter}:${directoryFn letter}${letter}") { - directoryFn = [ - (_: "/home/pvv/") - (l: "/amd/homepvv${l}/") - ]; - letter = homeLetters; - }) ++ [ - "/run/httpd-log-processor-access.fifo" - "/run/httpd-log-processor-error.fifo" - ]; - }; - }; - - # TODO: create phpfpm pools with php environments that contain packages similar to those present on tom } diff --git a/hosts/temmie/services/userweb/httpd.nix b/hosts/temmie/services/userweb/httpd.nix new file mode 100644 index 0000000..6e0dddb --- /dev/null +++ b/hosts/temmie/services/userweb/httpd.nix @@ -0,0 +1,254 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.httpd; + mcfg = config.services.pvv-userweb; +in +{ + services.httpd = { + enable = true; + adminAddr = "drift@pvv.ntnu.no"; + + # TODO: consider upstreaming systemd support + # TODO: mod_log_journald in v2.5 + package = pkgs.apacheHttpd.overrideAttrs (prev: { + nativeBuildInputs = prev.nativeBuildInputs ++ [ pkgs.pkg-config ]; + buildInputs = prev.buildInputs ++ [ pkgs.systemdLibs ]; + configureFlags = prev.configureFlags ++ [ "--enable-systemd" ]; + }); + + enablePHP = true; + phpPackage = mcfg.php.env; + phpOptions = mcfg.php.options; + + enablePerl = true; + + # TODO: mod_log_journald in v2.5 + extraModules = [ + "systemd" + "userdir" + { + name = "perl"; + path = let + mod_perl = pkgs.symlinkJoin { + name = "userweb_modperl_with_custom_perl_env"; + ignoreCollisions = true; + paths = [ + (pkgs.apacheHttpdPackages.mod_perl.override { + apacheHttpd = cfg.package.out; + }) + mcfg.perl.env + ]; + }; + in "${mod_perl}/modules/mod_perl.so"; + } + ]; + + logPerVirtualHost = false; + + extraConfig = '' + TraceEnable on + LogLevel warn rewrite:trace3 + + + SetHandler application/x-httpd-php + + + SetHandler application/x-httpd-php-source + Require all denied + + + SetHandler modperl + PerlResponseHandler ModPerl::Registry + Options +ExecCGI + + ''; + + virtualHosts."temmie.pvv.ntnu.no" = { + forceSSL = true; + enableACME = true; + + serverAliases = [ + "www2.pvv.ntnu.no" + ]; + + extraConfig = '' + CustomLog "${cfg.logDir}/access.log" combined + CustomLog "/run/httpd-log-processor-access.fifo" combined + ErrorLog "/run/httpd-log-processor-error.fifo" + ScriptLog "${cfg.logDir}/cgi.log" + + UserDir ${lib.concatMapStringsSep " " (l: "/home/pvv/${l}/*/web-docs") mcfg.homeLetters} + UserDir disabled root + AddHandler cgi-script .cgi + DirectoryIndex index.html index.html.var index.php index.php3 index.cgi index.phtml index.shtml meg.html + SetEnvIf Request_URI "^/~([^/]+)" USERDIR_USER=$1 + + + Options MultiViews Indexes SymLinksIfOwnerMatch ExecCGI IncludesNoExec + AllowOverride All + Require all granted + + + + AllowOverride All + Require all denied + + ''; + }; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + # socket activation comes in v2.5 + # systemd.sockets.httpd = { + # wantedBy = [ "sockets.target" ]; + # description = "HTTPD socket"; + # listenStreams = [ + # "0.0.0.0:80" + # "0.0.0.0:443" + # ]; + # }; + + # NOTE: 54 -> 33, this is the UID/GID we used for www-data on tom in the past. + # Any files accessed by or created by httpd will do so over NFS with this + # UID/GID pair as its credentials. + # This overlaps with the hardcoded `disnix` uid in nixpkgs, but we *probably* + # won't be using that for the foreseeable future. + users.users."wwwrun".uid = lib.mkForce 33; + users.groups."wwwrun".gid = lib.mkForce 33; + + systemd.targets.userweb = { + description = "PVV HTTPD UserWeb"; + }; + + systemd.slices.system-userweb = { + description = "PVV HTTPD UserWeb"; + }; + + systemd.services.httpd = { + after = [ + "pvv-homedirs.target" + "httpd-log-processor@access.socket" + "httpd-log-processor@error.socket" + ]; + requires = [ + "pvv-homedirs.target" + "httpd-log-processor@access.socket" + "httpd-log-processor@error.socket" + ]; + requiredBy = [ "userweb.target" ]; + + environment = { + PATH = lib.mkForce "/usr/bin"; + }; + + serviceConfig = { + Type = lib.mkForce "notify"; + ExecStart = lib.mkForce "${cfg.package}/bin/httpd -D FOREGROUND -f /etc/httpd/httpd.conf -k start"; + ExecReload = lib.mkForce "${cfg.package}/bin/httpd -f /etc/httpd/httpd.conf -k graceful"; + ExecStop = lib.mkForce ""; + KillMode = "mixed"; + Slice = "system-userweb.slice"; + + ConfigurationDirectory = [ "httpd" ]; + LogsDirectory = [ "httpd" ]; + LogsDirectoryMode = "0700"; + + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals mcfg.debugMode [ "CAP_SYS_PTRACE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ] ++ lib.optionals mcfg.debugMode [ "CAP_SYS_PTRACE" ]; + LockPersonality = !mcfg.debugMode; + PrivateDevices = true; + PrivateTmp = true; + # NOTE: this removes CAP_NET_BIND_SERVICE... + # PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = "tmpfs"; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectSystem = true; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SocketBindDeny = "any"; + SocketBindAllow = [ + "tcp:80" + "tcp:443" + ]; + SystemCallArchitectures = "native"; + SystemCallFilter = lib.mkIf (!mcfg.debugMode) [ "@system-service" ]; + UMask = "0077"; + + RuntimeDirectoryMode = "0750"; + RuntimeDirectory = [ "httpd/root-mnt" ]; + RootDirectory = "/run/httpd/root-mnt"; + MountAPIVFS = true; + BindReadOnlyPaths = [ + builtins.storeDir + "/etc" + "/dev/null" + "/var/lib/acme" + "/var/run/nscd" + "${mcfg.fhsEnv}/bin:/bin" + "${mcfg.fhsEnv}/sbin:/sbin" + "${mcfg.fhsEnv}/lib:/lib" + "${mcfg.fhsEnv}/share:/share" + ] ++ (lib.mapCartesianProduct ({ parent, child }: "${mcfg.fhsEnv}${child}:${parent}${child}") { + parent = [ + "/local" + "/opt" + "/opt/local" + "/store" + "/store/gnu" + "/usr" + "/usr/local" + "/run/current-system/sw" + ]; + child = [ + "/bin" + "/sbin" + "/lib" + "/libexec" + "/include" + "/share" + ]; + }); + BindPaths = (lib.mapCartesianProduct ({ directoryFn, letter }: "/run/pvv-home-mounts/${letter}:${directoryFn letter}${letter}") { + directoryFn = [ + (_: "/home/pvv/") + (l: "/amd/homepvv${l}/") + ]; + letter = mcfg.homeLetters; + }) ++ [ + "/run/httpd-log-processor-access.fifo" + "/run/httpd-log-processor-error.fifo" + ]; + }; + }; + + # TODO: create phpfpm pools with php environments that contain packages similar to those present on tom +} diff --git a/hosts/temmie/services/userweb/log-processor.nix b/hosts/temmie/services/userweb/log-processor.nix index e37732d..43d2b2a 100644 --- a/hosts/temmie/services/userweb/log-processor.nix +++ b/hosts/temmie/services/userweb/log-processor.nix @@ -1,8 +1,6 @@ { config, lib, pkgs, values, ... }: let - homeLetters = [ "a" "b" "c" "d" "h" "i" "j" "k" "l" "m" "z" ]; - - apache-log-processor = pkgs.callPackage ./apache-log-processor { }; + mcfg = config.services.pvv-userweb; in { sops.secrets = { @@ -80,7 +78,7 @@ in "+${lib.getExe pkgs.mount} --bind ${outputDir}/group /etc/group" ]; - ExecStart = "${lib.getExe apache-log-processor} %i"; + ExecStart = "${lib.getExe mcfg.apacheLogProcessorPackage} %i"; AmbientCapabilities = [ "CAP_SETUID" "CAP_SETGID" ]; CapabilityBoundingSet = [ "CAP_SETUID" "CAP_SETGID" ]; @@ -158,8 +156,10 @@ in subuid: files subgid: files ''}:/etc/nsswitch.conf" + ] ++ lib.optionals mcfg.debugMode [ + "/bin" ]; - BindPaths = map (l: "/run/pvv-home-mounts/${l}:/home/pvv/${l}") homeLetters ++ [ + BindPaths = map (l: "/run/pvv-home-mounts/${l}:/home/pvv/${l}") mcfg.homeLetters ++ [ "/var/log/httpd" ]; }; diff --git a/hosts/temmie/services/userweb/module.nix b/hosts/temmie/services/userweb/module.nix new file mode 100644 index 0000000..0cb2df2 --- /dev/null +++ b/hosts/temmie/services/userweb/module.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.pvv-userweb; +in +{ + options.services.pvv-userweb = { + enable = lib.mkEnableOption ""; + + debugMode = lib.mkEnableOption ""; + + apacheLogProcessorPackage = lib.mkOption { + type = lib.types.package; + default = pkgs.callPackage ./apache-log-processor { }; + }; + + homeLetters = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "a" "b" "c" "d" "h" "i" "j" "k" "l" "m" "z" ]; + readOnly = true; + }; + + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = _: [ ]; + }; + + php.extensions = lib.mkOption { + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = _: [ ]; + }; + + php.options = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + display_errors = "Off"; + display_startup_errors = "Off"; + post_max_size = "40M"; + upload_max_filesize = "40M"; + }; + apply = attrs: lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} = ${v}") attrs); + }; + + # https://nixos.org/manual/nixpkgs/stable/#ssec-php-user-guide-installing-with-extensions + php.env = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = pkgs.php.buildEnv { + extensions = cfg.php.extensions; + extraConfig = cfg.php.options; + }; + }; + + perl.packages = lib.mkOption { + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = _: [ ]; + }; + + perl.env = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = (pkgs.perl.withPackages cfg.perl.packages).overrideAttrs (prev: { + # NOTE: `pkgs.perl.propagatedBuildInputs` don't actually propagate through the + # wrapper derivation created by `withPackages`. This should compensate + # for that. + postBuild = prev.postBuild + '' + cp -r '${pkgs.perl}/nix-support' "$out"/nix-support + ''; + }); + }; + + python3.packages = lib.mkOption { + type = lib.types.functionTo (lib.types.listOf lib.types.package); + default = _: [ ]; + }; + + python3.env = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = pkgs.python3.buildEnv.override { + extraLibs = cfg.python3.packages pkgs.python3Packages; + ignoreCollisions = true; + }; + }; + + fhsEnv = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = let + + in pkgs.buildEnv { + name = "userweb-env"; + ignoreCollisions = true; + paths = with pkgs; [ + bash + config.services.bro.instances.userweb-sendmail.client.package + cfg.perl.env + cfg.python3.env + cfg.php.env + ] ++ cfg.packages; + + extraOutputsToInstall = [ + "man" + "doc" + ]; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.pvv-userweb.packages = lib.mkIf cfg.debugMode (with pkgs; [ + glibc.getent + strace + systemd + ]); + }; +} diff --git a/hosts/temmie/services/userweb/packages.nix b/hosts/temmie/services/userweb/packages.nix new file mode 100644 index 0000000..9a3248d --- /dev/null +++ b/hosts/temmie/services/userweb/packages.nix @@ -0,0 +1,104 @@ +{ pkgs, ... }: +{ + services.pvv-userweb = { + packages = with pkgs; [ + # Useful packages for homepages + exiftool + gnuplot + ikiwiki-full + imagemagick + jhead + ruby + sbcl + sourceHighlight + + # Missing packages from tom + # blosxom + # pyblosxom + # mediawiki (TODO: do people host their own mediawikis in userweb?) + # nanoblogger + + # Version control + cvs + rcs + git + + # Compression/Archival + bzip2 + gnutar + gzip + lz4 + unzip + xz + zip + zstd + + # Other tools you might expect to find on a normal system + acl + coreutils-full + curl + diffutils + file + findutils + gawk + gnugrep + gnumake + gnupg + gnused + less + man + util-linux + vim + wget + which + xdg-utils + ]; + + php.extensions = { all, ... }: with all; [ + bz2 + curl + decimal + gd + imagick + mysqli + mysqlnd + pgsql + posix + protobuf sqlite3 + uuid + xml + xsl + zlib + zstd + + pdo + pdo_mysql + pdo_pgsql + pdo_sqlite + ]; + + perl.packages = perlPkgs: with perlPkgs; [ + pkgs.exiftool + pkgs.ikiwiki + pkgs.irssi + pkgs.nix.libs.nix-perl-bindings + + CGI + DBDPg + DBDSQLite + DBDmysql + DBI + Git + ImageMagick + JSON + TemplateToolkit + ]; + + python3.packages = pythonPkgs: with pythonPkgs; [ + legacy-cgi + + matplotlib + requests + ]; + }; +}