1{ config, lib, options, pkgs, ... }:
2with lib;
3let
4 cfg = config.services.moonraker;
5 pkg = cfg.package;
6 opt = options.services.moonraker;
7 format = pkgs.formats.ini {
8 # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
9 listToValue = l:
10 if builtins.length l == 1 then generators.mkValueStringDefault {} (head l)
11 else lib.concatMapStrings (s: "\n ${generators.mkValueStringDefault {} s}") l;
12 mkKeyValue = generators.mkKeyValueDefault {} ":";
13 };
14
15 unifiedConfigDir = cfg.stateDir + "/config";
16in {
17 options = {
18 services.moonraker = {
19 enable = mkEnableOption "Moonraker, an API web server for Klipper";
20
21 package = mkPackageOption pkgs "moonraker" {
22 nullable = true;
23 example = "moonraker.override { useGpiod = true; }";
24 };
25
26 klipperSocket = mkOption {
27 type = types.path;
28 default = config.services.klipper.apiSocket;
29 defaultText = literalExpression "config.services.klipper.apiSocket";
30 description = "Path to Klipper's API socket.";
31 };
32
33 stateDir = mkOption {
34 type = types.path;
35 default = "/var/lib/moonraker";
36 description = "The directory containing the Moonraker databases.";
37 };
38
39 configDir = mkOption {
40 type = types.nullOr types.path;
41 default = null;
42 description = ''
43 Deprecated directory containing client-writable configuration files.
44
45 Clients will be able to edit files in this directory via the API. This directory must be writable.
46 '';
47 };
48
49 user = mkOption {
50 type = types.str;
51 default = "moonraker";
52 description = "User account under which Moonraker runs.";
53 };
54
55 group = mkOption {
56 type = types.str;
57 default = "moonraker";
58 description = "Group account under which Moonraker runs.";
59 };
60
61 address = mkOption {
62 type = types.str;
63 default = "127.0.0.1";
64 example = "0.0.0.0";
65 description = "The IP or host to listen on.";
66 };
67
68 port = mkOption {
69 type = types.ints.unsigned;
70 default = 7125;
71 description = "The port to listen on.";
72 };
73
74 settings = mkOption {
75 type = format.type;
76 default = { };
77 example = {
78 authorization = {
79 trusted_clients = [ "10.0.0.0/24" ];
80 cors_domains = [ "https://app.fluidd.xyz" "https://my.mainsail.xyz" ];
81 };
82 };
83 description = ''
84 Configuration for Moonraker. See the [documentation](https://moonraker.readthedocs.io/en/latest/configuration/)
85 for supported values.
86 '';
87 };
88
89 allowSystemControl = mkOption {
90 type = types.bool;
91 default = false;
92 description = ''
93 Whether to allow Moonraker to perform system-level operations.
94
95 Moonraker exposes APIs to perform system-level operations, such as
96 reboot, shutdown, and management of systemd units. See the
97 [documentation](https://moonraker.readthedocs.io/en/latest/web_api/#machine-commands)
98 for details on what clients are able to do.
99 '';
100 };
101 };
102 };
103
104 config = mkIf cfg.enable {
105 warnings = []
106 ++ (optional (head (cfg.settings.update_manager.enable_system_updates or [false])) ''
107 Enabling system updates is not supported on NixOS and will lead to non-removable warnings in some clients.
108 '')
109 ++ (optional (cfg.configDir != null) ''
110 services.moonraker.configDir has been deprecated upstream and will be removed.
111
112 Action: ${
113 if cfg.configDir == unifiedConfigDir
114 then "Simply remove services.moonraker.configDir from your config."
115 else "Move files from `${cfg.configDir}` to `${unifiedConfigDir}` then remove services.moonraker.configDir from your config."
116 }
117 '');
118
119 assertions = [
120 {
121 assertion = cfg.allowSystemControl -> config.security.polkit.enable;
122 message = "services.moonraker.allowSystemControl requires polkit to be enabled (security.polkit.enable).";
123 }
124 ];
125
126 users.users = optionalAttrs (cfg.user == "moonraker") {
127 moonraker = {
128 group = cfg.group;
129 uid = config.ids.uids.moonraker;
130 };
131 };
132
133 users.groups = optionalAttrs (cfg.group == "moonraker") {
134 moonraker.gid = config.ids.gids.moonraker;
135 };
136
137 environment.etc."moonraker.cfg".source = let
138 forcedConfig = {
139 server = {
140 host = cfg.address;
141 port = cfg.port;
142 klippy_uds_address = cfg.klipperSocket;
143 };
144 machine = {
145 validate_service = false;
146 };
147 } // (lib.optionalAttrs (cfg.configDir != null) {
148 file_manager = {
149 config_path = cfg.configDir;
150 };
151 });
152 fullConfig = recursiveUpdate cfg.settings forcedConfig;
153 in format.generate "moonraker.cfg" fullConfig;
154
155 systemd.tmpfiles.rules = [
156 "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
157 ] ++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -";
158
159 systemd.services.moonraker = {
160 description = "Moonraker, an API web server for Klipper";
161 wantedBy = [ "multi-user.target" ];
162 after = [ "network.target" ]
163 ++ optional config.services.klipper.enable "klipper.service";
164
165 # Moonraker really wants its own config to be writable...
166 script = ''
167 config_path=${
168 # Deprecated separate config dir
169 if cfg.configDir != null then "${cfg.configDir}/moonraker-temp.cfg"
170 # Config in unified data path
171 else "${unifiedConfigDir}/moonraker-temp.cfg"
172 }
173 mkdir -p $(dirname "$config_path")
174 cp /etc/moonraker.cfg "$config_path"
175 chmod u+w "$config_path"
176 exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
177 '';
178
179 # Needs `ip` command
180 path = [ pkgs.iproute2 ];
181
182 serviceConfig = {
183 WorkingDirectory = cfg.stateDir;
184 PrivateTmp = true;
185 Group = cfg.group;
186 User = cfg.user;
187 };
188 };
189
190 # set this to false, otherwise we'll get a warning indicating that `/etc/klipper.cfg`
191 # is not located in the moonraker config directory.
192 services.moonraker.settings = lib.mkIf (!config.services.klipper.mutableConfig) {
193 file_manager.check_klipper_config_path = false;
194 };
195
196 security.polkit.extraConfig = lib.optionalString cfg.allowSystemControl ''
197 // nixos/moonraker: Allow Moonraker to perform system-level operations
198 //
199 // This was enabled via services.moonraker.allowSystemControl.
200 polkit.addRule(function(action, subject) {
201 if ((action.id == "org.freedesktop.systemd1.manage-units" ||
202 action.id == "org.freedesktop.login1.power-off" ||
203 action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
204 action.id == "org.freedesktop.login1.reboot" ||
205 action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
206 action.id.startsWith("org.freedesktop.packagekit.")) &&
207 subject.user == "${cfg.user}") {
208 return polkit.Result.YES;
209 }
210 });
211 '';
212 };
213
214 meta.maintainers = with maintainers; [
215 cab404
216 vtuan10
217 zhaofengli
218 ];
219}