Unverified Commit 002ebbc4 authored by Jonas Heinrich's avatar Jonas Heinrich Committed by GitHub
Browse files

oncall: init at 2.1.7; nixos/oncall: init (#388723)

parents b5ff921a 82631e0e
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -58,6 +58,8 @@

- [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`.

- [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).

- [Homer](https://homer-demo.netlify.app/), a very simple static homepage for your server. Available as [services.homer](options.html#opt-services.homer).

- [Ghidra](https://ghidra-sre.org/), a software reverse engineering (SRE) suite of tools. Available as [programs.ghidra](options.html#opt-programs.ghidra).
+1 −0
Original line number Diff line number Diff line
@@ -1608,6 +1608,7 @@
  ./services/web-apps/nostr-rs-relay.nix
  ./services/web-apps/ocis.nix
  ./services/web-apps/olivetin.nix
  ./services/web-apps/oncall.nix
  ./services/web-apps/onlyoffice.nix
  ./services/web-apps/open-web-calendar.nix
  ./services/web-apps/openvscode-server.nix
+203 −0
Original line number Diff line number Diff line
{
  config,
  lib,
  pkgs,
  ...
}:
let

  cfg = config.services.oncall;
  settingsFormat = pkgs.formats.yaml { };
  configFile = settingsFormat.generate "oncall_extra_settings.yaml" cfg.settings;

in
{
  options.services.oncall = {

    enable = lib.mkEnableOption "Oncall web app";

    package = lib.mkPackageOption pkgs "oncall" { };

    database.createLocally = lib.mkEnableOption "Create the database and database user locally." // {
      default = true;
    };

    settings = lib.mkOption {
      type = lib.types.submodule {
        freeformType = settingsFormat.type;
        options = {
          oncall_host = lib.mkOption {
            type = lib.types.str;
            default = "localhost";
            description = "FQDN for the Oncall instance.";
          };
          db.conn = {
            kwargs = {
              user = lib.mkOption {
                type = lib.types.str;
                default = "oncall";
                description = "Database user.";
              };
              host = lib.mkOption {
                type = lib.types.str;
                default = "localhost";
                description = "Database host.";
              };
              database = lib.mkOption {
                type = lib.types.str;
                default = "oncall";
                description = "Database name.";
              };
            };
            str = lib.mkOption {
              type = lib.types.str;
              default = "%(scheme)s://%(user)s@%(host)s:%(port)s/%(database)s?charset=%(charset)s&unix_socket=/run/mysqld/mysqld.sock";
              description = ''
                Database connection scheme. The default specifies the
                connection through a local socket.
              '';
            };
            require_auth = lib.mkOption {
              type = lib.types.bool;
              default = true;
              description = ''
                Whether authentication is required to access the web app.
              '';
            };
          };
        };
      };
      default = { };
      description = ''
        Extra configuration options to append or override.
        For available and default option values see
        [upstream configuration file](https://github.com/linkedin/oncall/blob/master/configs/config.yaml)
        and the administration part in the
        [offical documentation](https://oncall.tools/docs/admin_guide.html).
      '';
    };

    secretFile = lib.mkOption {
      type = lib.types.pathWith {
        inStore = false;
        absolute = true;
      };
      example = "/run/keys/oncall-dbpassword";
      description = ''
        A YAML file containing secrets such as database or user passwords.
        Some variables that can be considered secrets are:

        - db.conn.kwargs.password:
          Password used to authenticate to the database.

        - session.encrypt_key:
          Key for encrypting/signing session cookies.
          Change to random long values in production.

        - session.sign_key:
          Key for encrypting/signing session cookies.
          Change to random long values in production.
      '';
    };

  };

  config = lib.mkIf cfg.enable {

    # Disable debug, only needed for development
    services.oncall.settings = lib.mkMerge [
      ({
        debug = lib.mkDefault false;
        auth.debug = lib.mkDefault false;
      })
    ];

    services.uwsgi = {
      enable = true;
      plugins = [ "python3" ];
      user = "oncall";
      instance = {
        type = "emperor";
        vassals = {
          oncall = {
            type = "normal";
            env = [
              "PYTHONPATH=${pkgs.oncall.pythonPath}"
              (
                "ONCALL_EXTRA_CONFIG="
                + (lib.concatStringsSep "," (
                  [ configFile ] ++ lib.optional (cfg.secretFile != null) cfg.secretFile
                ))
              )
              "STATIC_ROOT=/var/lib/oncall"
            ];
            module = "oncall.app:get_wsgi_app()";
            socket = "${config.services.uwsgi.runDir}/oncall.sock";
            socketGroup = "nginx";
            immediate-gid = "nginx";
            chmod-socket = "770";
            pyargv = "${pkgs.oncall}/share/configs/config.yaml";
            buffer-size = 32768;
          };
        };
      };
    };

    services.nginx = {
      enable = lib.mkDefault true;
      virtualHosts."${cfg.settings.oncall_host}".locations = {
        "/".extraConfig = "uwsgi_pass unix://${config.services.uwsgi.runDir}/oncall.sock;";
      };
    };

    services.mysql = lib.mkIf cfg.database.createLocally {
      enable = true;
      package = lib.mkDefault pkgs.mariadb;
      ensureDatabases = [ cfg.settings.db.conn.kwargs.database ];
      ensureUsers = [
        {
          name = cfg.settings.db.conn.kwargs.user;
          ensurePermissions = {
            "${cfg.settings.db.conn.kwargs.database}.*" = "ALL PRIVILEGES";
          };
        }
      ];
    };

    users.users.oncall = {
      group = "nginx";
      isSystemUser = true;
    };

    systemd = {
      services = {
        uwsgi.serviceConfig.StateDirectory = "oncall";
        oncall-setup-database = lib.mkIf cfg.database.createLocally {
          description = "Set up Oncall database";
          serviceConfig = {
            Type = "oneshot";
            RemainAfterExit = true;
          };
          requiredBy = [ "uwsgi.service" ];
          after = [ "mysql.service" ];
          script =
            let
              mysql = "${lib.getExe' config.services.mysql.package "mysql"}";
            in
            ''
              if [ ! -f /var/lib/oncall/.dbexists ]; then
                # Load database schema provided with package
                ${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema.v0.sql
                ${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema-update.v0-1602184489.sql
                touch /var/lib/oncall/.dbexists
              fi
            '';
        };
      };
    };

  };

  meta.maintainers = with lib.maintainers; [ onny ];

}
+1 −0
Original line number Diff line number Diff line
@@ -618,6 +618,7 @@ in
  odoo = handleTest ./odoo.nix { };
  odoo17 = handleTest ./odoo.nix { package = pkgs.odoo17; };
  odoo16 = handleTest ./odoo.nix { package = pkgs.odoo16; };
  oncall = runTest ./web-apps/oncall.nix;
  # 9pnet_virtio used to mount /nix partition doesn't support
  # hibernation. This test happens to work on x86_64-linux but
  # not on other platforms.
+156 −0
Original line number Diff line number Diff line
{
  lib,
  pkgs,
  config,
  ...
}:
let
  ldapDomain = "example.org";
  ldapSuffix = "dc=example,dc=org";

  ldapRootUser = "root";
  ldapRootPassword = "foobar23";

  testUser = "myuser";
  testPassword = "foobar23";
  teamName = "myteam";
in
{
  name = "oncall";
  meta.maintainers = with lib.maintainers; [ onny ];

  nodes = {
    machine = {
      virtualisation.memorySize = 2048;

      environment.etc."oncall-secrets.yml".text = ''
        auth:
          ldap_bind_password: "${ldapRootPassword}"
      '';

      environment.systemPackages = [ pkgs.jq ];

      services.oncall = {
        enable = true;
        settings = {
          auth = {
            module = "oncall.auth.modules.ldap_import";
            ldap_url = "ldap://localhost";
            ldap_user_suffix = "";
            ldap_bind_user = "cn=${ldapRootUser},${ldapSuffix}";
            ldap_base_dn = "ou=accounts,${ldapSuffix}";
            ldap_search_filter = "(uid=%s)";
            import_user = true;
            attrs = {
              username = "uid";
              full_name = "cn";
              email = "mail";
              mobile = "telephoneNumber";
              sms = "mobile";
            };
          };
        };
        secretFile = "/etc/oncall-secrets.yml";
      };

      services.openldap = {
        enable = true;
        settings = {
          children = {
            "cn=schema".includes = [
              "${pkgs.openldap}/etc/schema/core.ldif"
              "${pkgs.openldap}/etc/schema/cosine.ldif"
              "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
              "${pkgs.openldap}/etc/schema/nis.ldif"
            ];
            "olcDatabase={1}mdb" = {
              attrs = {
                objectClass = [
                  "olcDatabaseConfig"
                  "olcMdbConfig"
                ];
                olcDatabase = "{1}mdb";
                olcDbDirectory = "/var/lib/openldap/db";
                olcSuffix = ldapSuffix;
                olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
                olcRootPW = ldapRootPassword;
              };
            };
          };
        };
        declarativeContents = {
          ${ldapSuffix} = ''
            dn: ${ldapSuffix}
            objectClass: top
            objectClass: dcObject
            objectClass: organization
            o: ${ldapDomain}

            dn: ou=accounts,${ldapSuffix}
            objectClass: top
            objectClass: organizationalUnit

            dn: uid=${testUser},ou=accounts,${ldapSuffix}
            objectClass: top
            objectClass: inetOrgPerson
            uid: ${testUser}
            userPassword: ${testPassword}
            cn: Test User
            sn: User
            mail: test@example.org
            telephoneNumber: 012345678910
            mobile: 012345678910
          '';
        };
      };
    };
  };

  testScript = ''
    start_all()
    machine.wait_for_unit("uwsgi.service")
    machine.wait_for_unit("nginx.service")
    machine.wait_for_file("/run/uwsgi/oncall.sock")
    machine.wait_for_unit("oncall-setup-database.service")

    with subtest("Home screen loads"):
        machine.succeed(
            "curl -sSfL http://[::1]:80 | grep '<title>Oncall</title>'"
        )

    with subtest("Staticfiles can be fetched"):
        machine.wait_until_succeeds(
          "curl -sSfL http://[::1]:80/static/bundles/libs.js"
        )

    with subtest("Staticfiles are generated"):
        machine.succeed(
          "test -e /var/lib/oncall/static/bundles/libs.js"
        )

    with subtest("Create and verify team via REST API"):
        import json

        # Log in and store the session cookie
        login_response = machine.succeed("""
            curl -sSfL -c cookies -X POST \
                --data-raw 'username=${testUser}&password=${testPassword}' \
                http://[::1]:80/login
        """)

        # Parse csrf token
        login_response_data = json.loads(login_response)
        csrf_token = login_response_data["csrf_token"]

        # Create the team
        machine.succeed(
          f"""curl -sSfL -b cookies -X POST -H 'Content-Type: application/json' -H 'X-CSRF-Token: {csrf_token}' -d '{{"name": "${teamName}", "email": "test@example.com", "scheduling_timezone": "Europe/Berlin", "iris_enabled": false}}' http://[::1]:80/api/v0/teams/"""
        )

        # Query the created team
        machine.succeed("""
            curl -sSfL -b cookies http://[::1]:80/api/v0/teams/${teamName} | jq -e '.name == "${teamName}"'
        """)

  '';
}
Loading