Skip to content

Commit 002ebbc

Browse files
authored
oncall: init at 2.1.7; nixos/oncall: init (#388723)
2 parents b5ff921 + 82631e0 commit 002ebbc

File tree

12 files changed

+783
-0
lines changed

12 files changed

+783
-0
lines changed

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858

5959
- [vwifi](https://github.com/Raizo62/vwifi), a Wi-Fi simulator daemon leveraging the `mac80211_hwsim` and `vhost_vsock` kernel modules for efficient simulation of multi-node Wi-Fi networks. Available as {option}`services.vwifi`.
6060

61+
- [Oncall](https://oncall.tools), a web-based calendar tool designed for scheduling and managing on-call shifts. Available as [services.oncall](options.html#opt-services.oncall).
62+
6163
- [Homer](https://homer-demo.netlify.app/), a very simple static homepage for your server. Available as [services.homer](options.html#opt-services.homer).
6264

6365
- [Ghidra](https://ghidra-sre.org/), a software reverse engineering (SRE) suite of tools. Available as [programs.ghidra](options.html#opt-programs.ghidra).

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,7 @@
16081608
./services/web-apps/nostr-rs-relay.nix
16091609
./services/web-apps/ocis.nix
16101610
./services/web-apps/olivetin.nix
1611+
./services/web-apps/oncall.nix
16111612
./services/web-apps/onlyoffice.nix
16121613
./services/web-apps/open-web-calendar.nix
16131614
./services/web-apps/openvscode-server.nix
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
9+
cfg = config.services.oncall;
10+
settingsFormat = pkgs.formats.yaml { };
11+
configFile = settingsFormat.generate "oncall_extra_settings.yaml" cfg.settings;
12+
13+
in
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+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ in
618618
odoo = handleTest ./odoo.nix { };
619619
odoo17 = handleTest ./odoo.nix { package = pkgs.odoo17; };
620620
odoo16 = handleTest ./odoo.nix { package = pkgs.odoo16; };
621+
oncall = runTest ./web-apps/oncall.nix;
621622
# 9pnet_virtio used to mount /nix partition doesn't support
622623
# hibernation. This test happens to work on x86_64-linux but
623624
# not on other platforms.

nixos/tests/web-apps/oncall.nix

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{
2+
lib,
3+
pkgs,
4+
config,
5+
...
6+
}:
7+
let
8+
ldapDomain = "example.org";
9+
ldapSuffix = "dc=example,dc=org";
10+
11+
ldapRootUser = "root";
12+
ldapRootPassword = "foobar23";
13+
14+
testUser = "myuser";
15+
testPassword = "foobar23";
16+
teamName = "myteam";
17+
in
18+
{
19+
name = "oncall";
20+
meta.maintainers = with lib.maintainers; [ onny ];
21+
22+
nodes = {
23+
machine = {
24+
virtualisation.memorySize = 2048;
25+
26+
environment.etc."oncall-secrets.yml".text = ''
27+
auth:
28+
ldap_bind_password: "${ldapRootPassword}"
29+
'';
30+
31+
environment.systemPackages = [ pkgs.jq ];
32+
33+
services.oncall = {
34+
enable = true;
35+
settings = {
36+
auth = {
37+
module = "oncall.auth.modules.ldap_import";
38+
ldap_url = "ldap://localhost";
39+
ldap_user_suffix = "";
40+
ldap_bind_user = "cn=${ldapRootUser},${ldapSuffix}";
41+
ldap_base_dn = "ou=accounts,${ldapSuffix}";
42+
ldap_search_filter = "(uid=%s)";
43+
import_user = true;
44+
attrs = {
45+
username = "uid";
46+
full_name = "cn";
47+
email = "mail";
48+
mobile = "telephoneNumber";
49+
sms = "mobile";
50+
};
51+
};
52+
};
53+
secretFile = "/etc/oncall-secrets.yml";
54+
};
55+
56+
services.openldap = {
57+
enable = true;
58+
settings = {
59+
children = {
60+
"cn=schema".includes = [
61+
"${pkgs.openldap}/etc/schema/core.ldif"
62+
"${pkgs.openldap}/etc/schema/cosine.ldif"
63+
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
64+
"${pkgs.openldap}/etc/schema/nis.ldif"
65+
];
66+
"olcDatabase={1}mdb" = {
67+
attrs = {
68+
objectClass = [
69+
"olcDatabaseConfig"
70+
"olcMdbConfig"
71+
];
72+
olcDatabase = "{1}mdb";
73+
olcDbDirectory = "/var/lib/openldap/db";
74+
olcSuffix = ldapSuffix;
75+
olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
76+
olcRootPW = ldapRootPassword;
77+
};
78+
};
79+
};
80+
};
81+
declarativeContents = {
82+
${ldapSuffix} = ''
83+
dn: ${ldapSuffix}
84+
objectClass: top
85+
objectClass: dcObject
86+
objectClass: organization
87+
o: ${ldapDomain}
88+
89+
dn: ou=accounts,${ldapSuffix}
90+
objectClass: top
91+
objectClass: organizationalUnit
92+
93+
dn: uid=${testUser},ou=accounts,${ldapSuffix}
94+
objectClass: top
95+
objectClass: inetOrgPerson
96+
uid: ${testUser}
97+
userPassword: ${testPassword}
98+
cn: Test User
99+
sn: User
100+
101+
telephoneNumber: 012345678910
102+
mobile: 012345678910
103+
'';
104+
};
105+
};
106+
};
107+
};
108+
109+
testScript = ''
110+
start_all()
111+
machine.wait_for_unit("uwsgi.service")
112+
machine.wait_for_unit("nginx.service")
113+
machine.wait_for_file("/run/uwsgi/oncall.sock")
114+
machine.wait_for_unit("oncall-setup-database.service")
115+
116+
with subtest("Home screen loads"):
117+
machine.succeed(
118+
"curl -sSfL http://[::1]:80 | grep '<title>Oncall</title>'"
119+
)
120+
121+
with subtest("Staticfiles can be fetched"):
122+
machine.wait_until_succeeds(
123+
"curl -sSfL http://[::1]:80/static/bundles/libs.js"
124+
)
125+
126+
with subtest("Staticfiles are generated"):
127+
machine.succeed(
128+
"test -e /var/lib/oncall/static/bundles/libs.js"
129+
)
130+
131+
with subtest("Create and verify team via REST API"):
132+
import json
133+
134+
# Log in and store the session cookie
135+
login_response = machine.succeed("""
136+
curl -sSfL -c cookies -X POST \
137+
--data-raw 'username=${testUser}&password=${testPassword}' \
138+
http://[::1]:80/login
139+
""")
140+
141+
# Parse csrf token
142+
login_response_data = json.loads(login_response)
143+
csrf_token = login_response_data["csrf_token"]
144+
145+
# Create the team
146+
machine.succeed(
147+
f"""curl -sSfL -b cookies -X POST -H 'Content-Type: application/json' -H 'X-CSRF-Token: {csrf_token}' -d '{{"name": "${teamName}", "email": "[email protected]", "scheduling_timezone": "Europe/Berlin", "iris_enabled": false}}' http://[::1]:80/api/v0/teams/"""
148+
)
149+
150+
# Query the created team
151+
machine.succeed("""
152+
curl -sSfL -b cookies http://[::1]:80/api/v0/teams/${teamName} | jq -e '.name == "${teamName}"'
153+
""")
154+
155+
'';
156+
}

0 commit comments

Comments
 (0)