oncall: init at 2.1.7; nixos/oncall: init (#388723)
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ];
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 '<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}"'
|
||||
""")
|
||||
|
||||
'';
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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):
|
||||
@@ -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
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -4757,6 +4757,8 @@ 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 =
|
||||
@@ -6909,6 +6911,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 { };
|
||||
|
||||
Reference in New Issue
Block a user