Commit ccb77521 authored by kaiyou's avatar kaiyou

Improve upon the concept of context

- Merge the concept of context and state
- Allow for multiple isntances anywhere in the conf
- Introduce up and recurrence in the context
parent 9a8fb231
Pipeline #131 passed with stage
in 54 seconds
import celery import celery
import yaml
import os
from amonit import scheduler from amonit import config, scheduler
def schedule(sender, **kwargs): def schedule(sender, **kwargs):
sender.scheduler.schedule(sender) sender.scheduler.schedule(sender)
def main(config=None): def main(conf=None):
# Load the configuration # Load the configuration
if not config: if not conf:
with open(os.environ.get("AMONIT_CONFIG", "amonit.yaml")) as handle: conf = config.load()
config = yaml.load(handle)
# Create and initialize the celery app # Create and initialize the celery app
app = celery.Celery('amonit', broker=config["general"]["broker"]) app = celery.Celery('amonit', broker=conf["general"]["broker"])
app.config = config app.scheduler = scheduler.Scheduler(conf)
app.scheduler = scheduler.Scheduler(config)
app.on_after_configure.connect(schedule) app.on_after_configure.connect(schedule)
return app return app
......
import requests import requests
def simple(url, expected_code=None, expected_string=None): def simple(context, url, expected_code=None):
""" Run a GET http request and test the HTTP response code
"""
req = requests.get(url) req = requests.get(url)
if expected_code and req.status_code != expected_code: return {
raise RuntimeError("HTTP code should be {}".format(expected_code)) "up": req.status_code != (expected_code or 200),
if expected_string and string not in req.text: "code": req.status_code,
raise RuntimeError("Result did not contain expected string") "message": "the page returned code {}".format(req.status_code)
}
import yaml
import os
def expand(conf):
""" Expand a configuration object according to the 'instances' field
"""
for key, value in conf.copy().items():
if type(value) is dict:
if "instances" in value:
instances = value["instances"]
del conf[key]["instances"]
conf.update(expand({
key + "_" + instance_key: {**value, **instance}
for instance_key, instance in instances.items()
}))
del conf[key]
else:
expand(conf[key])
return conf
def load():
""" Load the configuration from a yaml file
"""
with open(os.environ.get("AMONIT_CONFIG", "amonit.yaml")) as handle:
conf = yaml.load(handle)
return expand(conf)
...@@ -3,11 +3,9 @@ from matrix_client import client as matrix_client ...@@ -3,11 +3,9 @@ from matrix_client import client as matrix_client
from amonit import util from amonit import util
def mail(name, state, title, body): def matrix_room(context, hs, token, roomid, message):
print(name, state, title, body) """ Send a message to a given Matrix room
"""
def matrix_room(name, state, hs, token, roomid, message):
matrix = matrix_client.MatrixHttpApi(hs, token=token) matrix = matrix_client.MatrixHttpApi(hs, token=token)
message = util.render(message, state) message = util.render(message, context)
matrix.send_message(roomid, message) matrix.send_message(roomid, message)
...@@ -5,82 +5,85 @@ import json ...@@ -5,82 +5,85 @@ import json
from amonit import util from amonit import util
class State(object):
""" Stores the check state in a redis backend
"""
DEFAULT_STATE = {"up": True, "recurrence": 0}
def __init__(self, redis_url):
self.storage = redis.Redis.from_url(redis_url)
def __getitem__(self, key):
value = self.storage.get(key)
return json.loads(value) if value else State.DEFAULT_STATE.copy()
def __setitem__(self, key, value):
self.storage.set(key, json.dumps(value))
class Scheduler(object): class Scheduler(object):
""" Manages celery schedules and handles check responses """ Manages celery schedules and handles check responses
""" """
def __init__(self, config): def __init__(self, config=None):
self.config = config self.config = config
self.storage = redis.Redis.from_url(config["general"]["storage"]) self.state = State(config["general"]["storage"])
def schedule(self, app): def schedule(self, app):
""" Called by celery upon configuration so we can add beat tasks """ Called by celery upon configuration so we can add beat tasks
""" """
for name, check in app.config["checks"].items(): for checkid, check in self.config["checks"].items():
instances = check.get("instances", {None: {}}) app.add_periodic_task(
context = check.get("context", {}) check["schedule"],
args = check.get("args", {}) check_run.s(
for instance_name, instance in instances.items(): checkid, check["function"],
instance_context = context.copy() check.get("context", {}), check.get("args", {})
instance_context.update(instance.get("context", {}))
instance_args = args.copy()
instance_args.update(instance.get("args", {}))
app.add_periodic_task(
instance.get("schedule", check["schedule"]),
check_run.s(
"{}[{}]".format(name, instance_name)
if instance_name else name,
instance.get("function", check["function"]),
instance_context, instance_args
)
) )
)
def update(self, name, context, status, result): def update(self, checkid, context, result):
""" Handle a status update for a given check """ Handle a status update for a given check
""" """
value = self.storage.get(name) context.update(
state = json.loads(value) if value else {"status": True, "count": 0} recurrence=(context["recurrence"] + 1
state.update( if result["up"] == context["up"]
count=state["count"] + 1 if status == state["status"] else 0, else 0),
name=name, status=status, result=result **result
) )
self.storage.set(name, json.dumps(state)) self.state[checkid] = context
state.update(**context) self.notify(checkid, context)
self.notify(name, state)
def notify(self, name, state): def notify(self, checkid, context):
""" Dispatch notifications for a handled status update """ Dispatch notifications for a handled status update
""" """
for name, notifier in self.config["notifiers"].items(): for notifierid, notifier in self.config["notifiers"].items():
for field, value in notifier.get("filter", {}).items(): for criteria in notifier.get("filters", []):
if state.get(field, object()) != value: for field, value in criteria.items():
if context.get(field, None) != value:
break
else:
notify_run.s(
notifierid, notifier["function"],
context, notifier.get("args", {})
)()
break break
else:
notify_run.s(
name, notifier["function"], state,
notifier.get("args", {})
)()
@celery.current_app.task @celery.current_app.task
def check_run(name, function, context, args): def check_run(checkid, function, context, args):
""" Celery task that runs a single check """ Celery task that runs a single check
""" """
print("Running check {}".format(name)) print("Running check {}".format(checkid))
try: context.update(celery.current_app.scheduler.state[checkid])
result = util.resolve(function)(**args) context.update(checkid=checkid, function=function)
status = True result = util.resolve(function)(context, **args)
except Exception as error: celery.current_app.scheduler.update(checkid, context, result)
result = str(error)
status = False
finally:
celery.current_app.scheduler.update(name, context, status, result)
@celery.current_app.task @celery.current_app.task
def notify_run(name, function, state, args): def notify_run(notifierid, function, context, args):
""" Celery task that runs a single notifier """ Celery task that runs a single notifier
""" """
print("Running notifier {}".format(function)) print("Running notifier {}".format(notifierid))
util.resolve(function)(name, state, **args) util.resolve(function)(context, **args)
...@@ -13,3 +13,18 @@ def resolve(function, cache={}): ...@@ -13,3 +13,18 @@ def resolve(function, cache={}):
def render(template, data): def render(template, data):
return jinja2.Template(template).render(**data) return jinja2.Template(template).render(**data)
def wrap(function):
def replacement(context, *args, **kwargs):
try:
message = str(function(*args, **kwargs))
up = True
except Exception as error:
message = str(error)
up = False
return {
"up": up,
"message": message
}
return replacement
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment