general: provide a health api (#23823)

This commit is contained in:
Christophe Siraut 2018-09-14 14:36:47 +02:00 committed by Frédéric Péters
parent 23735f44bc
commit a6df6bdcd6
11 changed files with 218 additions and 2 deletions

3
debian/control vendored
View File

@ -16,7 +16,8 @@ Depends: ${misc:Depends},
python-graypy, python-graypy,
python-apt, python-apt,
python-memcache, python-memcache,
python-prometheus-client python-prometheus-client,
python-djangorestframework
Recommends: python-django (>= 1.8), Recommends: python-django (>= 1.8),
python-gadjo, python-gadjo,
python-django-mellon (>= 1.2.22.26), python-django-mellon (>= 1.2.22.26),

View File

@ -1,12 +1,13 @@
import datetime import datetime
import json import json
import requests import requests
import socket
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urlparse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -163,6 +164,31 @@ class ServiceBase(models.Model):
def get_backoffice_menu_url(self): def get_backoffice_menu_url(self):
return None return None
def is_resolvable(self):
try:
netloc = urlparse(self.base_url).netloc
if netloc and socket.gethostbyname(netloc):
return True
except socket.gaierror:
return False
def has_valid_certificate(self):
if not self.is_resolvable():
return False
try:
requests.get(self.base_url, verify=True)
return True
except requests.exceptions.SSLError:
return False
except requests.exceptions.ConnectionError:
return False
def is_running(self):
if not self.is_resolvable():
return False
r = requests.get(self.get_admin_zones()[0].href, verify=False)
return bool(r.status_code is 200)
class Authentic(ServiceBase): class Authentic(ServiceBase):
use_as_idp_for_self = models.BooleanField( use_as_idp_for_self = models.BooleanField(

0
hobo/rest/__init__.py Normal file
View File

26
hobo/rest/serializers.py Normal file
View File

@ -0,0 +1,26 @@
# hobo - portal to configure and deploy applications
# Copyright (C) 2015-2018 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from rest_framework import serializers
class HealthBaseSerializer(serializers.Serializer):
title = serializers.CharField()
base_url = serializers.CharField()
is_resolvable = serializers.BooleanField()
has_valid_certificate = serializers.BooleanField()
is_running = serializers.BooleanField()
is_operational = serializers.BooleanField()

22
hobo/rest/urls.py Normal file
View File

@ -0,0 +1,22 @@
# hobo - portal to configure and deploy applications
# Copyright (C) 2015-2018 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import url
from .views import HealthList
urlpatterns = [
url(r'^health/$', HealthList.as_view(), name='api-health-list'),
]

33
hobo/rest/views.py Normal file
View File

@ -0,0 +1,33 @@
# hobo - portal to configure and deploy applications
# Copyright (C) 2015-2018 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
from rest_framework import generics
from rest_framework import permissions
from hobo.environment.models import AVAILABLE_SERVICES
from hobo.rest.serializers import HealthBaseSerializer
class HealthList(generics.ListAPIView):
serializer_class = HealthBaseSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_queryset(self):
qs = []
for service in AVAILABLE_SERVICES:
for instance in service.objects.all():
qs.append(instance)
return qs

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = (
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework',
'mellon', 'mellon',
'gadjo', 'gadjo',
'hobo.environment', 'hobo.environment',

View File

@ -10,6 +10,7 @@ from .environment.urls import urlpatterns as environment_urls
from .profile.urls import urlpatterns as profile_urls from .profile.urls import urlpatterns as profile_urls
from .theme.urls import urlpatterns as theme_urls from .theme.urls import urlpatterns as theme_urls
from .emails.urls import urlpatterns as emails_urls from .emails.urls import urlpatterns as emails_urls
from .rest.urls import urlpatterns as rest_urls
urlpatterns = [ urlpatterns = [
url(r'^$', home, name='home'), url(r'^$', home, name='home'),
@ -20,6 +21,7 @@ urlpatterns = [
url(r'^theme/', decorated_includes(admin_required, url(r'^theme/', decorated_includes(admin_required,
include(theme_urls))), include(theme_urls))),
url(r'^emails/', decorated_includes(admin_required, include(emails_urls))), url(r'^emails/', decorated_includes(admin_required, include(emails_urls))),
url(r'^api/', include(rest_urls)),
url(r'^menu.json$', menu_json, name='menu_json'), url(r'^menu.json$', menu_json, name='menu_json'),
url(r'^hobos.json$', hobo), url(r'^hobos.json$', hobo),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),

View File

@ -101,6 +101,7 @@ setup(
'django-mellon', 'django-mellon',
'django-tenant-schemas', 'django-tenant-schemas',
'prometheus_client', 'prometheus_client',
'djangorestframework>=3.1, <3.7',
], ],
zip_safe=False, zip_safe=False,
cmdclass={ cmdclass={

102
tests/test_health_api.py Normal file
View File

@ -0,0 +1,102 @@
import json
from mock import MagicMock
import pytest
import requests
import socket
from django.utils import timezone
from hobo.environment.models import Authentic, Combo
pytestmark = pytest.mark.django_db
def get_entry(ls, title):
"""
Search a list of dictionnaries
"""
entries = [i for i in ls if i['title'] == title]
if len(entries) > 1:
raise(Exception('There is more than 1 %s' % title))
return entries[0]
@pytest.fixture
def services(request):
now = timezone.now()
a = Authentic(title='blues', slug='blues', base_url='https://blues.example.publik',
last_operational_check_timestamp=now,
last_operational_success_timestamp=now)
a.save()
c = Combo(title='jazz', slug='jazz', base_url='https://jazz.example.publik')
c.save()
def test_response(app, services, monkeypatch):
monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
monkeypatch.setattr(requests, 'get', lambda x, verify: MagicMock(status_code=200))
response = app.get('/api/health/')
assert response.status_code == 200
content = json.loads(response.content)
assert len(content) == 2
assert content[0]['title'] == 'blues'
assert content[1]['title'] == 'jazz'
def test_is_resolvable(app, services, monkeypatch):
def gethostname(netloc):
if netloc == "jazz.example.publik":
return '176.31.123.109'
else:
raise socket.gaierror
monkeypatch.setattr(socket, 'gethostbyname', gethostname)
monkeypatch.setattr(requests, 'get', lambda x, verify: MagicMock(status_code=200))
response = app.get('/api/health/')
content = json.loads(response.content)
blues = get_entry(content, 'blues')
jazz = get_entry(content, 'jazz')
assert not blues['is_resolvable']
assert jazz['is_resolvable']
def test_is_running(app, services, monkeypatch):
def get(url, verify):
if url == 'https://jazz.example.publik/manage/':
return MagicMock(status_code=200)
else:
return MagicMock(status_code=404)
monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
monkeypatch.setattr(requests, 'get', get)
response = app.get('/api/health/')
content = json.loads(response.content)
blues = get_entry(content, 'blues')
jazz = get_entry(content, 'jazz')
assert not blues['is_running']
assert jazz['is_running']
def test_is_operational(app, services, monkeypatch):
monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
monkeypatch.setattr(requests, 'get', lambda x, verify: MagicMock(status_code=200))
response = app.get('/api/health/')
content = json.loads(response.content)
blues = get_entry(content, 'blues')
jazz = get_entry(content, 'jazz')
assert blues['is_operational']
assert not jazz['is_operational']
def test_has_valid_certificate(app, services, monkeypatch):
def get(url, verify):
if 'blues.example.publik' in url or not verify:
return MagicMock(status_code=200)
else:
raise requests.exceptions.SSLError
monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
monkeypatch.setattr(requests, 'get', get)
response = app.get('/api/health/')
content = json.loads(response.content)
blues = get_entry(content, 'blues')
jazz = get_entry(content, 'jazz')
assert blues['has_valid_certificate']
assert not jazz['has_valid_certificate']

View File

@ -38,6 +38,7 @@ deps:
cssselect cssselect
WebTest WebTest
django-mellon django-mellon
django-webtest<1.9.3
celery<4 celery<4
Markdown<3 Markdown<3
django18: django-tables2<1.1 django18: django-tables2<1.1
@ -48,6 +49,7 @@ deps:
passerelle: python-memcached passerelle: python-memcached
http://git.entrouvert.org/debian/django-tenant-schemas.git/snapshot/django-tenant-schemas-master.tar.gz http://git.entrouvert.org/debian/django-tenant-schemas.git/snapshot/django-tenant-schemas-master.tar.gz
httmock httmock
requests
commands = commands =
./getlasso.sh ./getlasso.sh
hobo: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests/} hobo: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests/}