diff options
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2405.section.md | 3 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/programs/tsm-client.nix | 299 | ||||
-rw-r--r-- | nixos/modules/services/backup/tsm.nix | 11 | ||||
-rw-r--r-- | nixos/modules/services/hardware/thinkfan.nix | 2 | ||||
-rw-r--r-- | nixos/modules/services/misc/ankisyncd.nix | 6 | ||||
-rw-r--r-- | nixos/modules/services/misc/guix/default.nix | 394 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/guix/basic.nix | 38 | ||||
-rw-r--r-- | nixos/tests/guix/default.nix | 8 | ||||
-rw-r--r-- | nixos/tests/guix/publish.nix | 95 | ||||
-rw-r--r-- | nixos/tests/guix/scripts/add-existing-files-to-store.scm | 52 | ||||
-rw-r--r-- | nixos/tests/guix/scripts/create-file-to-store.scm | 8 | ||||
-rw-r--r-- | nixos/tests/tsm-client-gui.nix | 6 |
14 files changed, 774 insertions, 150 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index d3ac5938820a..bfd4bcee63d3 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -14,9 +14,12 @@ In addition to numerous new and upgraded packages, this release has the followin <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> +- [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable). + - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable). - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable). +The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares. - [Clevis](https://github.com/latchset/clevis), a pluggable framework for automated decryption, used to unlock encrypted devices in initrd. Available as [boot.initrd.clevis.enable](#opt-boot.initrd.clevis.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b505a294c8b9..fee7c35ed8f4 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -684,6 +684,7 @@ ./services/misc/gollum.nix ./services/misc/gpsd.nix ./services/misc/greenclip.nix + ./services/misc/guix ./services/misc/headphones.nix ./services/misc/heisenbridge.nix ./services/misc/homepage-dashboard.nix diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix index 6cb225d102de..45d436221ee3 100644 --- a/nixos/modules/programs/tsm-client.nix +++ b/nixos/modules/programs/tsm-client.nix @@ -1,193 +1,144 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: # XXX migration code for freeform settings: `options` can be removed in 2025 +let optionsGlobal = options; in let - inherit (builtins) length map; - inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs; + inherit (lib.attrsets) attrNames attrValues mapAttrsToList removeAttrs; + inherit (lib.lists) all allUnique concatLists elem isList map; inherit (lib.modules) mkDefault mkIf; - inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption; - inherit (lib.strings) concatLines optionalString toLower; - inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule; + inherit (lib.options) mkEnableOption mkOption mkPackageOption; + inherit (lib.strings) concatLines match optionalString toLower; + inherit (lib.trivial) isInt; + inherit (lib.types) addCheck attrsOf coercedTo either enum int lines listOf nonEmptyStr nullOr oneOf path port singleLineStr strMatching submodule; - # Checks if given list of strings contains unique - # elements when compared without considering case. - # Type: checkIUnique :: [string] -> bool - # Example: checkIUnique ["foo" "Foo"] => false - checkIUnique = lst: - let - lenUniq = l: length (lib.lists.unique l); - in - lenUniq lst == lenUniq (map toLower lst); + scalarType = + # see the option's description below for the + # handling/transformation of each possible type + oneOf [ (enum [ true null ]) int path singleLineStr ]; # TSM rejects servername strings longer than 64 chars. - servernameType = strMatching ".{1,64}"; + servernameType = strMatching "[^[:space:]]{1,64}"; serverOptions = { name, config, ... }: { - options.name = mkOption { + freeformType = attrsOf (either scalarType (listOf scalarType)); + # Client system-options file directives are explained here: + # https://www.ibm.com/docs/en/storage-protect/8.1.20?topic=commands-processing-options + options.servername = mkOption { type = servernameType; + default = name; example = "mainTsmServer"; description = lib.mdDoc '' Local name of the IBM TSM server, - must be uncapitalized and no longer than 64 chars. - The value will be used for the - `server` - directive in {file}`dsm.sys`. + must not contain space or more than 64 chars. ''; }; - options.server = mkOption { + options.tcpserveraddress = mkOption { type = nonEmptyStr; example = "tsmserver.company.com"; description = lib.mdDoc '' Host/domain name or IP address of the IBM TSM server. - The value will be used for the - `tcpserveraddress` - directive in {file}`dsm.sys`. ''; }; - options.port = mkOption { + options.tcpport = mkOption { type = addCheck port (p: p<=32767); default = 1500; # official default description = lib.mdDoc '' TCP port of the IBM TSM server. - The value will be used for the - `tcpport` - directive in {file}`dsm.sys`. TSM does not support ports above 32767. ''; }; - options.node = mkOption { + options.nodename = mkOption { type = nonEmptyStr; example = "MY-TSM-NODE"; description = lib.mdDoc '' Target node name on the IBM TSM server. - The value will be used for the - `nodename` - directive in {file}`dsm.sys`. ''; }; options.genPasswd = mkEnableOption (lib.mdDoc '' automatic client password generation. - This option influences the - `passwordaccess` - directive in {file}`dsm.sys`. + This option does *not* cause a line in + {file}`dsm.sys` by itself, but generates a + corresponding `passwordaccess` directive. The password will be stored in the directory - given by the option {option}`passwdDir`. + given by the option {option}`passworddir`. *Caution*: If this option is enabled and the server forces to renew the password (e.g. on first connection), a random password will be generated and stored ''); - options.passwdDir = mkOption { - type = path; + options.passwordaccess = mkOption { + type = enum [ "generate" "prompt" ]; + visible = false; + }; + options.passworddir = mkOption { + type = nullOr path; + default = null; example = "/home/alice/tsm-password"; description = lib.mdDoc '' Directory that holds the TSM node's password information. - The value will be used for the - `passworddir` - directive in {file}`dsm.sys`. ''; }; - options.includeExclude = mkOption { - type = lines; - default = ""; + options.inclexcl = mkOption { + type = coercedTo lines + (pkgs.writeText "inclexcl.dsm.sys") + (nullOr path); + default = null; example = '' exclude.dir /nix/store include.encrypt /home/.../* ''; description = lib.mdDoc '' - `include.*` and - `exclude.*` directives to be - used when sending files to the IBM TSM server. - The lines will be written into a file that the - `inclexcl` - directive in {file}`dsm.sys` points to. - ''; - }; - options.extraConfig = mkOption { - # TSM option keys are case insensitive; - # we have to ensure there are no keys that - # differ only by upper and lower case. - type = addCheck - (attrsOf (nullOr str)) - (attrs: checkIUnique (attrNames attrs)); - default = {}; - example.compression = "yes"; - example.passwordaccess = null; - description = lib.mdDoc '' - Additional key-value pairs for the server stanza. - Values must be strings, or `null` - for the key not to be used in the stanza - (e.g. to overrule values generated by other options). - ''; - }; - options.text = mkOption { - type = lines; - example = literalExpression - ''lib.modules.mkAfter "compression no"''; - description = lib.mdDoc '' - Additional text lines for the server stanza. - This option can be used if certion configuration keys - must be used multiple times or ordered in a certain way - as the {option}`extraConfig` option can't - control the order of lines in the resulting stanza. - Note that the `server` - line at the beginning of the stanza is - not part of this option's value. + Text lines with `include.*` and `exclude.*` directives + to be used when sending files to the IBM TSM server, + or an absolute path pointing to a file with such lines. ''; }; - options.stanza = mkOption { - type = str; - internal = true; - visible = false; - description = lib.mdDoc "Server stanza text generated from the options."; - }; - config.name = mkDefault name; - # Client system-options file directives are explained here: - # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=commands-processing-options - config.extraConfig = - mapAttrs (lib.trivial.const mkDefault) ( - { - commmethod = "v6tcpip"; # uses v4 or v6, based on dns lookup result - tcpserveraddress = config.server; - tcpport = builtins.toString config.port; - nodename = config.node; - passwordaccess = if config.genPasswd then "generate" else "prompt"; - passworddir = ''"${config.passwdDir}"''; - } // optionalAttrs (config.includeExclude!="") { - inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"''; - } - ); - config.text = - let - attrset = filterAttrs (k: v: v!=null) config.extraConfig; - mkLine = k: v: k + optionalString (v!="") " ${v}"; - lines = mapAttrsToList mkLine attrset; - in - concatLines lines; - config.stanza = '' - server ${config.name} - ${config.text} - ''; + config.commmethod = mkDefault "v6tcpip"; # uses v4 or v6, based on dns lookup result + config.passwordaccess = if config.genPasswd then "generate" else "prompt"; + # XXX migration code for freeform settings, these can be removed in 2025: + options.warnings = optionsGlobal.warnings; + options.assertions = optionsGlobal.assertions; + imports = let inherit (lib.modules) mkRemovedOptionModule mkRenamedOptionModule; in [ + (mkRemovedOptionModule [ "extraConfig" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.") + (mkRemovedOptionModule [ "text" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.") + (mkRenamedOptionModule [ "name" ] [ "servername" ]) + (mkRenamedOptionModule [ "server" ] [ "tcpserveraddress" ]) + (mkRenamedOptionModule [ "port" ] [ "tcpport" ]) + (mkRenamedOptionModule [ "node" ] [ "nodename" ]) + (mkRenamedOptionModule [ "passwdDir" ] [ "passworddir" ]) + (mkRenamedOptionModule [ "includeExclude" ] [ "inclexcl" ]) + ]; }; options.programs.tsmClient = { enable = mkEnableOption (lib.mdDoc '' - IBM Spectrum Protect (Tivoli Storage Manager, TSM) + IBM Storage Protect (Tivoli Storage Manager, TSM) client command line applications with a client system-options file "dsm.sys" ''); servers = mkOption { - type = attrsOf (submodule [ serverOptions ]); + type = attrsOf (submodule serverOptions); default = {}; example.mainTsmServer = { - server = "tsmserver.company.com"; - node = "MY-TSM-NODE"; - extraConfig.compression = "yes"; + tcpserveraddress = "tsmserver.company.com"; + nodename = "MY-TSM-NODE"; + compression = "yes"; }; description = lib.mdDoc '' Server definitions ("stanzas") for the client system-options file. + The name of each entry will be used for + the internal `servername` by default. + Each attribute will be transformed into a line + with a key-value pair within the server's stanza. + Integers as values will be + canonically turned into strings. + The boolean value `true` will be turned + into a line with just the attribute's name. + The value `null` will not generate a line. + A list as values generates an entry for + each value, according to the rules above. ''; }; defaultServername = mkOption { @@ -222,45 +173,107 @@ let to add paths to the client system-options file. ''; }; - wrappedPackage = mkOption { - type = package; - readOnly = true; - description = lib.mdDoc '' - The TSM client derivation, wrapped with the path - to the client system-options file "dsm.sys". - This option is to provide the effective derivation + wrappedPackage = mkPackageOption pkgs "tsm-client" { + default = null; + extraDescription = '' + This option is to provide the effective derivation, + wrapped with the path to the + client system-options file "dsm.sys". + It should not be changed, but exists for other modules that want to call TSM executables. ''; - }; + } // { readOnly = true; }; }; cfg = config.programs.tsmClient; + servernames = map (s: s.servername) (attrValues cfg.servers); - assertions = [ - { - assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers); + assertions = + [ + { + assertion = allUnique (map toLower servernames); + message = '' + TSM server names + (option `programs.tsmClient.servers`) + contain duplicate name + (note that server names are case insensitive). + ''; + } + { + assertion = (cfg.defaultServername!=null)->(elem cfg.defaultServername servernames); + message = '' + TSM default server name + `programs.tsmClient.defaultServername="${cfg.defaultServername}"` + not found in server names in + `programs.tsmClient.servers`. + ''; + } + ] ++ (mapAttrsToList (name: serverCfg: { + assertion = all (key: null != match "[^[:space:]]+" key) (attrNames serverCfg); message = '' - TSM servernames contain duplicate name - (note that case doesn't matter!) + TSM server setting names in + `programs.tsmClient.servers.${name}.*` + contain spaces, but that's not allowed. + ''; + }) cfg.servers) ++ (mapAttrsToList (name: serverCfg: { + assertion = allUnique (map toLower (attrNames serverCfg)); + message = '' + TSM server setting names in + `programs.tsmClient.servers.${name}.*` + contain duplicate names + (note that setting names are case insensitive). + ''; + }) cfg.servers) + # XXX migration code for freeform settings, this can be removed in 2025: + ++ (enrichMigrationInfos "assertions" (addText: { assertion, message }: { inherit assertion; message = addText message; })); + + makeDsmSysLines = key: value: + # Turn a key-value pair from the server options attrset + # into zero (value==null), one (scalar value) or + # more (value is list) configuration stanza lines. + if isList value then map (makeDsmSysLines key) value else # recurse into list + if value == null then [ ] else # skip `null` value + [ (" ${key}${ + if value == true then "" else # just output key if value is `true` + if isInt value then " ${builtins.toString value}" else + if path.check value then " \"${value}\"" else # enclose path in ".." + if singleLineStr.check value then " ${value}" else + throw "assertion failed: cannot convert type" # should never happen + }") ]; + + makeDsmSysStanza = {servername, ... }@serverCfg: + let + # drop special values that should not go into server config block + attrs = removeAttrs serverCfg [ "servername" "genPasswd" + # XXX migration code for freeform settings, these can be removed in 2025: + "assertions" "warnings" + "extraConfig" "text" + "name" "server" "port" "node" "passwdDir" "includeExclude" + ]; + in + '' + servername ${servername} + ${concatLines (concatLists (mapAttrsToList makeDsmSysLines attrs))} ''; - } - { - assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers); - message = "TSM defaultServername not found in list of servers"; - } - ]; dsmSysText = '' - **** IBM Spectrum Protect (Tivoli Storage Manager) + **** IBM Storage Protect (Tivoli Storage Manager) **** client system-options file "dsm.sys". **** Do not edit! **** This file is generated by NixOS configuration. ${optionalString (cfg.defaultServername!=null) "defaultserver ${cfg.defaultServername}"} - ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)} + ${concatLines (map makeDsmSysStanza (attrValues cfg.servers))} ''; + # XXX migration code for freeform settings, this can be removed in 2025: + enrichMigrationInfos = what: how: concatLists ( + mapAttrsToList + (name: serverCfg: map (how (text: "In `programs.tsmClient.servers.${name}`: ${text}")) serverCfg."${what}") + cfg.servers + ); + in { @@ -275,6 +288,8 @@ in dsmSysApi = dsmSysCli; }; environment.systemPackages = [ cfg.wrappedPackage ]; + # XXX migration code for freeform settings, this can be removed in 2025: + warnings = enrichMigrationInfos "warnings" (addText: addText); }; meta.maintainers = [ lib.maintainers.yarny ]; diff --git a/nixos/modules/services/backup/tsm.nix b/nixos/modules/services/backup/tsm.nix index c4de0b16d47d..6798b18b3af7 100644 --- a/nixos/modules/services/backup/tsm.nix +++ b/nixos/modules/services/backup/tsm.nix @@ -3,6 +3,7 @@ let inherit (lib.attrsets) hasAttr; + inherit (lib.meta) getExe'; inherit (lib.modules) mkDefault mkIf; inherit (lib.options) mkEnableOption mkOption; inherit (lib.types) nonEmptyStr nullOr; @@ -10,7 +11,7 @@ let options.services.tsmBackup = { enable = mkEnableOption (lib.mdDoc '' automatic backups with the - IBM Spectrum Protect (Tivoli Storage Manager, TSM) client. + IBM Storage Protect (Tivoli Storage Manager, TSM) client. This also enables {option}`programs.tsmClient.enable` ''); @@ -78,10 +79,10 @@ in config = mkIf cfg.enable { inherit assertions; programs.tsmClient.enable = true; - programs.tsmClient.servers.${cfg.servername}.passwdDir = + programs.tsmClient.servers.${cfg.servername}.passworddir = mkDefault "/var/lib/tsm-backup/password"; systemd.services.tsm-backup = { - description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup"; + description = "IBM Storage Protect (Tivoli Storage Manager) Backup"; # DSM_LOG needs a trailing slash to have it treated as a directory. # `/var/log` would be littered with TSM log files otherwise. environment.DSM_LOG = "/var/log/tsm-backup/"; @@ -89,12 +90,12 @@ in environment.HOME = "/var/lib/tsm-backup"; serviceConfig = { # for exit status description see - # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=clients-client-return-codes + # https://www.ibm.com/docs/en/storage-protect/8.1.20?topic=clients-client-return-codes SuccessExitStatus = "4 8"; # The `-se` option must come after the command. # The `-optfile` option suppresses a `dsm.opt`-not-found warning. ExecStart = - "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null"; + "${getExe' cfgPrg.wrappedPackage "dsmc"} ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null"; LogsDirectory = "tsm-backup"; StateDirectory = "tsm-backup"; StateDirectoryMode = "0750"; diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix index 8fa7b456f20e..cca35f492b8e 100644 --- a/nixos/modules/services/hardware/thinkfan.nix +++ b/nixos/modules/services/hardware/thinkfan.nix @@ -217,6 +217,8 @@ in { systemd.services = { thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs); + thinkfan.serviceConfig.Restart = "on-failure"; + thinkfan.serviceConfig.RestartSec = "30s"; # must be added manually, see issue #81138 thinkfan.wantedBy = [ "multi-user.target" ]; diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix index e4de46e19a8f..f5acfbb0ee96 100644 --- a/nixos/modules/services/misc/ankisyncd.nix +++ b/nixos/modules/services/misc/ankisyncd.nix @@ -46,6 +46,12 @@ in }; config = mkIf cfg.enable { + warnings = [ + '' + `services.ankisyncd` has been replaced by `services.anki-sync-server` and will be removed after + 24.05 because anki-sync-server(-rs and python) are not maintained. + '' + ]; networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; systemd.services.ankisyncd = { diff --git a/nixos/modules/services/misc/guix/default.nix b/nixos/modules/services/misc/guix/default.nix new file mode 100644 index 000000000000..00e84dc74554 --- /dev/null +++ b/nixos/modules/services/misc/guix/default.nix @@ -0,0 +1,394 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.guix; + + package = cfg.package.override { inherit (cfg) stateDir storeDir; }; + + guixBuildUser = id: { + name = "guixbuilder${toString id}"; + group = cfg.group; + extraGroups = [ cfg.group ]; + createHome = false; + description = "Guix build user ${toString id}"; + isSystemUser = true; + }; + + guixBuildUsers = numberOfUsers: + builtins.listToAttrs (map + (user: { + name = user.name; + value = user; + }) + (builtins.genList guixBuildUser numberOfUsers)); + + # A set of Guix user profiles to be linked at activation. + guixUserProfiles = { + # The current Guix profile that is created through `guix pull`. + "current-guix" = "\${XDG_CONFIG_HOME}/guix/current"; + + # The default Guix profile similar to $HOME/.nix-profile from Nix. + "guix-profile" = "$HOME/.guix-profile"; + }; + + # All of the Guix profiles to be used. + guixProfiles = lib.attrValues guixUserProfiles; + + serviceEnv = { + GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale"; + LC_ALL = "C.UTF-8"; + }; +in +{ + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; + + options.services.guix = with lib; { + enable = mkEnableOption "Guix build daemon service"; + + group = mkOption { + type = types.str; + default = "guixbuild"; + example = "guixbuild"; + description = '' + The group of the Guix build user pool. + ''; + }; + + nrBuildUsers = mkOption { + type = types.ints.unsigned; + description = '' + Number of Guix build users to be used in the build pool. + ''; + default = 10; + example = 20; + }; + + extraArgs = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "--max-jobs=4" "--debug" ]; + description = '' + Extra flags to pass to the Guix daemon service. + ''; + }; + + package = mkPackageOption pkgs "guix" { + extraDescription = '' + It should contain {command}`guix-daemon` and {command}`guix` + executable. + ''; + }; + + storeDir = mkOption { + type = types.path; + default = "/gnu/store"; + description = '' + The store directory where the Guix service will serve to/from. Take + note Guix cannot take advantage of substitutes if you set it something + other than {file}`/gnu/store` since most of the cached builds are + assumed to be in there. + + ::: {.warning} + This will also recompile all packages because the normal cache no + longer applies. + ::: + ''; + }; + + stateDir = mkOption { + type = types.path; + default = "/var"; + description = '' + The state directory where Guix service will store its data such as its + user-specific profiles, cache, and state files. + + ::: {.warning} + Changing it to something other than the default will rebuild the + package. + ::: + ''; + example = "/gnu/var"; + }; + + publish = { + enable = mkEnableOption "substitute server for your Guix store directory"; + + generateKeyPair = mkOption { + type = types.bool; + description = '' + Whether to generate signing keys in {file}`/etc/guix` which are + required to initialize a substitute server. Otherwise, + `--public-key=$FILE` and `--private-key=$FILE` can be passed in + {option}`services.guix.publish.extraArgs`. + ''; + default = true; + example = false; + }; + + port = mkOption { + type = types.port; + default = 8181; + example = 8200; + description = '' + Port of the substitute server to listen on. + ''; + }; + + user = mkOption { + type = types.str; + default = "guix-publish"; + description = '' + Name of the user to change once the server is up. + ''; + }; + + extraArgs = mkOption { + type = with types; listOf str; + description = '' + Extra flags to pass to the substitute server. + ''; + default = []; + example = [ + "--compression=zstd:6" + "--discover=no" + ]; + }; + }; + + gc = { + enable = mkEnableOption "automatic garbage collection service for Guix"; + + extraArgs = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + List of arguments to be passed to {command}`guix gc`. + + When given no option, it will try to collect all garbage which is + often inconvenient so it is recommended to set [some + options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html). + ''; + example = [ + "--delete-generations=1m" + "--free-space=10G" + "--optimize" + ]; + }; + + dates = lib.mkOption { + type = types.str; + default = "03:15"; + example = "weekly"; + description = '' + How often the garbage collection occurs. This takes the time format + from {manpage}`systemd.time(7)`. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + environment.systemPackages = [ package ]; + + users.users = guixBuildUsers cfg.nrBuildUsers; + users.groups.${cfg.group} = { }; + + # Guix uses Avahi (through guile-avahi) both for the auto-discovering and + # advertising substitute servers in the local network. + services.avahi.enable = lib.mkDefault true; + services.avahi.publish.enable = lib.mkDefault true; + services.avahi.publish.userServices = lib.mkDefault true; + + # It's similar to Nix daemon so there's no question whether or not this + # should be sandboxed. + systemd.services.guix-daemon = { + environment = serviceEnv; + script = '' + ${lib.getExe' package "guix-daemon"} \ + --build-users-group=${cfg.group} \ + ${lib.escapeShellArgs cfg.extraArgs} + ''; + serviceConfig = { + OOMPolicy = "continue"; + RemainAfterExit = "yes"; + Restart = "always"; + TasksMax = 8192; + }; + unitConfig.RequiresMountsFor = [ + cfg.storeDir + cfg.stateDir + ]; + wantedBy = [ "multi-user.target" ]; + }; + + # This is based from Nix daemon socket unit from upstream Nix package. + # Guix build daemon has support for systemd-style socket activation. + systemd.sockets.guix-daemon = { + description = "Guix daemon socket"; + before = [ "multi-user.target" ]; + listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ]; + unitConfig = { + RequiresMountsFor = [ + cfg.storeDir + cfg.stateDir + ]; + ConditionPathIsReadWrite = "${cfg.stateDir}/guix/daemon-socket"; + }; + wantedBy = [ "socket.target" ]; + }; + + systemd.mounts = [{ + description = "Guix read-only store directory"; + before = [ "guix-daemon.service" ]; + what = cfg.storeDir; + where = cfg.storeDir; + type = "none"; + options = "bind,ro"; + + unitConfig.DefaultDependencies = false; + wantedBy = [ "guix-daemon.service" ]; + }]; + + # Make transferring files from one store to another easier with the usual + # case being of most substitutes from the official Guix CI instance. + system.activationScripts.guix-authorize-keys = '' + for official_server_keys in ${package}/share/guix/*.pub; do + ${lib.getExe' package "guix"} archive --authorize < $official_server_keys + done + ''; + + # Link the usual Guix profiles to the home directory. This is useful in + # ephemeral setups where only certain part of the filesystem is + # persistent (e.g., "Erase my darlings"-type of setup). + system.userActivationScripts.guix-activate-user-profiles.text = let + linkProfileToPath = acc: profile: location: let + guixProfile = "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}"; + in acc + '' + [ -d "${guixProfile}" ] && ln -sf "${guixProfile}" "${location}" + ''; + + activationScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles; + in '' + # Don't export this please! It is only expected to be used for this + # activation script and nothing else. + XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config} + + # Linking the usual Guix profiles into the home directory. + ${activationScript} + ''; + + # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by + # virtually every Guix-built packages. This is so that Guix-installed + # applications wouldn't use incompatible locale data and not touch its host + # system. + environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles; + + # What Guix profiles export is very similar to Nix profiles so it is + # acceptable to list it here. Also, it is more likely that the user would + # want to use packages explicitly installed from Guix so we're putting it + # first. + environment.profiles = lib.mkBefore guixProfiles; + } + + (lib.mkIf cfg.publish.enable { + systemd.services.guix-publish = { + description = "Guix remote store"; + environment = serviceEnv; + + # Mounts will be required by the daemon service anyways so there's no + # need add RequiresMountsFor= or something similar. + requires = [ "guix-daemon.service" ]; + after = [ "guix-daemon.service" ]; + partOf = [ "guix-daemon.service" ]; + + preStart = lib.mkIf cfg.publish.generateKeyPair '' + # Generate the keypair if it's missing. + [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \ + ${lib.getExe' package "guix"} archive --generate-key || { + rm /etc/guix/signing-key.*; + ${lib.getExe' package "guix"} archive --generate-key; + } + ''; + script = '' + ${lib.getExe' package "guix"} publish \ + --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \ + ${lib.escapeShellArgs cfg.publish.extraArgs} + ''; + + serviceConfig = { + Restart = "always"; + RestartSec = 10; + + ProtectClock = true; + ProtectHostname = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + SystemCallFilter = [ + "@system-service" + "@debug" + "@setuid" + ]; + + RestrictNamespaces = true; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + + # While the permissions can be set, it is assumed to be taken by Guix + # daemon service which it has already done the setup. + ConfigurationDirectory = "guix"; + + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ + "CAP_NET_BIND_SERVICE" + "CAP_SETUID" + "CAP_SETGID" + ]; + }; + wantedBy = [ "multi-user.target" ]; + }; + + users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") { + description = "Guix publish user"; + group = config.users.groups.guix-publish.name; + isSystemUser = true; + }; + users.groups.guix-publish = {}; + }) + + (lib.mkIf cfg.gc.enable { + # This service should be handled by root to collect all garbage by all + # users. + systemd.services.guix-gc = { + description = "Guix garbage collection"; + startAt = cfg.gc.dates; + script = '' + ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs} + ''; + + serviceConfig = { + Type = "oneshot"; + + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateNetworks = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelTunables = true; + SystemCallFilter = [ + "@default" + "@file-system" + "@basic-io" + "@system-service" + ]; + }; + }; + + systemd.timers.guix-gc.timerConfig.Persistent = true; + }) + ]); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 09f33e35fc8d..e0572e3bed9c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -359,6 +359,7 @@ in { grow-partition = runTest ./grow-partition.nix; grub = handleTest ./grub.nix {}; guacamole-server = handleTest ./guacamole-server.nix {}; + guix = handleTest ./guix {}; gvisor = handleTest ./gvisor.nix {}; hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; }; hadoop_3_2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_2; }; diff --git a/nixos/tests/guix/basic.nix b/nixos/tests/guix/basic.nix new file mode 100644 index 000000000000..7f90bdeeb1e0 --- /dev/null +++ b/nixos/tests/guix/basic.nix @@ -0,0 +1,38 @@ +# Take note the Guix store directory is empty. Also, we're trying to prevent +# Guix from trying to downloading substitutes because of the restricted +# access (assuming it's in a sandboxed environment). +# +# So this test is what it is: a basic test while trying to use Guix as much as +# we possibly can (including the API) without triggering its download alarm. + +import ../make-test-python.nix ({ lib, pkgs, ... }: { + name = "guix-basic"; + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; + + nodes.machine = { config, ... }: { + environment.etc."guix/scripts".source = ./scripts; + services.guix.enable = true; + }; + + testScript = '' + import pathlib + + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("guix-daemon.service") + + # Can't do much here since the environment has restricted network access. + with subtest("Guix basic package management"): + machine.succeed("guix build --dry-run --verbosity=0 hello") + machine.succeed("guix show hello") + + # This is to see if the Guix API is usable and mostly working. + with subtest("Guix API scripting"): + scripts_dir = pathlib.Path("/etc/guix/scripts") + + text_msg = "Hello there, NixOS!" + text_store_file = machine.succeed(f"guix repl -- {scripts_dir}/create-file-to-store.scm '{text_msg}'") + assert machine.succeed(f"cat {text_store_file}") == text_msg + + machine.succeed(f"guix repl -- {scripts_dir}/add-existing-files-to-store.scm {scripts_dir}") + ''; +}) diff --git a/nixos/tests/guix/default.nix b/nixos/tests/guix/default.nix new file mode 100644 index 000000000000..a017668c05a7 --- /dev/null +++ b/nixos/tests/guix/default.nix @@ -0,0 +1,8 @@ +{ system ? builtins.currentSystem +, pkgs ? import ../../.. { inherit system; } +}: + +{ + basic = import ./basic.nix { inherit system pkgs; }; + publish = import ./publish.nix { inherit system pkgs; }; +} diff --git a/nixos/tests/guix/publish.nix b/nixos/tests/guix/publish.nix new file mode 100644 index 000000000000..6dbe8f99ebd6 --- /dev/null +++ b/nixos/tests/guix/publish.nix @@ -0,0 +1,95 @@ +# Testing out the substitute server with two machines in a local network. As a +# bonus, we'll also test a feature of the substitute server being able to +# advertise its service to the local network with Avahi. + +import ../make-test-python.nix ({ pkgs, lib, ... }: let + publishPort = 8181; + inherit (builtins) toString; +in { + name = "guix-publish"; + + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; + + nodes = let + commonConfig = { config, ... }: { + # We'll be using '--advertise' flag with the + # substitute server which requires Avahi. + services.avahi = { + enable = true; + nssmdns = true; + publish = { + enable = true; + userServices = true; + }; + }; + }; + in { + server = { config, lib, pkgs, ... }: { + imports = [ commonConfig ]; + + services.guix = { + enable = true; + publish = { + enable = true; + port = publishPort; + + generateKeyPair = true; + extraArgs = [ "--advertise" ]; + }; + }; + + networking.firewall.allowedTCPPorts = [ publishPort ]; + }; + + client = { config, lib, pkgs, ... }: { + imports = [ commonConfig ]; + + services.guix = { + enable = true; + + extraArgs = [ + # Force to only get all substitutes from the local server. We don't + # have anything in the Guix store directory and we cannot get + # anything from the official substitute servers anyways. + "--substitute-urls='http://server.local:${toString publishPort}'" + + # Enable autodiscovery of the substitute servers in the local + # network. This machine shouldn't need to import the signing key from + # the substitute server since it is automatically done anyways. + "--discover=yes" + ]; + }; + }; + }; + + testScript = '' + import pathlib + + start_all() + + scripts_dir = pathlib.Path("/etc/guix/scripts") + + for machine in machines: + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("guix-daemon.service") + machine.wait_for_unit("avahi-daemon.service") + + server.wait_for_unit("guix-publish.service") + server.wait_for_open_port(${toString publishPort}) + server.succeed("curl http://localhost:${toString publishPort}/") + + # Now it's the client turn to make use of it. + substitute_server = "http://server.local:${toString publishPort}" + client.wait_for_unit("network-online.target") + response = client.succeed(f"curl {substitute_server}") + assert "Guix Substitute Server" in response + + # Authorizing the server to be used as a substitute server. + client.succeed(f"curl -O {substitute_server}/signing-key.pub") + client.succeed("guix archive --authorize < ./signing-key.pub") + + # Since we're using the substitute server with the `--advertise` flag, we + # might as well check it. + client.succeed("avahi-browse --resolve --terminate _guix_publish._tcp | grep '_guix_publish._tcp'") + ''; +}) diff --git a/nixos/tests/guix/scripts/add-existing-files-to-store.scm b/nixos/tests/guix/scripts/add-existing-files-to-store.scm new file mode 100644 index 000000000000..fa47320b6a51 --- /dev/null +++ b/nixos/tests/guix/scripts/add-existing-files-to-store.scm @@ -0,0 +1,52 @@ +;; A simple script that adds each file given from the command-line into the +;; store and checks them if it's the same. +(use-modules (guix) + (srfi srfi-1) + (ice-9 ftw) + (rnrs io ports)) + +;; This is based from tests/derivations.scm from Guix source code. +(define* (directory-contents dir #:optional (slurp get-bytevector-all)) + "Return an alist representing the contents of DIR" + (define prefix-len (string-length dir)) + (sort (file-system-fold (const #t) + (lambda (path stat result) + (alist-cons (string-drop path prefix-len) + (call-with-input-file path slurp) + result)) + (lambda (path stat result) result) + (lambda (path stat result) result) + (lambda (path stat result) result) + (lambda (path stat errno result) result) + '() + dir) + (lambda (e1 e2) + (string<? (car e1) (car e2))))) + +(define* (check-if-same store drv path) + "Check if the given path and its store item are the same" + (let* ((filetype (stat:type (stat drv)))) + (case filetype + ((regular) + (and (valid-path? store drv) + (equal? (call-with-input-file path get-bytevector-all) + (call-with-input-file drv get-bytevector-all)))) + ((directory) + (and (valid-path? store drv) + (equal? (directory-contents path) + (directory-contents drv)))) + (else #f)))) + +(define* (add-and-check-item-to-store store path) + "Add PATH to STORE and check if the contents are the same" + (let* ((store-item (add-to-store store + (basename path) + #t "sha256" path)) + (is-same (check-if-same store store-item path))) + (if (not is-same) + (exit 1)))) + +(with-store store + (map (lambda (path) + (add-and-check-item-to-store store (readlink* path))) + (cdr (command-line)))) diff --git a/nixos/tests/guix/scripts/create-file-to-store.scm b/nixos/tests/guix/scripts/create-file-to-store.scm new file mode 100644 index 000000000000..467e4c4fd53f --- /dev/null +++ b/nixos/tests/guix/scripts/create-file-to-store.scm @@ -0,0 +1,8 @@ +;; A script that creates a store item with the given text and prints the +;; resulting store item path. +(use-modules (guix)) + +(with-store store + (display (add-text-to-store store "guix-basic-test-text" + (string-join + (cdr (command-line)))))) diff --git a/nixos/tests/tsm-client-gui.nix b/nixos/tests/tsm-client-gui.nix index e11501da53d0..c9632546db6e 100644 --- a/nixos/tests/tsm-client-gui.nix +++ b/nixos/tests/tsm-client-gui.nix @@ -18,9 +18,9 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: { defaultServername = "testserver"; servers.testserver = { # 192.0.0.8 is a "dummy address" according to RFC 7600 - server = "192.0.0.8"; - node = "SOME-NODE"; - passwdDir = "/tmp"; + tcpserveraddress = "192.0.0.8"; + nodename = "SOME-NODE"; + passworddir = "/tmp"; }; }; }; |