Skip to content

Commit

Permalink
Rework API and add Dummy drivers (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
maretskiy authored and boris-42 committed Dec 26, 2016
1 parent 22986b8 commit ed48a1c
Show file tree
Hide file tree
Showing 18 changed files with 566 additions and 225 deletions.
31 changes: 21 additions & 10 deletions etc/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@
},
"notify_backends": {
"sf": {
"salesforce": {
"atuh_url": "https://somedomain.my.salesforce.com",
"username": "babalbalba",
"password": "abablabal",
"environment": "what?",
"client_id": "...",
"client_secret": "...",
"organization_id": "...."
}
}
"sfdc": {
"atuh_url": "https://somedomain.my.salesforce.com",
"username": "foo_username",
"password": "foo_pa55w0rd",
"organization_id": "mycorp",
"environment": "fooenv",
"client_id": "",
"client_secret": ""
}
},
"dummy": {
"dummy_pass": {},
"dummy_fail": {}
},
"dummyrand": {
"dummy_random": {"probability": 0.5}
},
"dummyerr": {
"dummy_err": {},
"dummy_err_explained": {}
}
}
}
162 changes: 63 additions & 99 deletions notify/api/v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.

import json
import hashlib
import logging

import flask
import jsonschema

from notify import config
from notify.drivers import driver
from notify import driver


LOG = logging.getLogger("api")
Expand All @@ -30,110 +29,75 @@
bp = flask.Blueprint("notify", __name__)


PAYLOAD_SCHEMA = {
"type": "object",
"$schema": "http://json-schema.org/draft-04/schema",

"properties": {
"region": {
"type": "string"
},
"description": {
"type": "string"
},
"severity": {
"enum": ["OK", "INFO", "UNKNOWN", "WARNING", "CRITICAL", "DOWN"]
},
"who": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": True
},
"what": {
"type": "string"
}
},
"required": ["description", "region", "severity", "who", "what"],
"additionalProperties": False
}


# NOTE(boris-42): Use here pool of resources
CACHE = {}


def make_hash(dct):
"""Generate MD5 hash of given dict.
:param dct: dict to hash. There may be collisions
if it isn't flat (includes sub-dicts)
:returns: str MD5 hexdigest (32 chars)
"""
str_repr = "|".join(["%s:%s" % x for x in sorted(dct.items())])
return hashlib.md5(str_repr.encode("utf-8")).hexdigest()


@bp.route("/notify/<backends>", methods=["POST"])
def send_notification(backends):
global CACHE

backends = set(backends.split(","))
payload = flask.request.get_json(force=True, silent=True)

if not payload:
return flask.jsonify({"error": "Missed Payload"}), 400

try:
content = json.loads(flask.request.form['payload'])
jsonschema.validate(content, PAYLOAD_SCHEMA)
except Exception as e:
return flask.jsonify({
"errors": True,
"status": 400,
"description": "Wrong payload: %s" % str(e),
"results": []
}), 400

conf = config.get_config()
resp = {
"errors": False,
"status": 200,
"description": "",
"results": []
}

for backend in backends.split(","):
result = {
"backend": backend,
"status": 200,
"errors": False,
"description": "",
"results": []
}
if backend in conf["notify_backends"]:
for dr, driver_cfg in conf["notify_backends"][backend].items():
r = {
"backend": backend,
"driver": dr,
"error": False,
"status": 200
}
try:

driver_key = "%s.%s" % (backend, dr)
if driver_key not in CACHE:
# NOTE(boris-42): We should use here pool with locks
CACHE[driver_key] = driver.get_driver(dr)(driver_cfg)

# NOTE(boris-42): It would be smarter to call all drivers
# notify in parallel
CACHE[driver_key].notify(content)

except Exception as e:
print(e)
r["status"] = 500
r["error"] = True
resp["errors"] = True
result["errors"] = True
r["description"] = ("Something went wrong %s.%s"
% (backend, dr))

result["results"].append(r)
else:
result["status"] = 404
result["errors"] = True
resp["errors"] = True
result["description"] = "Backend %s not found" % backend

resp["results"].append(result)

return flask.jsonify(resp), resp["status"]
driver.Driver.validate_payload(payload)
except ValueError as e:
return flask.jsonify({"error": "Bad Payload: {}".format(e)}), 400

notify_backends = config.get_config()["notify_backends"]

unexpected = backends - set(notify_backends)
if unexpected:
mesg = "Unexpected backends: {}".format(", ".join(unexpected))
return flask.jsonify({"error": mesg}), 400

