general: provide a health api (#23823)
This commit is contained in:
parent
23735f44bc
commit
a6df6bdcd6
|
@ -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),
|
||||||
|
|
|
@ -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,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()
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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
|
|
@ -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',
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -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={
|
||||||
|
|
|
@ -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']
|
2
tox.ini
2
tox.ini
|
@ -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/}
|
||||||
|
|
Loading…
Reference in New Issue