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