|
|
@ -3,32 +3,39 @@ from threading import Thread |
|
|
|
|
|
|
|
from django.core.management.base import BaseCommand |
|
|
|
from django.utils import timezone |
|
|
|
from hc.api.models import Check |
|
|
|
from hc.api.models import Check, Flip |
|
|
|
|
|
|
|
|
|
|
|
def notify(check_id, stdout): |
|
|
|
check = Check.objects.get(id=check_id) |
|
|
|
def notify(flip_id, stdout): |
|
|
|
flip = Flip.objects.get(id=flip_id) |
|
|
|
|
|
|
|
check = flip.owner |
|
|
|
# Set the historic status here but *don't save it*. |
|
|
|
# It would be nicer to pass the status explicitly, as a separate parameter. |
|
|
|
check.status = flip.new_status |
|
|
|
# And just to make sure it doesn't get saved by a future coding accident: |
|
|
|
setattr(check, "save", None) |
|
|
|
|
|
|
|
tmpl = "Sending alert, status=%s, code=%s\n" |
|
|
|
stdout.write(tmpl % (check.status, check.code)) |
|
|
|
stdout.write(tmpl % (flip.new_status, check.code)) |
|
|
|
|
|
|
|
# Set dates for followup nags |
|
|
|
if check.status == "down" and check.user.profile: |
|
|
|
if flip.new_status == "down" and check.user.profile: |
|
|
|
check.user.profile.set_next_nag_date() |
|
|
|
|
|
|
|
# Send notifications |
|
|
|
errors = check.send_alert() |
|
|
|
errors = check.send_alert(flip) |
|
|
|
for ch, error in errors: |
|
|
|
stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error)) |
|
|
|
|
|
|
|
|
|
|
|
def notify_on_thread(check_id, stdout): |
|
|
|
t = Thread(target=notify, args=(check_id, stdout)) |
|
|
|
def notify_on_thread(flip_id, stdout): |
|
|
|
t = Thread(target=notify, args=(flip_id, stdout)) |
|
|
|
t.start() |
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand): |
|
|
|
help = 'Sends UP/DOWN email alerts' |
|
|
|
owned = Check.objects.filter(user__isnull=False).order_by("alert_after") |
|
|
|
|
|
|
|
def add_arguments(self, parser): |
|
|
|
parser.add_argument( |
|
|
@ -47,56 +54,73 @@ class Command(BaseCommand): |
|
|
|
help='Send alerts synchronously, without using threads', |
|
|
|
) |
|
|
|
|
|
|
|
def handle_one(self, use_threads=True): |
|
|
|
""" Process a single check. """ |
|
|
|
def process_one_flip(self, use_threads=True): |
|
|
|
""" Find unprocessed flip, send notifications. """ |
|
|
|
|
|
|
|
now = timezone.now() |
|
|
|
# Order by processed, otherwise Django will automatically order by id |
|
|
|
# and make the query less efficient |
|
|
|
q = Flip.objects.filter(processed=None).order_by("processed") |
|
|
|
flip = q.first() |
|
|
|
if flip is None: |
|
|
|
return False |
|
|
|
|
|
|
|
# Look for checks that are going down |
|
|
|
q = self.owned.filter(alert_after__lt=now, status="up") |
|
|
|
check = q.first() |
|
|
|
q = Flip.objects.filter(id=flip.id, processed=None) |
|
|
|
num_updated = q.update(processed=timezone.now()) |
|
|
|
if num_updated != 1: |
|
|
|
# Nothing got updated: another worker process got there first. |
|
|
|
return True |
|
|
|
|
|
|
|
# If none found, look for checks that are going up |
|
|
|
if not check: |
|
|
|
q = self.owned.filter(alert_after__gt=now, status="down") |
|
|
|
check = q.first() |
|
|
|
if use_threads: |
|
|
|
notify_on_thread(flip.id, self.stdout) |
|
|
|
else: |
|
|
|
notify(flip.id, self.stdout) |
|
|
|
|
|
|
|
return True |
|
|
|
|
|
|
|
def handle_going_down(self): |
|
|
|
""" Process a single check going down. """ |
|
|
|
|
|
|
|
now = timezone.now() |
|
|
|
|
|
|
|
check = Check.objects.filter(alert_after__lt=now, status="up").first() |
|
|
|
if check is None: |
|
|
|
return False |
|
|
|
|
|
|
|
q = Check.objects.filter(id=check.id, status=check.status) |
|
|
|
current_status = check.get_status() |
|
|
|
|
|
|
|
# During the grace period sendalerts considers the check as "up": |
|
|
|
if current_status == "grace": |
|
|
|
current_status = "up" |
|
|
|
q = Check.objects.filter(id=check.id, status="up") |
|
|
|
|
|
|
|
if check.status == current_status: |
|
|
|
# Stored status is already up-to-date. Update alert_after |
|
|
|
# as needed but don't send notifications |
|
|
|
current_status = check.get_status() |
|
|
|
if current_status != "down": |
|
|
|
# It is not down yet. Update alert_after |
|
|
|
q.update(alert_after=check.get_alert_after()) |
|
|
|
return True |
|
|
|
else: |
|
|
|
# Atomically update status to the opposite |
|
|
|
num_updated = q.update(status=current_status) |
|
|
|
if num_updated == 1: |
|
|
|
# Send notifications only if status update succeeded |
|
|
|
# (no other sendalerts process got there first) |
|
|
|
if use_threads: |
|
|
|
notify_on_thread(check.id, self.stdout) |
|
|
|
else: |
|
|
|
notify(check.id, self.stdout) |
|
|
|
|
|
|
|
return True |
|
|
|
# Atomically update status |
|
|
|
num_updated = q.update(status="down") |
|
|
|
if num_updated != 1: |
|
|
|
# Nothing got updated: another worker process got there first. |
|
|
|
return True |
|
|
|
|
|
|
|
flip = Flip(owner=check) |
|
|
|
flip.created = check.get_alert_after() |
|
|
|
flip.old_status = "up" |
|
|
|
flip.new_status = "down" |
|
|
|
flip.save() |
|
|
|
|
|
|
|
return False |
|
|
|
check.status = "down" |
|
|
|
check.save() |
|
|
|
return True |
|
|
|
|
|
|
|
def handle(self, use_threads=True, loop=True, *args, **options): |
|
|
|
self.stdout.write("sendalerts is now running\n") |
|
|
|
|
|
|
|
i, sent = 0, 0 |
|
|
|
while True: |
|
|
|
while self.handle_one(use_threads): |
|
|
|
# Create flips for any checks going down |
|
|
|
while self.handle_going_down(): |
|
|
|
pass |
|
|
|
|
|
|
|
# Process the unprocessed flips |
|
|
|
while self.process_one_flip(use_threads): |
|
|
|
sent += 1 |
|
|
|
|
|
|
|
if not loop: |
|
|
|