result = {"payload": payload, "result": {},
"total": 0, "passed": 0, "failed": 0, "errors": 0}

for backend in backends:
for drv_name, drv_conf in notify_backends[backend].items():

key = "{}.{}".format(drv_name, make_hash(drv_conf))
if key not in CACHE:
CACHE[key] = driver.get_driver(drv_name, drv_conf)
driver_ins = CACHE[key]

result["total"] += 1
if backend not in result["result"]:
result["result"][backend] = {}

# TODO(maretskiy): run in parallel
try:
status = driver_ins.notify(payload)

result["passed"] += status
result["failed"] += not status
result["result"][backend][drv_name] = {"status": status}
except driver.ExplainedError as e:
result["result"][backend][drv_name] = {"error": str(e)}
result["errors"] += 1
except Exception as e:
LOG.error(("Backend '{}' driver '{}' "
"error: {}").format(backend, drv_name, e))
error = "Something has went wrong!"
result["result"][backend][drv_name] = {"error": error}
result["errors"] += 1

return flask.jsonify(result), 200


def get_blueprints():
Expand Down
124 changes: 124 additions & 0 deletions notify/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright 2016: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import importlib
import logging

import jsonschema


DRIVERS = {}


def get_driver(name, conf):
"""Get driver by name.
:param name: driver name
:param conf: dict, driver configuration
:rtype: Driver
:raises: RuntimeError
"""
global DRIVERS
if name not in DRIVERS:
try:
module = importlib.import_module("notify.drivers." + name)
DRIVERS[name] = module.Driver
except (ImportError, AttributeError):
mesg = "Unexpected driver: '{}'".format(name)
logging.error(mesg)
raise RuntimeError(mesg)

driver_cls = DRIVERS[name]

try:
driver_cls.validate_config(conf)
except ValueError as e:
mesg = "Bad configuration for driver '{}'".format(name)
logging.error("{}: {}".format(mesg, e))
raise RuntimeError(mesg)

return driver_cls(conf)


class ExplainedError(Exception):
"""Error that should be delivered to end user."""


class Driver(object):
"""Base for notification drivers."""

PAYLOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema",
"type": "object",
"properties": {
"region": {"type": "string"},
"description": {"type": "string"},
"severity": {
"enum": ["OK", "INFO", "UNKNOWN", "WARNING",
"CRITICAL", "DOWN"]},
"who": {"type": "array",
"items": {"type": "string"},
"minItems": 1,
"uniqueItems": True},
"what": {"type": "string"},
"affected_hosts": {"type": "array"}
},
"required": ["region", "description", "severity", "who", "what"],
"additionalProperties": False
}

CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema",
"type": "object"
}

@classmethod
def validate_payload(cls, payload):
"""Payload validation.
:param payload: notification payload
:raises: ValueError
"""
try:
jsonschema.validate(payload, cls.PAYLOAD_SCHEMA)
except jsonschema.exceptions.ValidationError as e:
raise ValueError(str(e))

@classmethod
def validate_config(cls, conf):
"""Driver configuration validation.
:param conf: driver configuration
:raises: ValueError
"""
try:
jsonschema.validate(conf, cls.CONFIG_SCHEMA)
except jsonschema.exceptions.ValidationError as e:
raise ValueError(str(e))

def __init__(self, config):
self.config = config

def notify(self, payload):
"""Send notification alert.
This method must be overriden by specific driver implementation.
:param payload: alert data
:type payload: dict, validated api.PAYLOAD_SCHEMA
:returns: status whether notification is successful
:rtype: bool
"""
raise NotImplementedError()
23 changes: 5 additions & 18 deletions notify/drivers/driver.py → notify/drivers/dummy_err.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Copyright 2016: Mirantis Inc.
# All Rights Reserved.
#
Expand All @@ -14,23 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.

import importlib
import logging


def get_driver(driver_type):
try:
return importlib.import_module("." + driver_type + ".driver",
"notify.drivers").Driver
except ImportError:
logging.error("Could not load driver for '{}'".format(driver_type))
raise

from notify import driver

class Driver(object):

def __init__(self, config):
self.config = config
class Driver(driver.Driver):
"""Simulate unexpected error by raising ValueError."""

def notify(self):
raise NotImplemented()
def notify(self, payload):
raise ValueError("This error message is for logging only!")
Loading

0 comments on commit ed48a1c

Please sign in to comment.