Skip to content

Commit b1684f8

Browse files
committed
feat: Create NixOS module
1 parent 9df4e7c commit b1684f8

File tree

2 files changed

+329
-3
lines changed

2 files changed

+329
-3
lines changed

flake.nix

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
};
99

1010
outputs = inputs@{ self, nixpkgs, flake-utils, devenv }:
11-
flake-utils.lib.eachDefaultSystem (system:
11+
(flake-utils.lib.eachDefaultSystem (system:
1212
let
1313
inherit (pkgs.lib) optional optionals;
1414
pkgs = nixpkgs.legacyPackages.${system};
@@ -17,7 +17,7 @@
1717
beamPackages = pkgs.beam.packagesWith pkgs.beam.interpreters.erlang;
1818

1919
src = ./.;
20-
version = "0.0.0";
20+
version = builtins.readFile ./VERSION;
2121
pname = "teslamate";
2222

2323
mixFodDeps = beamPackages.fetchMixDeps {
@@ -79,6 +79,9 @@
7979
mix phx.digest --no-deps-check
8080
'';
8181

82+
meta = {
83+
mainProgram = "teslamate";
84+
};
8285
};
8386

8487
postgres_port = 7000;
@@ -153,11 +156,39 @@
153156
}];
154157

155158
};
159+
160+
moduleTest = (nixpkgs.lib.nixos.runTest {
161+
hostPkgs = pkgs;
162+
defaults.documentation.enable = false;
163+
imports = [{
164+
name = "teslamate";
165+
nodes.server = {
166+
imports = [ self.nixosModules.default ];
167+
services.teslamate = {
168+
enable = true;
169+
secrestFile = builtins.toFile "teslamate.env" ''
170+
ENCRYPTION_KEY=123456789
171+
DATABASE_PASS=123456789
172+
RELEASE_COOKIE=123456789
173+
'';
174+
postgres.enable = true;
175+
grafana.enable = true;
176+
};
177+
};
178+
179+
testScript = ''
180+
server.wait_for_open_port(4000)
181+
'';
182+
}];
183+
}).config.result;
156184
in {
157185
packages = {
158186
devenv-up = devShell.config.procfileScript;
159187
default = pkg;
160188
};
161189
devShells.default = devShell;
162-
});
190+
checks.default = moduleTest;
191+
})) // {
192+
nixosModules.default = import ./module.nix { inherit self; };
193+
};
163194
}

