Commit 4a57097a authored by Jonas Heinrich's avatar Jonas Heinrich
Browse files

oncall: init at 2.1.7

parent 9bebe64e
Loading
Loading
Loading
Loading
+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";
  };
}
+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))
+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):
+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