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.port;
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 server = {
157 host = cfg.address;
158 port = cfg.port;
159 klippy_uds_address = cfg.klipperSocket;
160 };
161 machine = {
162 validate_service = false;
163 };
164 }
165 // (lib.optionalAttrs (cfg.configDir != null) {
166 file_manager = {
167 config_path = cfg.configDir;
168 };
169 });
170 fullConfig = lib.recursiveUpdate cfg.settings forcedConfig;
171 in
172 format.generate "moonraker.cfg" fullConfig;
173
174 systemd.tmpfiles.rules = [
175 "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
176 ]
177 ++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
178 ++ lib.optionals cfg.analysis.enable [
179 "d '${cfg.stateDir}/tools/klipper_estimator' - ${cfg.user} ${cfg.group} - -"
180 "L+ '${cfg.stateDir}/tools/klipper_estimator/klipper_estimator_linux' - - - - ${lib.getExe pkgs.klipper-estimator}"
181 ];
182
183 systemd.services.moonraker = {
184 description = "Moonraker, an API web server for Klipper";
185 wantedBy = [ "multi-user.target" ];
186 after = [ "network.target" ] ++ lib.optional config.services.klipper.enable "klipper.service";
187
188 # Moonraker really wants its own config to be writable...
189 script = ''
190 config_path=${
191 # Deprecated separate config dir
192 if cfg.configDir != null then
193 "${cfg.configDir}/moonraker-temp.cfg"
194 # Config in unified data path
195 else
196 "${unifiedConfigDir}/moonraker-temp.cfg"
197 }
198 mkdir -p $(dirname "$config_path")
199 cp /etc/moonraker.cfg "$config_path"
200 chmod u+w "$config_path"
201 exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
202 '';
203
204 # Needs `ip` command
205 path = [ pkgs.iproute2 ];
206
207 serviceConfig = {
208 WorkingDirectory = cfg.stateDir;
209 PrivateTmp = true;
210 Group = cfg.group;
211 User = cfg.user;
212 };
213 };
214
215 services.moonraker.settings = {
216 # set this to false, otherwise we'll get a warning indicating that `/etc/klipper.cfg`
217 # is not located in the moonraker config directory.
218 file_manager.check_klipper_config_path = lib.mkIf (!config.services.klipper.mutableConfig) false;
219
220 # enable analysis with our own klipper-estimator, disable updating it
221 analysis = lib.mkIf (cfg.analysis.enable) {
222 platform = "linux";
223 enable_estimator_updates = false;
224 };
225 # suppress PolicyKit warnings if system control is disabled
226 machine.provider = lib.mkIf (!cfg.allowSystemControl) (lib.mkDefault "none");
227 };
228
229 security.polkit.extraConfig = lib.optionalString cfg.allowSystemControl ''
230 // nixos/moonraker: Allow Moonraker to perform system-level operations
231 //
232 // This was enabled via services.moonraker.allowSystemControl.
233 polkit.addRule(function(action, subject) {
234 if ((action.id == "org.freedesktop.systemd1.manage-units" ||
235 action.id == "org.freedesktop.login1.power-off" ||
236 action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
237 action.id == "org.freedesktop.login1.reboot" ||
238 action.id == "org.freedesktop.login1.reboot-multiple-sessions" ||
239 action.id.startsWith("org.freedesktop.packagekit.")) &&
240 subject.user == "${cfg.user}") {
241 return polkit.Result.YES;
242 }
243 });
244 '';
245 };
246
247 meta.maintainers = with lib.maintainers; [
248 cab404
249 vtuan10
250 zhaofengli
251 ];
252}