module.nix

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
{ self }:
2+
{ config, lib, pkgs, ...}:
3+
let
4+
teslamate = self.packages.${pkgs.system}.default;
5+
cfg = config.services.teslamate;
6+
7+
inherit (lib) mkEnableOption mkOption types mkIf mkMerge getExe literalExpression;
8+
in {
9+
options.services.teslamate = {
10+
enable = mkEnableOption "Teslamate";
11+
12+
secrestFile = mkOption {
13+
type = types.str;
14+
example = "/run/secrets/teslamate.env";
15+
description = lib.mdDoc ''
16+
Path to an env file containing the secrets used by TeslaMate.
17+
18+
Must contain at least:
19+
- `ENCRYPTION_KEY` - encryption key used to encrypt database
20+
- `DATABASE_PASS` - password used to authenticate to database
21+
- `RELEASE_COOKIE` - unique value used by elixir for clustering
22+
'';
23+
};
24+
25+
autoStart = mkOption {
26+
type = types.bool;
27+
default = true;
28+
description = "Whether to start teslamate on boot.";
29+
};
30+
31+
listenAddress = mkOption {
32+
type = with types; nullOr str;
33+
default = null;
34+
example = "127.0.0.1";
35+
description = "IP address where the web interface is exposed or `null` for all addresses";
36+
};
37+
38+
port = mkOption {
39+
type = types.port;
40+
default = 4000;
41+
description = "Port the TeslaMate service will listen on";
42+
};
43+
44+
virtualHost = mkOption {
45+
type = types.str;
46+
default = if config.networking.domain == null then "localhost" else config.networking.fqdn;
47+
defaultText = literalExpression ''
48+
if config.networking.domain == null then "localhost" else config.networking.fqdn
49+
'';
50+
description = "Host part used for generating URLs throughout the app. Will be combined with urlPath";
51+
};
52+
53+
urlPath = mkOption {
54+
type = types.str;
55+
default = "/";
56+
description = "Path prefix used for generating URLs throughout the app. Will be combined with virtualHost";
57+
};
58+
59+
postgres = {
60+
enable = mkOption {
61+
type = types.bool;
62+
default = false;
63+
description = lib.mdDoc ''
64+
Whether to create a postgres server with the recommended configuration.
65+
66+
Other settings will still be used even if `enable` is false to configure
67+
database connection.
68+
'';
69+
};
70+
71+
user = mkOption {
72+
type = types.str;
73+
default = "teslamate";
74+
description = "PostgresQL database user";
75+
};
76+
77+
database = mkOption {
78+
type = types.str;
79+
default = "teslamate";
80+
description = "PostgresQL database to connect to";
81+
};
82+
83+
host = mkOption {
84+
type = types.str;
85+
default = "127.0.0.1";
86+
description = "Hostname of the database server";
87+
};
88+
89+
port = mkOption {
90+
type = types.port;
91+
default = 5432;
92+
description = "Postgresql database port. Must be correct even if `services.teslamate.postgres.enable` is false";
93+
};
94+
};
95+
96+
grafana = {
97+
enable = mkOption {
98+
type = types.bool;
99+
default = false;
100+
description = "Whether to create and provision grafana with the TeslaMate dashboards";
101+
};
102+
103+
listenAddress = mkOption {
104+
type = types.str;
105+
default = "0.0.0.0";
106+
description = "IP address for grafana to listen to.";
107+
};
108+
109+
port = mkOption {
110+
type = types.port;
111+
default = 3000;
112+
description = "Port for grafana web service";
113+
};
114+
115+
urlPath = mkOption {
116+
type = types.str;
117+
default = "/";
118+
description = "Path that grafana is mounted on. Useful if using a reverse proxy to vend teslamate and grafana on the same port";
119+
};
120+
};
121+
122+
mqtt = {
123+
enable = mkEnableOption "TeslaMate MQTT integration";
124+
125+
host = mkOption {
126+
type = types.str;
127+
default = "127.0.0.1";
128+
description = "MQTT host";
129+
};
130+
131+
port = mkOption {
132+
type = with types; nullOr port;
133+
default = null;
134+
example = 1883;
135+
description = "MQTT port.";
136+
};
137+
};
138+
};
139+
140+
config = mkIf cfg.enable
141+
(mkMerge [
142+
{
143+
users.users.teslamate = {
144+
isSystemUser = true;
145+
group = "teslamate";
146+
home = "/var/lib/teslamate";
147+
createHome = true;
148+
};
149+
users.groups.teslamate = {};
150+
151+
systemd.services.teslamate = {
152+
description = "TeslaMate";
153+
after = [ "network.target" "postgresql.service" ];
154+
wantedBy = mkIf cfg.autoStart [ "multi-user.target" ];
155+
serviceConfig = {
156+
User = "teslamate";
157+
Restart = "on-failure";
158+
RestartSec = 5;
159+
160+
WorkingDirectory = "/var/lib/teslamate";
161+
162+
ExecStartPre = ''${getExe teslamate} eval "TeslaMate.Release.migrate"'';
163+
ExecStart = "${getExe teslamate} start";
164+
ExecStop = "${getExe teslamate} stop";
165+
166+
EnvironmentFile = cfg.secrestFile;
167+
};
168+
environment = mkMerge [
169+
{
170+
PORT = toString cfg.port;
171+
DATABASE_USER = cfg.postgres.user;
172+
DATABASE_NAME = cfg.postgres.database;
173+
DATABASE_HOST = cfg.postgres.host;
174+
DATABASE_PORT = toString cfg.postgres.port;
175+
VIRTUAL_HOST = cfg.virtualHost;
176+
URL_PATH = cfg.urlPath;
177+
HTTP_BINDING_ADDRESS = mkIf (cfg.listenAddress != null) cfg.listenAddress;
178+
DISABLE_MQTT = mkIf (!cfg.mqtt.enable) "true";
179+
}
180+
(mkIf cfg.mqtt.enable {
181+
MQTT_HOST = cfg.mqtt.host;
182+
MQTT_PORT = mkIf (cfg.mqtt.port != null) (toString cfg.mqtt.port);
183+
})
184+
];
185+
};
186+
}
187+
(mkIf cfg.postgres.enable {
188+
services.postgresql = {
189+
enable = true;
190+
package = pkgs.postgresql_16;
191+
inherit (cfg.postgres) port;
192+
193+
initialScript = pkgs.writeText "teslamate-psql-init" ''
194+
\set password `echo $DATABASE_PASS`
195+
CREATE DATABASE ${cfg.postgres.database};
196+
CREATE USER ${cfg.postgres.user} with encrypted password :'password';
197+
GRANT ALL PRIVILEGES ON DATABASE ${cfg.postgres.database} TO ${cfg.postgres.user};
198+
ALTER USER ${cfg.postgres.user} WITH SUPERUSER;
199+
'';
200+
};
201+
202+
# Include secrets in postgres as well
203+
systemd.services.postgresql = {
204+
serviceConfig = {
205+
EnvironmentFile = cfg.secrestFile;
206+
};
207+
};
208+
})
209+
(mkIf cfg.grafana.enable {
210+
services.grafana = {
211+
enable = true;
212+
settings = {
213+
server = {
214+
domain = cfg.virtualHost;
215+
http_port = cfg.grafana.port;
216+
http_addr = cfg.grafana.listenAddress;
217+
root_url = "http://%(domain)s${cfg.grafana.urlPath}";
218+
serve_from_sub_path = cfg.grafana.urlPath != "/";
219+
};
220+
security = {
221+
allow_embedding = true;
222+
disable_gravatr = true;
223+
};
224+
users = {
225+
allow_sign_up = false;
226+
};
227+
"auth.anonymous".enabled = false;
228+
"auth.basic".enabled = false;
229+
analytics.reporting_enabled = false;
230+
};
231+
provision = {
232+
enable = true;
233+
datasources.path = ./grafana/datasource.yml;
234+
# Need to duplicate dashboards.yml since it contains absolute paths
235+
# which are incompatible with NixOS
236+
dashboards.settings = {
237+
apiVersion = 1;
238+
providers = [
239+
{
240+
name = "teslamate";
241+
orgId = 1;
242+
folder = "TeslaMate";
243+
folderUid = "Nr4ofiDZk";
244+
type = "file";
245+
disableDeletion = false;
246+
editable = true;
247+
updateIntervalSeconds = 86400;
248+
options.path = lib.sources.sourceFilesBySuffices
249+
./grafana/dashboards
250+
[ ".json" ];
251+
}
252+
{
253+
name = "teslamate_internal";
254+
orgId = 1;
255+
folder = "Internal";
256+
folderUid = "Nr5ofiDZk";
257+
type = "file";
258+
disableDeletion = false;
259+
editable = true;
260+
updateIntervalSeconds = 86400;
261+
options.path = lib.sources.sourceFilesBySuffices
262+
./grafana/dashboards/internal
263+
[ ".json" ];
264+
}
265+
{
266+
name = "teslamate_reports";
267+
orgId = 1;
268+
folder = "Reports";
269+
folderUid = "Nr6ofiDZk";
270+
type = "file";
271+
disableDeletion = false;
272+
editable = true;
273+
updateIntervalSeconds = 86400;
274+
options.path = lib.sources.sourceFilesBySuffices
275+
./grafana/dashboards/reports
276+
[ ".json" ];
277+
}
278+
];
279+
};
280+
};
281+
};
282+
283+
systemd.services.grafana = {
284+
serviceConfig.EnvironmentFile = cfg.secrestFile;
285+
environment = {
286+
DATABASE_USER = cfg.postgres.user;
287+
DATABASE_NAME = cfg.postgres.database;
288+
DATABASE_HOST = cfg.postgres.host;
289+
DATABASE_PORT = toString cfg.postgres.port;
290+
DATABASE_SSL_MODE = "disable";
291+
};
292+
};
293+
})
294+
]);
295+
}

0 commit comments

Comments
 (0)