From fa76fbc8d7a0576eb94ff16cc5f6161e1c8fec68 Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Tue, 22 Jan 2019 14:34:56 +0100 Subject: [PATCH] manage availability check through the UI (#29965) --- .../migrations/0011_auto_20190205_1126.py | 30 +++++++++++ passerelle/base/models.py | 30 +++++++++++ passerelle/base/urls.py | 7 ++- passerelle/base/views.py | 54 ++++++++++++++++++- passerelle/contrib/planitech/models.py | 5 ++ .../manage/manage_availability_form.html | 17 ++++++ .../passerelle/manage/service_view.html | 3 ++ passerelle/views.py | 7 +++ tests/test_availability.py | 20 +++++++ tests/test_manager.py | 53 +++++++++++++++++- 10 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 passerelle/base/migrations/0011_auto_20190205_1126.py create mode 100644 passerelle/templates/passerelle/manage/manage_availability_form.html diff --git a/passerelle/base/migrations/0011_auto_20190205_1126.py b/passerelle/base/migrations/0011_auto_20190205_1126.py new file mode 100644 index 00000000..1a4b006c --- /dev/null +++ b/passerelle/base/migrations/0011_auto_20190205_1126.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-02-05 10:26 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('base', '0010_loggingparameters_trace_emails'), + ] + + operations = [ + migrations.CreateModel( + name='AvailabilityParameters', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('resource_pk', models.PositiveIntegerField()), + ('run_check', models.BooleanField(default=True, verbose_name='Run regular availability checks')), + ('resource_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + migrations.AlterUniqueTogether( + name='availabilityparameters', + unique_together=set([('resource_type', 'resource_pk')]), + ), + ] diff --git a/passerelle/base/models.py b/passerelle/base/models.py index d6921432..6904dcbb 100644 --- a/passerelle/base/models.py +++ b/passerelle/base/models.py @@ -176,6 +176,18 @@ class BaseResource(models.Model): parameters.log_level = value parameters.save() + @property + def availability_parameters(self): + resource_type = ContentType.objects.get_for_model(self) + try: + return AvailabilityParameters.objects.get( + resource_type=resource_type, + resource_pk=self.id) + except AvailabilityParameters.DoesNotExist: + return AvailabilityParameters( + resource_type=resource_type, + resource_pk=self.id) + def soap_client(self, **kwargs): return passerelle.utils.SOAPClient(resource=self, **kwargs) @@ -387,9 +399,15 @@ class BaseResource(models.Model): def check_status(self): # should raise an exception if status is not ok raise NotImplementedError + check_status.not_implemented = True def availability(self): # "availability" cron job to update service statuses + + # eventually skip it + if not self.availability_parameters.run_check: + return + currently_down = self.down() try: self.check_status() @@ -477,6 +495,18 @@ class LoggingParameters(models.Model): unique_together = (('resource_type', 'resource_pk')) +class AvailabilityParameters(models.Model): + resource_type = models.ForeignKey(ContentType) + resource_pk = models.PositiveIntegerField() + resource = fields.GenericForeignKey('resource_type', 'resource_pk') + run_check = models.BooleanField( + default=True, verbose_name=_('Run regular availability checks'), + help_text=_('Run an availability check every 5 minutes')) + + class Meta: + unique_together = (('resource_type', 'resource_pk')) + + class ResourceLog(models.Model): timestamp = models.DateTimeField(auto_now_add=True) diff --git a/passerelle/base/urls.py b/passerelle/base/urls.py index 1cdfe1df..33536a31 100644 --- a/passerelle/base/urls.py +++ b/passerelle/base/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import url from .views import ApiUserCreateView, ApiUserUpdateView, ApiUserDeleteView, \ ApiUserListView, AccessRightDeleteView, AccessRightCreateView, \ - LoggingParametersUpdateView + LoggingParametersUpdateView, ManageAvailabilityView access_urlpatterns = [ url(r'^$', ApiUserListView.as_view(), name='apiuser-list'), @@ -15,5 +15,8 @@ access_urlpatterns = [ url(r'^accessright/add/(?P[\w,-]+)/(?P[\w,-]+)/(?P[\w,-]+)/', AccessRightCreateView.as_view(), name='access-right-add'), url(r'logging/parameters/(?P[\w,-]+)/(?P[\w,-]+)/$', - LoggingParametersUpdateView.as_view(), name='logging-parameters') + LoggingParametersUpdateView.as_view(), name='logging-parameters'), + url(r'manage/availability/(?P[\w,-]+)/(?P[\w,-]+)/$', + ManageAvailabilityView.as_view(), name='manage-availability') + ] diff --git a/passerelle/base/views.py b/passerelle/base/views.py index 20b4a134..51072ca2 100644 --- a/passerelle/base/views.py +++ b/passerelle/base/views.py @@ -5,7 +5,7 @@ from django.forms import models as model_forms from django.views.generic import * from django.http import Http404 -from .models import ApiUser, AccessRight, LoggingParameters +from .models import ApiUser, AccessRight, LoggingParameters, AvailabilityParameters, ResourceStatus from .forms import ApiUserForm, AccessRightForm from ..utils import get_trusted_services @@ -30,6 +30,7 @@ class ResourceView(DetailView): context['absolute_uri'] = '%s%s' % ( context['site_base_uri'], self.request.path) + return context @@ -136,3 +137,54 @@ class LoggingParametersUpdateView(FormView): parameters.trace_emails = form.cleaned_data['trace_emails'] parameters.save() return super(LoggingParametersUpdateView, self).form_valid(form) + + +class ManageAvailabilityView(FormView): + template_name = 'passerelle/manage/manage_availability_form.html' + + def get_context_data(self, **kwargs): + context = super(ManageAvailabilityView, self).get_context_data(**kwargs) + connector = self.get_resource() + context['connector'] = connector + context['availability_status'] = connector.get_availability_status() + return context + + def get_form_class(self): + form_class = model_forms.modelform_factory( + AvailabilityParameters, + fields=['run_check']) + return form_class + + def get_initial(self): + d = self.initial.copy() + d['resource_type'] = self.kwargs['resource_type'] + d['resource_pk'] = self.kwargs['resource_pk'] + d['run_check'] = self.get_resource().availability_parameters.run_check + return d + + def get_resource(self): + content_type = ContentType.objects.get_for_id(self.kwargs['resource_type']) + return content_type.model_class().objects.get(pk=self.kwargs['resource_pk']) + + def get_success_url(self): + return self.get_resource().get_absolute_url() + + def form_valid(self, form): + resource = self.get_resource() + parameters = resource.availability_parameters + run_check = form.cleaned_data['run_check'] + + if not run_check and resource.down(): + resource_type = ContentType.objects.get_for_model(resource) + ResourceStatus( + resource_type=resource_type, + resource_pk=self.kwargs['resource_pk'], + status='up', + message='').save() + + if parameters.run_check != run_check: + parameters.run_check = run_check + parameters.save() + resource.logger.info(u'availability checks %s', 'enabled' if run_check else 'disabled') + + return super(ManageAvailabilityView, self).form_valid(form) diff --git a/passerelle/contrib/planitech/models.py b/passerelle/contrib/planitech/models.py index ac2ef80a..2114bfad 100644 --- a/passerelle/contrib/planitech/models.py +++ b/passerelle/contrib/planitech/models.py @@ -602,6 +602,11 @@ class PlanitechConnector(BaseResource): } } + def check_status(self): + auth_url = urlparse.urljoin(self.url, 'auth') + response = self.requests.get(auth_url, headers={'MH-LOGIN': self.username}) + response.raise_for_status() + class Pairing(models.Model): diff --git a/passerelle/templates/passerelle/manage/manage_availability_form.html b/passerelle/templates/passerelle/manage/manage_availability_form.html new file mode 100644 index 00000000..ae805075 --- /dev/null +++ b/passerelle/templates/passerelle/manage/manage_availability_form.html @@ -0,0 +1,17 @@ +{% extends "passerelle/manage.html" %} +{% load i18n %} + +{% block appbar %} +

{% trans 'Connector Status' %} : {{availability_status.status}}

+{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/passerelle/templates/passerelle/manage/service_view.html b/passerelle/templates/passerelle/manage/service_view.html index b375d08a..4270cb5f 100644 --- a/passerelle/templates/passerelle/manage/service_view.html +++ b/passerelle/templates/passerelle/manage/service_view.html @@ -15,6 +15,9 @@ {% endwith %} +{% if object|can_edit:request.user and has_check_status %} +{% trans 'availability check' %} +{% endif %} {% if object|can_edit:request.user %} {% trans 'logging parameters' %} {% endif %} diff --git a/passerelle/views.py b/passerelle/views.py index cbe237c0..3931230a 100644 --- a/passerelle/views.py +++ b/passerelle/views.py @@ -138,6 +138,13 @@ class GenericConnectorMixin(object): class GenericConnectorView(GenericConnectorMixin, DetailView): + + def get_context_data(self, slug=None, **kwargs): + context = super(GenericConnectorView, self).get_context_data(**kwargs) + context['has_check_status'] = not hasattr( + context['object'].check_status, 'not_implemented') + return context + def get_template_names(self): template_names = super(DetailView, self).get_template_names()[:] if self.model.manager_view_template_name: diff --git a/tests/test_availability.py b/tests/test_availability.py index 435c5d02..bb0bd75f 100644 --- a/tests/test_availability.py +++ b/tests/test_availability.py @@ -73,3 +73,23 @@ def test_feed_availability(app, connector): connector.availability() assert connector.get_availability_status().down() assert '500' in connector.get_availability_status().message + + +def test_availability_checks_disabled(app, connector): + with HTTMock(up_mock): + connector.availability() + assert connector.get_availability_status().up() + + av = connector.availability_parameters + av.run_check = False + av.save() + + with HTTMock(down_mock): + connector.availability() + assert connector.get_availability_status().up() + + av.run_check = True + av.save() + with HTTMock(down_mock): + connector.availability() + assert connector.get_availability_status().down() diff --git a/tests/test_manager.py b/tests/test_manager.py index 39903c70..71d93e31 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.files import File import pytest -from passerelle.base.models import ApiUser, AccessRight, ResourceLog +from passerelle.base.models import ApiUser, AccessRight, ResourceLog, ResourceStatus from passerelle.apps.csvdatasource.models import CsvDataSource, Query pytestmark = pytest.mark.django_db @@ -197,3 +197,54 @@ def test_logging_parameters(app, admin_user): resp = app.get(csv.get_absolute_url()) resp = resp.click('logging parameters') assert resp.form['trace_emails'].value == 'fred@localhost' + + +def test_availability_parameters(app, admin_user, monkeypatch): + data = StringIO('1;Foo\n2;Bar\n3;Baz') + csv = CsvDataSource.objects.create( + csv_file=File(data, 't.csv'), + columns_keynames='id, text', slug='test', title='a title', description='a description') + app = login(app) + resp = app.get(csv.get_absolute_url()) + + assert csv.availability_parameters.run_check + # csv connector has the default check_status which does nothing + # so availability check is hidden + assert 'availability check' not in resp.text + + def check_status(*args, **kwargs): + return True + + monkeypatch.setattr(CsvDataSource, 'check_status', check_status) + + resp = app.get(csv.get_absolute_url()) + assert 'availability check' in resp.text + + resp = resp.click('availability check') + assert 'up' in resp.text + resp.form['run_check'] = False + resp = resp.form.submit() + # Connector status not changed, availability parameters changed + assert not csv.availability_parameters.run_check + + resp = app.get(csv.get_absolute_url()) + resp = resp.click('availability check') + resp.form['run_check'] = True + resp = resp.form.submit() + + # Connector down + resource_type = ContentType.objects.get_for_model(csv) + status = ResourceStatus( + resource_type=resource_type, resource_pk=csv.pk, + status='down', message='') + status.save() + assert csv.down() + resp = app.get(csv.get_absolute_url()) + resp = resp.click('availability check') + resp.form['run_check'] = False + resp = resp.form.submit() + # Connector is put back up + assert not csv.availability_parameters.run_check + assert not csv.down() + status = csv.get_availability_status() + assert status.status == 'up'