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