Skip to content

Commit ed48a1c

Browse files
maretskiyboris-42
authored andcommitted
Rework API and add Dummy drivers (#18)
1 parent 22986b8 commit ed48a1c

File tree

18 files changed

+566
-225
lines changed

18 files changed

+566
-225
lines changed

etc/config.json

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,26 @@
1010
},
1111
"notify_backends": {
1212
"sf": {
13-
"salesforce": {
14-
"atuh_url": "https://somedomain.my.salesforce.com",
15-
"username": "babalbalba",
16-
"password": "abablabal",
17-
"environment": "what?",
18-
"client_id": "...",
19-
"client_secret": "...",
20-
"organization_id": "...."
21-
}
22-
}
13+
"sfdc": {
14+
"atuh_url": "https://somedomain.my.salesforce.com",
15+
"username": "foo_username",
16+
"password": "foo_pa55w0rd",
17+
"organization_id": "mycorp",
18+
"environment": "fooenv",
19+
"client_id": "",
20+
"client_secret": ""
21+
}
22+
},
23+
"dummy": {
24+
"dummy_pass": {},
25+
"dummy_fail": {}
26+
},
27+
"dummyrand": {
28+
"dummy_random": {"probability": 0.5}
29+
},
30+
"dummyerr": {
31+
"dummy_err": {},
32+
"dummy_err_explained": {}
33+
}
2334
}
2435
}

notify/api/v1/api.py

Lines changed: 63 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
# License for the specific language governing permissions and limitations
1414
# under the License.
1515

16-
import json
16+
import hashlib
1717
import logging
1818

1919
import flask
20-
import jsonschema
2120

2221
from notify import config
23-
from notify.drivers import driver
22+
from notify import driver
2423

2524

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

3231

33-
PAYLOAD_SCHEMA = {
34-
"type": "object",
35-
"$schema": "http://json-schema.org/draft-04/schema",
36-
37-
"properties": {
38-
"region": {
39-
"type": "string"
40-
},
41-
"description": {
42-
"type": "string"
43-
},
44-
"severity": {
45-
"enum": ["OK", "INFO", "UNKNOWN", "WARNING", "CRITICAL", "DOWN"]
46-
},
47-
"who": {
48-
"type": "array",
49-
"items": {
50-
"type": "string"
51-
},
52-
"minItems": 1,
53-
"uniqueItems": True
54-
},
55-
"what": {
56-
"type": "string"
57-
}
58-
},
59-
"required": ["description", "region", "severity", "who", "what"],
60-
"additionalProperties": False
61-
}
62-
63-
64-
# NOTE(boris-42): Use here pool of resources
6532
CACHE = {}
6633

6734

35+
def make_hash(dct):
36+
"""Generate MD5 hash of given dict.
37+
38+
:param dct: dict to hash. There may be collisions
39+
if it isn't flat (includes sub-dicts)
40+
:returns: str MD5 hexdigest (32 chars)
41+
"""
42+
str_repr = "|".join(["%s:%s" % x for x in sorted(dct.items())])
43+
return hashlib.md5(str_repr.encode("utf-8")).hexdigest()
44+
45+
6846
@bp.route("/notify/<backends>", methods=["POST"])
6947
def send_notification(backends):
7048
global CACHE
7149

