1 { config, lib, options, pkgs, ... }:
3 cfg = config.services.moonraker;
5 opt = options.services.moonraker;
6 format = pkgs.formats.ini {
7 # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
9 if builtins.length l == 1 then lib.generators.mkValueStringDefault {} (lib.head l)
10 else lib.concatMapStrings (s: "\n ${lib.generators.mkValueStringDefault {} s}") l;
11 mkKeyValue = lib.generators.mkKeyValueDefault {} ":";
14 unifiedConfigDir = cfg.stateDir + "/config";
17 services.moonraker = {
18 enable = lib.mkEnableOption "Moonraker, an API web server for Klipper";
20 package = lib.mkPackageOption pkgs "moonraker" {
22 example = "moonraker.override { useGpiod = true; }";
25 klipperSocket = lib.mkOption {
26 type = lib.types.path;
27 default = config.services.klipper.apiSocket;
28 defaultText = lib.literalExpression "config.services.klipper.apiSocket";
29 description = "Path to Klipper's API socket.";
32 stateDir = lib.mkOption {
33 type = lib.types.path;
34 default = "/var/lib/moonraker";
35 description = "The directory containing the Moonraker databases.";
38 configDir = lib.mkOption {
39 type = lib.types.nullOr lib.types.path;
42 Deprecated directory containing client-writable configuration files.
44 Clients will be able to edit files in this directory via the API. This directory must be writable.
50 default = "moonraker";
51 description = "User account under which Moonraker runs.";
54 group = lib.mkOption {
56 default = "moonraker";
57 description = "Group account under which Moonraker runs.";
60 address = lib.mkOption {
62 default = "127.0.0.1";
64 description = "The IP or host to listen on.";
68 type = lib.types.ints.unsigned;
70 description = "The port to listen on.";
73 settings = lib.mkOption {
78 trusted_clients = [ "10.0.0.0/24" ];
79 cors_domains = [ "https://app.fluidd.xyz" "https://my.mainsail.xyz" ];
83 Configuration for Moonraker. See the [documentation](https://moonraker.readthedocs.io/en/latest/configuration/)
88 allowSystemControl = lib.mkOption {
89 type = lib.types.bool;
92 Whether to allow Moonraker to perform system-level operations.
94 Moonraker exposes APIs to perform system-level operations, such as
95 reboot, shutdown, and management of systemd units. See the
96 [documentation](https://moonraker.readthedocs.io/en/latest/web_api/#machine-commands)
97 for details on what clients are able to do.
103 config = lib.mkIf cfg.enable {
105 ++ (lib.optional (lib.head (cfg.settings.update_manager.enable_system_updates or [false])) ''
106 Enabling system updates is not supported on NixOS and will lead to non-removable warnings in some clients.
108 ++ (lib.optional (cfg.configDir != null) ''
109 services.moonraker.configDir has been deprecated upstream and will be removed.
112 if cfg.configDir == unifiedConfigDir
113 then "Simply remove services.moonraker.configDir from your config."
114 else "Move files from `${cfg.configDir}` to `${unifiedConfigDir}` then remove services.moonraker.configDir from your config."
120 assertion = cfg.allowSystemControl -> config.security.polkit.enable;
121 message = "services.moonraker.allowSystemControl requires polkit to be enabled (security.polkit.enable).";
125 users.users = lib.optionalAttrs (cfg.user == "moonraker") {
128 uid = config.ids.uids.moonraker;
132 users.groups = lib.optionalAttrs (cfg.group == "moonraker") {
133 moonraker.gid = config.ids.gids.moonraker;
136 environment.etc."moonraker.cfg".source = let
141 klippy_uds_address = cfg.klipperSocket;
144 validate_service = false;
146 } // (lib.optionalAttrs (cfg.configDir != null) {
148 config_path = cfg.configDir;
151 fullConfig = lib.recursiveUpdate cfg.settings forcedConfig;
152 in format.generate "moonraker.cfg" fullConfig;
154 systemd.tmpfiles.rules = [
155 "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
156 ] ++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -";
158 systemd.services.moonraker = {
159 description = "Moonraker, an API web server for Klipper";
160 wantedBy = [ "multi-user.target" ];
161 after = [ "network.target" ]
162 ++ lib.optional config.services.klipper.enable "klipper.service";
164 # Moonraker really wants its own config to be writable...
167 # Deprecated separate config dir
168 if cfg.configDir != null then "${cfg.configDir}/moonraker-temp.cfg"
169 # Config in unified data path
170 else "${unifiedConfigDir}/moonraker-temp.cfg"
172 mkdir -p $(dirname "$config_path")
173 cp /etc/moonraker.cfg "$config_path"
174 chmod u+w "$config_path"
175 exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
179 path = [ pkgs.iproute2 ];
182 WorkingDirectory = cfg.stateDir;
189 # set this to false, otherwise we'll get a warning indicating that `/etc/klipper.cfg`
190 # is not located in the moonraker config directory.
191 services.moonraker.settings = lib.mkIf (!config.services.klipper.mutableConfig) {
192 file_manager.check_klipper_config_path = false;
195 security.polkit.extraConfig = lib.optionalString cfg.allowSystemControl ''
196 // nixos/moonraker: Allow Moonraker to perform system-level operations
198 // This was enabled via services.moonraker.allowSystemControl.
199 polkit.addRule(function(action, subject) {
200 if ((action.id == "org.freedesktop.systemd1.manage-units" ||
201 action.id == "org.freedesktop.login1.power-off" ||
202 action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
203 action.id == "org.freedesktop.login1.reboot" ||
204 action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
205 action.id.startsWith("org.freedesktop.packagekit.")) &&
206 subject.user == "${cfg.user}") {
207 return polkit.Result.YES;
213 meta.maintainers = with lib.maintainers; [