From 4a57097af6e1fb56330a8703e99d5a9f7b73d063 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 10 Mar 2025 16:42:10 +0100 Subject: [PATCH 1/5] oncall: init at 2.1.7 --- pkgs/by-name/on/oncall/package.nix | 128 ++++++++++++++++++ .../on/oncall/support_custom_state_dir.patch | 56 ++++++++ .../on/oncall/support_extra_config.patch | 120 ++++++++++++++++ pkgs/by-name/on/oncall/verbose_logging.patch | 33 +++++ 4 files changed, 337 insertions(+) create mode 100644 pkgs/by-name/on/oncall/package.nix create mode 100644 pkgs/by-name/on/oncall/support_custom_state_dir.patch create mode 100644 pkgs/by-name/on/oncall/support_extra_config.patch create mode 100644 pkgs/by-name/on/oncall/verbose_logging.patch diff --git a/pkgs/by-name/on/oncall/package.nix b/pkgs/by-name/on/oncall/package.nix new file mode 100644 index 000000000000..62355b4496d3 --- /dev/null +++ b/pkgs/by-name/on/oncall/package.nix @@ -0,0 +1,128 @@ +{ + lib, + python3, + fetchFromGitHub, + fetchPypi, + oncall, + nixosTests, + + # Override Python packages using + # self: super: { pkg = super.pkg.overridePythonAttrs (oldAttrs: { ... }); } + # Applied after defaultOverrides + packageOverrides ? self: super: { }, +}: +let + defaultOverrides = [ + # Override the version of some packages pinned in Oncall's setup.py + (self: super: { + # Support for Falcon 4.X missing + # https://github.com/linkedin/oncall/issues/430 + falcon = super.falcon.overridePythonAttrs (oldAttrs: rec { + version = "3.1.3"; + src = fetchFromGitHub { + owner = "falconry"; + repo = "falcon"; + tag = version; + hash = "sha256-7719gOM8WQVjODwOSo7HpH3HMFFeCGQQYBKktBAevig="; + }; + }); + }) + ]; + + python = python3.override { + self = python; + packageOverrides = lib.composeManyExtensions (defaultOverrides ++ [ packageOverrides ]); + }; +in +python.pkgs.buildPythonApplication rec { + pname = "oncall"; + version = "2.1.7"; + format = "setuptools"; + + src = fetchFromGitHub { + owner = "linkedin"; + repo = pname; + tag = "v${version}"; + hash = "sha256-oqzU4UTpmAcZhqRilquxWQVyHv8bqq0AGraiSqwauiI="; + }; + + patches = [ + # Add support for loading extra settings file + ./support_extra_config.patch + + # Support storing assets in custom state dir + ./support_custom_state_dir.patch + + # Log Python errors to uwsgi + ./verbose_logging.patch + ]; + + dependencies = with python.pkgs; [ + beaker + falcon + falcon-cors + gevent + gunicorn + icalendar + irisclient + jinja2 + phonenumbers + pymysql + python-ldap + pytz + pyyaml + ujson + webassets + ]; + + postInstall = '' + mkdir "$out/share" + cp -r configs db "$out/share/" + ''; + + checkInputs = with python.pkgs; [ + pytestCheckHook + pytest-mock + ]; + + disabledTestPaths = [ + # Tests require running web server + "e2e/test_audit.py" + "e2e/test_events.py" + "e2e/test_ical.py" + "e2e/test_login.py" + "e2e/test_notification.py" + "e2e/test_override.py" + "e2e/test_pin.py" + "e2e/test_populate.py" + "e2e/test_roles.py" + "e2e/test_roster_suggest.py" + "e2e/test_rosters.py" + "e2e/test_schedules.py" + "e2e/test_services.py" + "e2e/test_subscription.py" + "e2e/test_teams.py" + "e2e/test_users.py" + ]; + + pythonImportsCheck = [ + "oncall" + ]; + + passthru = { + tests = { + inherit (nixosTests) oncall; + }; + inherit python; + pythonPath = "${python.pkgs.makePythonPath dependencies}:${oncall}/${python.sitePackages}"; + }; + + meta = { + description = "A calendar web-app designed for scheduling and managing on-call shifts"; + homepage = "http://oncall.tools"; + changelog = "https://github.com/linkedin/oncall/blob/${src.tag}/CHANGELOG.md"; + license = lib.licenses.bsd2; + maintainers = with lib.maintainers; [ onny ]; + mainProgram = "oncall"; + }; +} diff --git a/pkgs/by-name/on/oncall/support_custom_state_dir.patch b/pkgs/by-name/on/oncall/support_custom_state_dir.patch new file mode 100644 index 000000000000..f0e181c255d9 --- /dev/null +++ b/pkgs/by-name/on/oncall/support_custom_state_dir.patch @@ -0,0 +1,56 @@ +diff --git a/src/oncall/ui/__init__.py b/src/oncall/ui/__init__.py +index a94fb17..364404a 100644 +--- a/src/oncall/ui/__init__.py ++++ b/src/oncall/ui/__init__.py +@@ -18,8 +18,12 @@ from webassets.ext.jinja2 import AssetsExtension + from webassets.script import CommandLineEnvironment + + STATIC_ROOT = environ.get('STATIC_ROOT', path.abspath(path.dirname(__file__))) ++SOURCE_ROOT = path.abspath(path.dirname(__file__)) + assets_env = AssetsEnvironment(path.join(STATIC_ROOT, 'static'), + url='/static') ++assets_env.cache = False ++assets_env.manifest = False ++assets_env.load_path = [ path.join(SOURCE_ROOT, 'static') ] + + assets_env.register('libs', Bundle( + 'js/jquery-3.3.1.min.js', 'js/handlebars-4.0.12.min.js', 'js/bootstrap.min.js', +@@ -45,7 +49,7 @@ logger = logging.getLogger('webassets') + logger.addHandler(logging.StreamHandler()) + + jinja2_env = Jinja2Environment(extensions=[AssetsExtension], autoescape=True) +-jinja2_env.loader = FileSystemLoader(path.join(STATIC_ROOT, 'templates')) ++jinja2_env.loader = FileSystemLoader(path.join(SOURCE_ROOT, 'templates')) + jinja2_env.assets_environment = assets_env + + _filename_ascii_strip_re = re.compile(r'[^A-Za-z0-9_.-]') +@@ -113,14 +117,15 @@ def secure_filename(filename): + class StaticResource(object): + allow_no_auth = True + +- def __init__(self, path): ++ def __init__(self, path, root): + self.path = path.lstrip('/') ++ self.root = root + + def on_get(self, req, resp, filename): + suffix = path.splitext(req.path)[1] + resp.content_type = mimes.get(suffix, 'application/octet-stream') + +- filepath = path.join(STATIC_ROOT, self.path, secure_filename(filename)) ++ filepath = path.join(self.root, self.path, secure_filename(filename)) + try: + resp.stream = open(filepath, 'rb') + resp.content_length = path.getsize(filepath) +@@ -153,8 +158,8 @@ def init(application, config): + + application.add_sink(index, '/') + application.add_route('/static/bundles/{filename}', +- StaticResource('/static/bundles')) ++ StaticResource('/static/bundles', STATIC_ROOT)) + application.add_route('/static/images/{filename}', +- StaticResource('/static/images')) ++ StaticResource('/static/images', SOURCE_ROOT)) + application.add_route('/static/fonts/{filename}', +- StaticResource('/static/fonts')) ++ StaticResource('/static/fonts', SOURCE_ROOT)) diff --git a/pkgs/by-name/on/oncall/support_extra_config.patch b/pkgs/by-name/on/oncall/support_extra_config.patch new file mode 100644 index 000000000000..25468c8d8858 --- /dev/null +++ b/pkgs/by-name/on/oncall/support_extra_config.patch @@ -0,0 +1,120 @@ +diff --git a/src/oncall/bin/notifier.py b/src/oncall/bin/notifier.py +index 25142b8..cbc92aa 100644 +--- a/src/oncall/bin/notifier.py ++++ b/src/oncall/bin/notifier.py +@@ -32,11 +32,29 @@ send_queue = queue.Queue() + + default_timezone = None + ++def merge_dict(extend_me, extend_by): ++ if isinstance(extend_by, dict): ++ for k, v in extend_by.items(): ++ if isinstance(v, dict) and isinstance(extend_me.get(k), dict): ++ merge_dict(extend_me[k], v) ++ else: ++ extend_me[k] = v ++ return extend_me + + def load_config_file(config_path): + with open(config_path, 'r', encoding='utf-8') as h: + config = yaml.safe_load(h) + ++ # Check for extra config files from environment variable ++ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') ++ if extra_config_paths: ++ for extra_path in extra_config_paths.split(','): ++ extra_path = extra_path.strip() ++ if os.path.isfile(extra_path): ++ with open(extra_path, 'r') as f: ++ extra_config = yaml.safe_load(f) or {} ++ config = merge_dict(config, extra_config) ++ + if 'init_config_hook' in config: + try: + module = config['init_config_hook'] +diff --git a/src/oncall/user_sync/ldap_sync.py b/src/oncall/user_sync/ldap_sync.py +index ef9a8ec..c5f027d 100644 +--- a/src/oncall/user_sync/ldap_sync.py ++++ b/src/oncall/user_sync/ldap_sync.py +@@ -6,6 +6,7 @@ import time + import yaml + import logging + import ldap ++import os + + from oncall import metrics + from ldap.controls import SimplePagedResultsControl +@@ -447,9 +448,28 @@ def main(config): + logger.info('Sleeping for %s seconds' % sleep_time) + sleep(sleep_time) + ++def merge_dict(extend_me, extend_by): ++ if isinstance(extend_by, dict): ++ for k, v in extend_by.items(): ++ if isinstance(v, dict) and isinstance(extend_me.get(k), dict): ++ merge_dict(extend_me[k], v) ++ else: ++ extend_me[k] = v ++ return extend_me + + if __name__ == '__main__': + config_path = sys.argv[1] + with open(config_path, 'r', encoding='utf-8') as config_file: + config = yaml.safe_load(config_file) ++ ++ # Check for extra config files from environment variable ++ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') ++ if extra_config_paths: ++ for extra_path in extra_config_paths.split(','): ++ extra_path = extra_path.strip() ++ if os.path.isfile(extra_path): ++ with open(extra_path, 'r') as f: ++ extra_config = yaml.safe_load(f) or {} ++ config = merge_dict(config, extra_config) ++ + main(config) +diff --git a/src/oncall/utils.py b/src/oncall/utils.py +index a0b695c..278ca1d 100644 +--- a/src/oncall/utils.py ++++ b/src/oncall/utils.py +@@ -13,6 +13,7 @@ from pytz import timezone + from .constants import ONCALL_REMINDER + from . import constants + import re ++import os + + invalid_char_reg = re.compile(r'[!"#%-,\.\/;->@\[-\^`\{-~]+') + DAY = 86400 +@@ -27,10 +28,31 @@ def insert_notification(x, y): + def update_notification(x, y): + pass + ++def merge_dict(extend_me, extend_by): ++ if isinstance(extend_by, dict): ++ for k, v in extend_by.items(): ++ if isinstance(v, dict) and isinstance(extend_me.get(k), dict): ++ merge_dict(extend_me[k], v) ++ else: ++ extend_me[k] = v ++ return extend_me + + def read_config(config_path): ++ + with open(config_path, 'r', encoding='utf8') as config_file: +- return yaml.safe_load(config_file) ++ config = yaml.safe_load(config_file) ++ ++ # Check for extra config files from environment variable ++ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') ++ if extra_config_paths: ++ for extra_path in extra_config_paths.split(','): ++ extra_path = extra_path.strip() ++ if os.path.isfile(extra_path): ++ with open(extra_path, 'r') as f: ++ extra_config = yaml.safe_load(f) or {} ++ config = merge_dict(config, extra_config) ++ ++ return config + + + def create_notification(context, team_id, role_ids, type_name, users_involved, cursor, **kwargs): diff --git a/pkgs/by-name/on/oncall/verbose_logging.patch b/pkgs/by-name/on/oncall/verbose_logging.patch new file mode 100644 index 000000000000..b2b4dfda5af6 --- /dev/null +++ b/pkgs/by-name/on/oncall/verbose_logging.patch @@ -0,0 +1,33 @@ +diff --git a/src/oncall/app.py b/src/oncall/app.py +index 370fcf4..59f014e 100644 +--- a/src/oncall/app.py ++++ b/src/oncall/app.py +@@ -62,9 +62,19 @@ class AuthMiddleware(object): + + application = None + ++def handle_uncaught_exception(req, resp, ex, params): ++ logging.exception('Unhandled error') ++ raise falcon.HTTPInternalServerError(title='App error') ++ ++ ++def handle_http_error(req, resp, ex, params): ++ logging.exception('HTTP error') ++ raise ex ++ + + def init_falcon_api(config): + global application ++ + cors = CORS(allow_origins_list=config.get('allow_origins_list', [])) + middlewares = [ + SecurityHeaderMiddleware(), +@@ -74,6 +84,8 @@ def init_falcon_api(config): + if config.get('require_auth'): + middlewares.append(AuthMiddleware()) + application = falcon.App(middleware=middlewares) ++ application.add_error_handler(falcon.HTTPError, handle_http_error) ++ application.add_error_handler(Exception, handle_uncaught_exception) + application.req_options.auto_parse_form_urlencoded = False + application.set_error_serializer(json_error_serializer) + application.req_options.strip_url_path_trailing_slash = True From ce6e5f037a83642e6864a8738b1c174aa691046d Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 10 Mar 2025 16:58:58 +0100 Subject: [PATCH 2/5] nixos/oncall: init --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/web-apps/oncall.nix | 203 ++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 nixos/modules/services/web-apps/oncall.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 453e0ed450bd..d4576463ed1e 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -62,6 +62,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). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 073f8ebede1a..d4f09fef8364 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/services/web-apps/oncall.nix b/nixos/modules/services/web-apps/oncall.nix new file mode 100644 index 000000000000..cd842606f278 --- /dev/null +++ b/nixos/modules/services/web-apps/oncall.nix @@ -0,0 +1,203 @@ +{ + 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 ]; + +} From cb106a1989e26ffe65d4e35374d11fd0a305f8e9 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Tue, 11 Mar 2025 17:06:38 +0100 Subject: [PATCH 3/5] python3Packages.falcon-cors: init at 1.1.7 --- .../python-modules/falcon-cors/default.nix | 36 +++++++++++++++++++ pkgs/top-level/python-packages.nix | 4 +++ 2 files changed, 40 insertions(+) create mode 100644 pkgs/development/python-modules/falcon-cors/default.nix diff --git a/pkgs/development/python-modules/falcon-cors/default.nix b/pkgs/development/python-modules/falcon-cors/default.nix new file mode 100644 index 000000000000..b32ff5f4922f --- /dev/null +++ b/pkgs/development/python-modules/falcon-cors/default.nix @@ -0,0 +1,36 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + falcon, +}: + +buildPythonPackage rec { + pname = "falcon-cors"; + version = "1.1.7"; + + src = fetchFromGitHub { + owner = "lwcolton"; + repo = "falcon-cors"; + tag = version; + hash = "sha256-jlEWP7gXbWfdY4coEIM6NWuBf4LOGbUAFMNvqip/FcA="; + }; + + build-system = [ setuptools ]; + + dependencies = [ falcon ]; + + # Test fail with falcon >= 4 + # https://github.com/lwcolton/falcon-cors/issues/25 + doCheck = false; + + pythonImportsCheck = [ "falcon_cors" ]; + + meta = { + description = "CORS support for Falcon"; + homepage = "https://github.com/lwcolton/falcon-cors"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ onny ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 1df25bc30548..9cb4339d3912 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -4749,12 +4749,16 @@ self: super: with self; { falcon = callPackage ../development/python-modules/falcon { }; + falcon-cors = callPackage ../development/python-modules/falcon-cors { }; + falconpy = callPackage ../development/python-modules/falconpy { }; faraday-agent-parameters-types = callPackage ../development/python-modules/faraday-agent-parameters-types { }; + falcon-cors = callPackage ../development/python-modules/falcon-cors { }; + faraday-plugins = callPackage ../development/python-modules/faraday-plugins { }; farama-notifications = callPackage ../development/python-modules/farama-notifications { }; From ca662571c58b14baff06e881b2e317af70d817f3 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Tue, 11 Mar 2025 17:06:59 +0100 Subject: [PATCH 4/5] python3Packages.irisclient: init 1.2.0 --- .../python-modules/irisclient/default.nix | 43 +++++++++++++++++++ pkgs/top-level/python-packages.nix | 4 +- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 pkgs/development/python-modules/irisclient/default.nix diff --git a/pkgs/development/python-modules/irisclient/default.nix b/pkgs/development/python-modules/irisclient/default.nix new file mode 100644 index 000000000000..d3250cdb1aa6 --- /dev/null +++ b/pkgs/development/python-modules/irisclient/default.nix @@ -0,0 +1,43 @@ +{ + lib, + buildPythonPackage, + setuptools, + fetchFromGitHub, + requests, + pytestCheckHook, + httmock, + pytest-mock, +}: + +buildPythonPackage rec { + pname = "irisclient"; + version = "1.2.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "houqp"; + repo = "iris-python-client"; + tag = "v${version}"; + hash = "sha256-fXMw2BopkEqjklR6jr7QQIZyxLq6NHKm2rHwTCbtxR0="; + }; + + build-system = [ setuptools ]; + + dependencies = [ requests ]; + + checkInputs = [ + httmock + pytestCheckHook + pytest-mock + ]; + + pythonImportsCheck = [ "irisclient" ]; + + meta = { + description = "Python client for Iris REST api"; + changelog = "https://github.com/houqp/iris-python-client/blob/v${src.tag}/HISTORY.rst"; + homepage = "https://github.com/houqp/iris-python-client"; + license = lib.licenses.bsd2; + maintainers = with lib.maintainers; [ onny ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 9cb4339d3912..860350ccde9a 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -4757,8 +4757,6 @@ self: super: with self; { callPackage ../development/python-modules/faraday-agent-parameters-types { }; - falcon-cors = callPackage ../development/python-modules/falcon-cors { }; - faraday-plugins = callPackage ../development/python-modules/faraday-plugins { }; farama-notifications = callPackage ../development/python-modules/farama-notifications { }; @@ -6893,6 +6891,8 @@ self: super: with self; { irctokens = callPackage ../development/python-modules/irctokens { }; + irisclient = callPackage ../development/python-modules/irisclient { }; + isal = callPackage ../development/python-modules/isal { }; isbnlib = callPackage ../development/python-modules/isbnlib { }; From 82631e0e4c98e0861ee2182fd16836878bb62ef0 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Wed, 19 Mar 2025 16:22:04 +0100 Subject: [PATCH 5/5] nixos/tests/oncall: init --- nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/oncall.nix | 156 ++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 nixos/tests/web-apps/oncall.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 22058134ecb2..3235dc0a62ba 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -619,6 +619,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. diff --git a/nixos/tests/web-apps/oncall.nix b/nixos/tests/web-apps/oncall.nix new file mode 100644 index 000000000000..295bdd8fa308 --- /dev/null +++ b/nixos/tests/web-apps/oncall.nix @@ -0,0 +1,156 @@ +{ + 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 'Oncall'" + ) + + 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}"' + """) + + ''; +}