Loading pkgs/by-name/on/oncall/package.nix 0 → 100644 +128 −0 Original line number Diff line number Diff line { 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"; }; } pkgs/by-name/on/oncall/support_custom_state_dir.patch 0 → 100644 +56 −0 Original line number Diff line number Diff line 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)) pkgs/by-name/on/oncall/support_extra_config.patch 0 → 100644 +120 −0 Original line number Diff line number Diff line 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): pkgs/by-name/on/oncall/verbose_logging.patch 0 → 100644 +33 −0 Original line number Diff line number Diff line 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 Loading
pkgs/by-name/on/oncall/package.nix 0 → 100644 +128 −0 Original line number Diff line number Diff line { 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"; }; }
pkgs/by-name/on/oncall/support_custom_state_dir.patch 0 → 100644 +56 −0 Original line number Diff line number Diff line 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))
pkgs/by-name/on/oncall/support_extra_config.patch 0 → 100644 +120 −0 Original line number Diff line number Diff line 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):
pkgs/by-name/on/oncall/verbose_logging.patch 0 → 100644 +33 −0 Original line number Diff line number Diff line 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