lol

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

authored by

Jonas Heinrich and committed by
GitHub
002ebbc4 b5ff921a

+783
+2
nixos/doc/manual/release-notes/rl-2505.section.md
··· 58 58 59 59 - [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`. 60 60 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 + 61 63 - [Homer](https://homer-demo.netlify.app/), a very simple static homepage for your server. Available as [services.homer](options.html#opt-services.homer). 62 64 63 65 - [Ghidra](https://ghidra-sre.org/), a software reverse engineering (SRE) suite of tools. Available as [programs.ghidra](options.html#opt-programs.ghidra).
+1
nixos/modules/module-list.nix
··· 1608 1608 ./services/web-apps/nostr-rs-relay.nix 1609 1609 ./services/web-apps/ocis.nix 1610 1610 ./services/web-apps/olivetin.nix 1611 + ./services/web-apps/oncall.nix 1611 1612 ./services/web-apps/onlyoffice.nix 1612 1613 ./services/web-apps/open-web-calendar.nix 1613 1614 ./services/web-apps/openvscode-server.nix
+203
nixos/modules/services/web-apps/oncall.nix
··· 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 + }
+1
nixos/tests/all-tests.nix
··· 618 618 odoo = handleTest ./odoo.nix { }; 619 619 odoo17 = handleTest ./odoo.nix { package = pkgs.odoo17; }; 620 620 odoo16 = handleTest ./odoo.nix { package = pkgs.odoo16; }; 621 + oncall = runTest ./web-apps/oncall.nix; 621 622 # 9pnet_virtio used to mount /nix partition doesn't support 622 623 # hibernation. This test happens to work on x86_64-linux but 623 624 # not on other platforms.
+156
nixos/tests/web-apps/oncall.nix
··· 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 + mail: test@example.org 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": "test@example.com", "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 + }
+128
pkgs/by-name/on/oncall/package.nix
··· 1 + { 2 + lib, 3 + python3, 4 + fetchFromGitHub, 5 + fetchPypi, 6 + oncall, 7 + nixosTests, 8 + 9 + # Override Python packages using 10 + # self: super: { pkg = super.pkg.overridePythonAttrs (oldAttrs: { ... }); } 11 + # Applied after defaultOverrides 12 + packageOverrides ? self: super: { }, 13 + }: 14 + let 15 + defaultOverrides = [ 16 + # Override the version of some packages pinned in Oncall's setup.py 17 + (self: super: { 18 + # Support for Falcon 4.X missing 19 + # https://github.com/linkedin/oncall/issues/430 20 + falcon = super.falcon.overridePythonAttrs (oldAttrs: rec { 21 + version = "3.1.3"; 22 + src = fetchFromGitHub { 23 + owner = "falconry"; 24 + repo = "falcon"; 25 + tag = version; 26 + hash = "sha256-7719gOM8WQVjODwOSo7HpH3HMFFeCGQQYBKktBAevig="; 27 + }; 28 + }); 29 + }) 30 + ]; 31 + 32 + python = python3.override { 33 + self = python; 34 + packageOverrides = lib.composeManyExtensions (defaultOverrides ++ [ packageOverrides ]); 35 + }; 36 + in 37 + python.pkgs.buildPythonApplication rec { 38 + pname = "oncall"; 39 + version = "2.1.7"; 40 + format = "setuptools"; 41 + 42 + src = fetchFromGitHub { 43 + owner = "linkedin"; 44 + repo = pname; 45 + tag = "v${version}"; 46 + hash = "sha256-oqzU4UTpmAcZhqRilquxWQVyHv8bqq0AGraiSqwauiI="; 47 + }; 48 + 49 + patches = [ 50 + # Add support for loading extra settings file 51 + ./support_extra_config.patch 52 + 53 + # Support storing assets in custom state dir 54 + ./support_custom_state_dir.patch 55 + 56 + # Log Python errors to uwsgi 57 + ./verbose_logging.patch 58 + ]; 59 + 60 + dependencies = with python.pkgs; [ 61 + beaker 62 + falcon 63 + falcon-cors 64 + gevent 65 + gunicorn 66 + icalendar 67 + irisclient 68 + jinja2 69 + phonenumbers 70 + pymysql 71 + python-ldap 72 + pytz 73 + pyyaml 74 + ujson 75 + webassets 76 + ]; 77 + 78 + postInstall = '' 79 + mkdir "$out/share" 80 + cp -r configs db "$out/share/" 81 + ''; 82 + 83 + checkInputs = with python.pkgs; [ 84 + pytestCheckHook 85 + pytest-mock 86 + ]; 87 + 88 + disabledTestPaths = [ 89 + # Tests require running web server 90 + "e2e/test_audit.py" 91 + "e2e/test_events.py" 92 + "e2e/test_ical.py" 93 + "e2e/test_login.py" 94 + "e2e/test_notification.py" 95 + "e2e/test_override.py" 96 + "e2e/test_pin.py" 97 + "e2e/test_populate.py" 98 + "e2e/test_roles.py" 99 + "e2e/test_roster_suggest.py" 100 + "e2e/test_rosters.py" 101 + "e2e/test_schedules.py" 102 + "e2e/test_services.py" 103 + "e2e/test_subscription.py" 104 + "e2e/test_teams.py" 105 + "e2e/test_users.py" 106 + ]; 107 + 108 + pythonImportsCheck = [ 109 + "oncall" 110 + ]; 111 + 112 + passthru = { 113 + tests = { 114 + inherit (nixosTests) oncall; 115 + }; 116 + inherit python; 117 + pythonPath = "${python.pkgs.makePythonPath dependencies}:${oncall}/${python.sitePackages}"; 118 + }; 119 + 120 + meta = { 121 + description = "A calendar web-app designed for scheduling and managing on-call shifts"; 122 + homepage = "http://oncall.tools"; 123 + changelog = "https://github.com/linkedin/oncall/blob/${src.tag}/CHANGELOG.md"; 124 + license = lib.licenses.bsd2; 125 + maintainers = with lib.maintainers; [ onny ]; 126 + mainProgram = "oncall"; 127 + }; 128 + }
+56
pkgs/by-name/on/oncall/support_custom_state_dir.patch
··· 1 + diff --git a/src/oncall/ui/__init__.py b/src/oncall/ui/__init__.py 2 + index a94fb17..364404a 100644 3 + --- a/src/oncall/ui/__init__.py 4 + +++ b/src/oncall/ui/__init__.py 5 + @@ -18,8 +18,12 @@ from webassets.ext.jinja2 import AssetsExtension 6 + from webassets.script import CommandLineEnvironment 7 + 8 + STATIC_ROOT = environ.get('STATIC_ROOT', path.abspath(path.dirname(__file__))) 9 + +SOURCE_ROOT = path.abspath(path.dirname(__file__)) 10 + assets_env = AssetsEnvironment(path.join(STATIC_ROOT, 'static'), 11 + url='/static') 12 + +assets_env.cache = False 13 + +assets_env.manifest = False 14 + +assets_env.load_path = [ path.join(SOURCE_ROOT, 'static') ] 15 + 16 + assets_env.register('libs', Bundle( 17 + 'js/jquery-3.3.1.min.js', 'js/handlebars-4.0.12.min.js', 'js/bootstrap.min.js', 18 + @@ -45,7 +49,7 @@ logger = logging.getLogger('webassets') 19 + logger.addHandler(logging.StreamHandler()) 20 + 21 + jinja2_env = Jinja2Environment(extensions=[AssetsExtension], autoescape=True) 22 + -jinja2_env.loader = FileSystemLoader(path.join(STATIC_ROOT, 'templates')) 23 + +jinja2_env.loader = FileSystemLoader(path.join(SOURCE_ROOT, 'templates')) 24 + jinja2_env.assets_environment = assets_env 25 + 26 + _filename_ascii_strip_re = re.compile(r'[^A-Za-z0-9_.-]') 27 + @@ -113,14 +117,15 @@ def secure_filename(filename): 28 + class StaticResource(object): 29 + allow_no_auth = True 30 + 31 + - def __init__(self, path): 32 + + def __init__(self, path, root): 33 + self.path = path.lstrip('/') 34 + + self.root = root 35 + 36 + def on_get(self, req, resp, filename): 37 + suffix = path.splitext(req.path)[1] 38 + resp.content_type = mimes.get(suffix, 'application/octet-stream') 39 + 40 + - filepath = path.join(STATIC_ROOT, self.path, secure_filename(filename)) 41 + + filepath = path.join(self.root, self.path, secure_filename(filename)) 42 + try: 43 + resp.stream = open(filepath, 'rb') 44 + resp.content_length = path.getsize(filepath) 45 + @@ -153,8 +158,8 @@ def init(application, config): 46 + 47 + application.add_sink(index, '/') 48 + application.add_route('/static/bundles/{filename}', 49 + - StaticResource('/static/bundles')) 50 + + StaticResource('/static/bundles', STATIC_ROOT)) 51 + application.add_route('/static/images/{filename}', 52 + - StaticResource('/static/images')) 53 + + StaticResource('/static/images', SOURCE_ROOT)) 54 + application.add_route('/static/fonts/{filename}', 55 + - StaticResource('/static/fonts')) 56 + + StaticResource('/static/fonts', SOURCE_ROOT))
+120
pkgs/by-name/on/oncall/support_extra_config.patch
··· 1 + diff --git a/src/oncall/bin/notifier.py b/src/oncall/bin/notifier.py 2 + index 25142b8..cbc92aa 100644 3 + --- a/src/oncall/bin/notifier.py 4 + +++ b/src/oncall/bin/notifier.py 5 + @@ -32,11 +32,29 @@ send_queue = queue.Queue() 6 + 7 + default_timezone = None 8 + 9 + +def merge_dict(extend_me, extend_by): 10 + + if isinstance(extend_by, dict): 11 + + for k, v in extend_by.items(): 12 + + if isinstance(v, dict) and isinstance(extend_me.get(k), dict): 13 + + merge_dict(extend_me[k], v) 14 + + else: 15 + + extend_me[k] = v 16 + + return extend_me 17 + 18 + def load_config_file(config_path): 19 + with open(config_path, 'r', encoding='utf-8') as h: 20 + config = yaml.safe_load(h) 21 + 22 + + # Check for extra config files from environment variable 23 + + extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') 24 + + if extra_config_paths: 25 + + for extra_path in extra_config_paths.split(','): 26 + + extra_path = extra_path.strip() 27 + + if os.path.isfile(extra_path): 28 + + with open(extra_path, 'r') as f: 29 + + extra_config = yaml.safe_load(f) or {} 30 + + config = merge_dict(config, extra_config) 31 + + 32 + if 'init_config_hook' in config: 33 + try: 34 + module = config['init_config_hook'] 35 + diff --git a/src/oncall/user_sync/ldap_sync.py b/src/oncall/user_sync/ldap_sync.py 36 + index ef9a8ec..c5f027d 100644 37 + --- a/src/oncall/user_sync/ldap_sync.py 38 + +++ b/src/oncall/user_sync/ldap_sync.py 39 + @@ -6,6 +6,7 @@ import time 40 + import yaml 41 + import logging 42 + import ldap 43 + +import os 44 + 45 + from oncall import metrics 46 + from ldap.controls import SimplePagedResultsControl 47 + @@ -447,9 +448,28 @@ def main(config): 48 + logger.info('Sleeping for %s seconds' % sleep_time) 49 + sleep(sleep_time) 50 + 51 + +def merge_dict(extend_me, extend_by): 52 + + if isinstance(extend_by, dict): 53 + + for k, v in extend_by.items(): 54 + + if isinstance(v, dict) and isinstance(extend_me.get(k), dict): 55 + + merge_dict(extend_me[k], v) 56 + + else: 57 + + extend_me[k] = v 58 + + return extend_me 59 + 60 + if __name__ == '__main__': 61 + config_path = sys.argv[1] 62 + with open(config_path, 'r', encoding='utf-8') as config_file: 63 + config = yaml.safe_load(config_file) 64 + + 65 + + # Check for extra config files from environment variable 66 + + extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') 67 + + if extra_config_paths: 68 + + for extra_path in extra_config_paths.split(','): 69 + + extra_path = extra_path.strip() 70 + + if os.path.isfile(extra_path): 71 + + with open(extra_path, 'r') as f: 72 + + extra_config = yaml.safe_load(f) or {} 73 + + config = merge_dict(config, extra_config) 74 + + 75 + main(config) 76 + diff --git a/src/oncall/utils.py b/src/oncall/utils.py 77 + index a0b695c..278ca1d 100644 78 + --- a/src/oncall/utils.py 79 + +++ b/src/oncall/utils.py 80 + @@ -13,6 +13,7 @@ from pytz import timezone 81 + from .constants import ONCALL_REMINDER 82 + from . import constants 83 + import re 84 + +import os 85 + 86 + invalid_char_reg = re.compile(r'[!"#%-,\.\/;->@\[-\^`\{-~]+') 87 + DAY = 86400 88 + @@ -27,10 +28,31 @@ def insert_notification(x, y): 89 + def update_notification(x, y): 90 + pass 91 + 92 + +def merge_dict(extend_me, extend_by): 93 + + if isinstance(extend_by, dict): 94 + + for k, v in extend_by.items(): 95 + + if isinstance(v, dict) and isinstance(extend_me.get(k), dict): 96 + + merge_dict(extend_me[k], v) 97 + + else: 98 + + extend_me[k] = v 99 + + return extend_me 100 + 101 + def read_config(config_path): 102 + + 103 + with open(config_path, 'r', encoding='utf8') as config_file: 104 + - return yaml.safe_load(config_file) 105 + + config = yaml.safe_load(config_file) 106 + + 107 + + # Check for extra config files from environment variable 108 + + extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG') 109 + + if extra_config_paths: 110 + + for extra_path in extra_config_paths.split(','): 111 + + extra_path = extra_path.strip() 112 + + if os.path.isfile(extra_path): 113 + + with open(extra_path, 'r') as f: 114 + + extra_config = yaml.safe_load(f) or {} 115 + + config = merge_dict(config, extra_config) 116 + + 117 + + return config 118 + 119 + 120 + def create_notification(context, team_id, role_ids, type_name, users_involved, cursor, **kwargs):
+33
pkgs/by-name/on/oncall/verbose_logging.patch
··· 1 + diff --git a/src/oncall/app.py b/src/oncall/app.py 2 + index 370fcf4..59f014e 100644 3 + --- a/src/oncall/app.py 4 + +++ b/src/oncall/app.py 5 + @@ -62,9 +62,19 @@ class AuthMiddleware(object): 6 + 7 + application = None 8 + 9 + +def handle_uncaught_exception(req, resp, ex, params): 10 + + logging.exception('Unhandled error') 11 + + raise falcon.HTTPInternalServerError(title='App error') 12 + + 13 + + 14 + +def handle_http_error(req, resp, ex, params): 15 + + logging.exception('HTTP error') 16 + + raise ex 17 + + 18 + 19 + def init_falcon_api(config): 20 + global application 21 + + 22 + cors = CORS(allow_origins_list=config.get('allow_origins_list', [])) 23 + middlewares = [ 24 + SecurityHeaderMiddleware(), 25 + @@ -74,6 +84,8 @@ def init_falcon_api(config): 26 + if config.get('require_auth'): 27 + middlewares.append(AuthMiddleware()) 28 + application = falcon.App(middleware=middlewares) 29 + + application.add_error_handler(falcon.HTTPError, handle_http_error) 30 + + application.add_error_handler(Exception, handle_uncaught_exception) 31 + application.req_options.auto_parse_form_urlencoded = False 32 + application.set_error_serializer(json_error_serializer) 33 + application.req_options.strip_url_path_trailing_slash = True
+36
pkgs/development/python-modules/falcon-cors/default.nix
··· 1 + { 2 + lib, 3 + buildPythonPackage, 4 + fetchFromGitHub, 5 + setuptools, 6 + falcon, 7 + }: 8 + 9 + buildPythonPackage rec { 10 + pname = "falcon-cors"; 11 + version = "1.1.7"; 12 + 13 + src = fetchFromGitHub { 14 + owner = "lwcolton"; 15 + repo = "falcon-cors"; 16 + tag = version; 17 + hash = "sha256-jlEWP7gXbWfdY4coEIM6NWuBf4LOGbUAFMNvqip/FcA="; 18 + }; 19 + 20 + build-system = [ setuptools ]; 21 + 22 + dependencies = [ falcon ]; 23 + 24 + # Test fail with falcon >= 4 25 + # https://github.com/lwcolton/falcon-cors/issues/25 26 + doCheck = false; 27 + 28 + pythonImportsCheck = [ "falcon_cors" ]; 29 + 30 + meta = { 31 + description = "CORS support for Falcon"; 32 + homepage = "https://github.com/lwcolton/falcon-cors"; 33 + license = lib.licenses.asl20; 34 + maintainers = with lib.maintainers; [ onny ]; 35 + }; 36 + }
+43
pkgs/development/python-modules/irisclient/default.nix
··· 1 + { 2 + lib, 3 + buildPythonPackage, 4 + setuptools, 5 + fetchFromGitHub, 6 + requests, 7 + pytestCheckHook, 8 + httmock, 9 + pytest-mock, 10 + }: 11 + 12 + buildPythonPackage rec { 13 + pname = "irisclient"; 14 + version = "1.2.0"; 15 + pyproject = true; 16 + 17 + src = fetchFromGitHub { 18 + owner = "houqp"; 19 + repo = "iris-python-client"; 20 + tag = "v${version}"; 21 + hash = "sha256-fXMw2BopkEqjklR6jr7QQIZyxLq6NHKm2rHwTCbtxR0="; 22 + }; 23 + 24 + build-system = [ setuptools ]; 25 + 26 + dependencies = [ requests ]; 27 + 28 + checkInputs = [ 29 + httmock 30 + pytestCheckHook 31 + pytest-mock 32 + ]; 33 + 34 + pythonImportsCheck = [ "irisclient" ]; 35 + 36 + meta = { 37 + description = "Python client for Iris REST api"; 38 + changelog = "https://github.com/houqp/iris-python-client/blob/v${src.tag}/HISTORY.rst"; 39 + homepage = "https://github.com/houqp/iris-python-client"; 40 + license = lib.licenses.bsd2; 41 + maintainers = with lib.maintainers; [ onny ]; 42 + }; 43 + }
+4
pkgs/top-level/python-packages.nix
··· 4757 4757 4758 4758 falcon = callPackage ../development/python-modules/falcon { }; 4759 4759 4760 + falcon-cors = callPackage ../development/python-modules/falcon-cors { }; 4761 + 4760 4762 falconpy = callPackage ../development/python-modules/falconpy { }; 4761 4763 4762 4764 faraday-agent-parameters-types = ··· 6908 6910 ircstates = callPackage ../development/python-modules/ircstates { }; 6909 6911 6910 6912 irctokens = callPackage ../development/python-modules/irctokens { }; 6913 + 6914 + irisclient = callPackage ../development/python-modules/irisclient { }; 6911 6915 6912 6916 isal = callPackage ../development/python-modules/isal { }; 6913 6917