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