50+
backends = set(backends.split(","))
51+
payload = flask.request.get_json(force=True, silent=True)
52+
53+
if not payload:
54+
return flask.jsonify({"error": "Missed Payload"}), 400
55+
7256
try:
73-
content = json.loads(flask.request.form['payload'])
74-
jsonschema.validate(content, PAYLOAD_SCHEMA)
75-
except Exception as e:
76-
return flask.jsonify({
77-
"errors": True,
78-
"status": 400,
79-
"description": "Wrong payload: %s" % str(e),
80-
"results": []
81-
}), 400
82-
83-
conf = config.get_config()
84-
resp = {
85-
"errors": False,
86-
"status": 200,
87-
"description": "",
88-
"results": []
89-
}
90-
91-
for backend in backends.split(","):
92-
result = {
93-
"backend": backend,
94-
"status": 200,
95-
"errors": False,
96-
"description": "",
97-
"results": []
98-
}
99-
if backend in conf["notify_backends"]:
100-
for dr, driver_cfg in conf["notify_backends"][backend].items():
101-
r = {
102-
"backend": backend,
103-
"driver": dr,
104-
"error": False,
105-
"status": 200
106-
}
107-
try:
108-
109-
driver_key = "%s.%s" % (backend, dr)
110-
if driver_key not in CACHE:
111-
# NOTE(boris-42): We should use here pool with locks
112-
CACHE[driver_key] = driver.get_driver(dr)(driver_cfg)
113-
114-
# NOTE(boris-42): It would be smarter to call all drivers
115-
# notify in parallel
116-
CACHE[driver_key].notify(content)
117-
118-
except Exception as e:
119-
print(e)
120-
r["status"] = 500
121-
r["error"] = True
122-
resp["errors"] = True
123-
result["errors"] = True
124-
r["description"] = ("Something went wrong %s.%s"
125-
% (backend, dr))
126-
127-
result["results"].append(r)
128-
else:
129-
result["status"] = 404
130-
result["errors"] = True
131-
resp["errors"] = True
132-
result["description"] = "Backend %s not found" % backend
133-
134-
resp["results"].append(result)
135-
136-
return flask.jsonify(resp), resp["status"]
57+
driver.Driver.validate_payload(payload)
58+
except ValueError as e:
59+
return flask.jsonify({"error": "Bad Payload: {}".format(e)}), 400
60+
61+
notify_backends = config.get_config()["notify_backends"]
62+
63+
unexpected = backends - set(notify_backends)
64+
if unexpected:
65+
mesg = "Unexpected backends: {}".format(", ".join(unexpected))
66+
return flask.jsonify({"error": mesg}), 400
67+
68+
result = {"payload": payload, "result": {},
69+
"total": 0, "passed": 0, "failed": 0, "errors": 0}
70+
71+
for backend in backends:
72+
for drv_name, drv_conf in notify_backends[backend].items():
73+
74+
key = "{}.{}".format(drv_name, make_hash(drv_conf))
75+
if key not in CACHE:
76+
CACHE[key] = driver.get_driver(drv_name, drv_conf)
77+
driver_ins = CACHE[key]
78+
79+
result["total"] += 1
80+
if backend not in result["result"]:
81+
result["result"][backend] = {}
82+
83+
# TODO(maretskiy): run in parallel
84+
try:
85+
status = driver_ins.notify(payload)
86+
87+
result["passed"] += status
88+
result["failed"] += not status
89+
result["result"][backend][drv_name] = {"status": status}
90+
except driver.ExplainedError as e:
91+
result["result"][backend][drv_name] = {"error": str(e)}
92+
result["errors"] += 1
93+
except Exception as e:
94+
LOG.error(("Backend '{}' driver '{}' "
95+
"error: {}").format(backend, drv_name, e))
96+
error = "Something has went wrong!"
97+
result["result"][backend][drv_name] = {"error": error}
98+
result["errors"] += 1
99+
100+
return flask.jsonify(result), 200
137101

138102

139103
def get_blueprints():

