1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.oncall;
10 settingsFormat = pkgs.formats.yaml { };
11 configFile = settingsFormat.generate "oncall_extra_settings.yaml" cfg.settings;
12
13in
14{
15 options.services.oncall = {
16
17 enable = lib.mkEnableOption "Oncall web app";
18
19 package = lib.mkPackageOption pkgs "oncall" { };
20
21 database.createLocally = lib.mkEnableOption "Create the database and database user locally." // {
22 default = true;
23 };
24
25 settings = lib.mkOption {
26 type = lib.types.submodule {
27 freeformType = settingsFormat.type;
28 options = {
29 oncall_host = lib.mkOption {
30 type = lib.types.str;
31 default = "localhost";
32 description = "FQDN for the Oncall instance.";
33 };
34 db.conn = {
35 kwargs = {
36 user = lib.mkOption {
37 type = lib.types.str;
38 default = "oncall";
39 description = "Database user.";
40 };
41 host = lib.mkOption {
42 type = lib.types.str;
43 default = "localhost";
44 description = "Database host.";
45 };
46 database = lib.mkOption {
47 type = lib.types.str;
48 default = "oncall";
49 description = "Database name.";
50 };
51 };
52 str = lib.mkOption {
53 type = lib.types.str;
54 default = "%(scheme)s://%(user)s@%(host)s:%(port)s/%(database)s?charset=%(charset)s&unix_socket=/run/mysqld/mysqld.sock";
55 description = ''
56 Database connection scheme. The default specifies the
57 connection through a local socket.
58 '';
59 };
60 require_auth = lib.mkOption {
61 type = lib.types.bool;
62 default = true;
63 description = ''
64 Whether authentication is required to access the web app.
65 '';
66 };
67 };
68 };
69 };
70 default = { };
71 description = ''
72 Extra configuration options to append or override.
73 For available and default option values see
74 [upstream configuration file](https://github.com/linkedin/oncall/blob/master/configs/config.yaml)
75 and the administration part in the
76 [offical documentation](https://oncall.tools/docs/admin_guide.html).
77 '';
78 };
79
80 secretFile = lib.mkOption {
81 type = lib.types.pathWith {
82 inStore = false;
83 absolute = true;
84 };
85 example = "/run/keys/oncall-dbpassword";
86 description = ''
87 A YAML file containing secrets such as database or user passwords.
88 Some variables that can be considered secrets are:
89
90 - db.conn.kwargs.password:
91 Password used to authenticate to the database.
92
93 - session.encrypt_key:
94 Key for encrypting/signing session cookies.
95 Change to random long values in production.
96
97 - session.sign_key:
98 Key for encrypting/signing session cookies.
99 Change to random long values in production.
100 '';
101 };
102
103 };
104
105 config = lib.mkIf cfg.enable {
106
107 # Disable debug, only needed for development
108 services.oncall.settings = lib.mkMerge [
109 ({
110 debug = lib.mkDefault false;
111 auth.debug = lib.mkDefault false;
112 })
113 ];
114
115 services.uwsgi = {
116 enable = true;
117 plugins = [ "python3" ];
118 user = "oncall";
119 instance = {
120 type = "emperor";
121 vassals = {
122 oncall = {
123 type = "normal";
124 env = [
125 "PYTHONPATH=${pkgs.oncall.pythonPath}"
126 (
127 "ONCALL_EXTRA_CONFIG="
128 + (lib.concatStringsSep "," (
129 [ configFile ] ++ lib.optional (cfg.secretFile != null) cfg.secretFile
130 ))
131 )
132 "STATIC_ROOT=/var/lib/oncall"
133 ];
134 module = "oncall.app:get_wsgi_app()";
135 socket = "${config.services.uwsgi.runDir}/oncall.sock";
136 socketGroup = "nginx";
137 immediate-gid = "nginx";
138 chmod-socket = "770";
139 pyargv = "${pkgs.oncall}/share/configs/config.yaml";
140 buffer-size = 32768;
141 };
142 };
143 };
144 };
145
146 services.nginx = {
147 enable = lib.mkDefault true;
148 virtualHosts."${cfg.settings.oncall_host}".locations = {
149 "/".extraConfig = "uwsgi_pass unix://${config.services.uwsgi.runDir}/oncall.sock;";
150 };
151 };
152
153 services.mysql = lib.mkIf cfg.database.createLocally {
154 enable = true;
155 package = lib.mkDefault pkgs.mariadb;
156 ensureDatabases = [ cfg.settings.db.conn.kwargs.database ];
157 ensureUsers = [
158 {
159 name = cfg.settings.db.conn.kwargs.user;
160 ensurePermissions = {
161 "${cfg.settings.db.conn.kwargs.database}.*" = "ALL PRIVILEGES";
162 };
163 }
164 ];
165 };
166
167 users.users.oncall = {
168 group = "nginx";
169 isSystemUser = true;
170 };
171
172 systemd = {
173 services = {
174 uwsgi.serviceConfig.StateDirectory = "oncall";
175 oncall-setup-database = lib.mkIf cfg.database.createLocally {
176 description = "Set up Oncall database";
177 serviceConfig = {
178 Type = "oneshot";
179 RemainAfterExit = true;
180 };
181 requiredBy = [ "uwsgi.service" ];
182 after = [ "mysql.service" ];
183 script =
184 let
185 mysql = "${lib.getExe' config.services.mysql.package "mysql"}";
186 in
187 ''
188 if [ ! -f /var/lib/oncall/.dbexists ]; then
189 # Load database schema provided with package
190 ${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema.v0.sql
191 ${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema-update.v0-1602184489.sql
192 touch /var/lib/oncall/.dbexists
193 fi
194 '';
195 };
196 };
197 };
198
199 };
200
201 meta.maintainers = with lib.maintainers; [ onny ];
202
203}