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