notify/driver.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Copyright 2016: Mirantis Inc.
2+
# All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
import importlib
17+
import logging
18+
19+
import jsonschema
20+
21+
22+
DRIVERS = {}
23+
24+
25+
def get_driver(name, conf):
26+
"""Get driver by name.
27+
28+
:param name: driver name
29+
:param conf: dict, driver configuration
30+
:rtype: Driver
31+
:raises: RuntimeError
32+
"""
33+
global DRIVERS
34+
if name not in DRIVERS:
35+
try:
36+
module = importlib.import_module("notify.drivers." + name)
37+
DRIVERS[name] = module.Driver
38+
except (ImportError, AttributeError):
39+
mesg = "Unexpected driver: '{}'".format(name)
40+
logging.error(mesg)
41+
raise RuntimeError(mesg)
42+
43+
driver_cls = DRIVERS[name]
44+
45+
try:
46+
driver_cls.validate_config(conf)
47+
except ValueError as e:
48+
mesg = "Bad configuration for driver '{}'".format(name)
49+
logging.error("{}: {}".format(mesg, e))
50+
raise RuntimeError(mesg)
51+
52+
return driver_cls(conf)
53+
54+
55+
class ExplainedError(Exception):
56+
"""Error that should be delivered to end user."""
57+
58+
59+
class Driver(object):
60+
"""Base for notification drivers."""
61+
62+
PAYLOAD_SCHEMA = {
63+
"$schema": "http://json-schema.org/draft-04/schema",
64+
"type": "object",
65+
"properties": {
66+
"region": {"type": "string"},
67+
"description": {"type": "string"},
68+
"severity": {
69+
"enum": ["OK", "INFO", "UNKNOWN", "WARNING",
70+
"CRITICAL", "DOWN"]},
71+
"who": {"type": "array",
72+
"items": {"type": "string"},
73+
"minItems": 1,
74+
"uniqueItems": True},
75+
"what": {"type": "string"},
76+
"affected_hosts": {"type": "array"}
77+
},
78+
"required": ["region", "description", "severity", "who", "what"],
79+
"additionalProperties": False
80+
}
81+
82+
CONFIG_SCHEMA = {
83+
"$schema": "http://json-schema.org/draft-04/schema",
84+
"type": "object"
85+
}
86+
87+
@classmethod
88+
def validate_payload(cls, payload):
89+
"""Payload validation.
90+
91+
:param payload: notification payload
92+
:raises: ValueError
93+
"""
94+
try:
95+
jsonschema.validate(payload, cls.PAYLOAD_SCHEMA)
96+
except jsonschema.exceptions.ValidationError as e:
97+
raise ValueError(str(e))
98+
99+
@classmethod
100+
def validate_config(cls, conf):
101+
"""Driver configuration validation.
102+
103+
:param conf: driver configuration
104+
:raises: ValueError
105+
"""
106+
try:
107+
jsonschema.validate(conf, cls.CONFIG_SCHEMA)
108+
except jsonschema.exceptions.ValidationError as e:
109+
raise ValueError(str(e))
110+
111+
def __init__(self, config):
112+
self.config = config
113+
114+
def notify(self, payload):
115+
"""Send notification alert.
116+
117+
This method must be overriden by specific driver implementation.
118+
119+
:param payload: alert data
120+
:type payload: dict, validated api.PAYLOAD_SCHEMA
121+
:returns: status whether notification is successful
122+
:rtype: bool
123+
"""
124+
raise NotImplementedError()
Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Copyright 2016: Mirantis Inc.
32
# All Rights Reserved.
43
#
@@ -14,23 +13,11 @@
1413
# License for the specific language governing permissions and limitations
1514
# under the License.
1615

17-
import importlib
18-
import logging
19-
20-
21-
def get_driver(driver_type):
22-
try:
23-
return importlib.import_module("." + driver_type + ".driver",
24-
"notify.drivers").Driver
25-
except ImportError:
26-
logging.error("Could not load driver for '{}'".format(driver_type))
27-
raise
28-
16+
from notify import driver
2917

30-
class Driver(object):
3118

32-
def __init__(self, config):
33-
self.config = config
19+
class Driver(driver.Driver):
20+
"""Simulate unexpected error by raising ValueError."""
3421

35-
def notify(self):
36-
raise NotImplemented()
22+
def notify(self, payload):
23+
raise ValueError("This error message is for logging only!")

0 commit comments

Comments
 (0)