diff --git a/debian/control b/debian/control
index c7f83b8..33a0609 100644
--- a/debian/control
+++ b/debian/control
@@ -16,7 +16,8 @@ Depends: ${misc:Depends},
python-graypy,
python-apt,
python-memcache,
- python-prometheus-client
+ python-prometheus-client,
+ python-djangorestframework
Recommends: python-django (>= 1.8),
python-gadjo,
python-django-mellon (>= 1.2.22.26),
diff --git a/hobo/environment/models.py b/hobo/environment/models.py
index d002d06..01ab76f 100644
--- a/hobo/environment/models.py
+++ b/hobo/environment/models.py
@@ -1,12 +1,13 @@
import datetime
import json
-
import requests
+import socket
from django.conf import settings
from django.db import models
from django.utils.crypto import get_random_string
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.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
@@ -163,6 +164,31 @@ class ServiceBase(models.Model):
def get_backoffice_menu_url(self):
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):
use_as_idp_for_self = models.BooleanField(
diff --git a/hobo/rest/__init__.py b/hobo/rest/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hobo/rest/serializers.py b/hobo/rest/serializers.py
new file mode 100644
index 0000000..3675df2
--- /dev/null
+++ b/hobo/rest/serializers.py
@@ -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 .
+
+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()
diff --git a/hobo/rest/urls.py b/hobo/rest/urls.py
new file mode 100644
index 0000000..75675b7
--- /dev/null
+++ b/hobo/rest/urls.py
@@ -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 .
+
+from django.conf.urls import url
+from .views import HealthList
+
+urlpatterns = [
+ url(r'^health/$', HealthList.as_view(), name='api-health-list'),
+]
diff --git a/hobo/rest/views.py b/hobo/rest/views.py
new file mode 100644
index 0000000..a8cf9c6
--- /dev/null
+++ b/hobo/rest/views.py
@@ -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 .
+
+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
diff --git a/hobo/settings.py b/hobo/settings.py
index c8a5a72..071985e 100644
--- a/hobo/settings.py
+++ b/hobo/settings.py
@@ -36,6 +36,7 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'rest_framework',
'mellon',
'gadjo',
'hobo.environment',
diff --git a/hobo/urls.py b/hobo/urls.py
index c0af2d4..d247818 100644
--- a/hobo/urls.py
+++ b/hobo/urls.py
@@ -10,6 +10,7 @@ from .environment.urls import urlpatterns as environment_urls
from .profile.urls import urlpatterns as profile_urls
from .theme.urls import urlpatterns as theme_urls
from .emails.urls import urlpatterns as emails_urls
+from .rest.urls import urlpatterns as rest_urls
urlpatterns = [
url(r'^$', home, name='home'),
@@ -20,6 +21,7 @@ urlpatterns = [
url(r'^theme/', decorated_includes(admin_required,
include(theme_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'^hobos.json$', hobo),
url(r'^admin/', include(admin.site.urls)),
diff --git a/setup.py b/setup.py
index 8cf8a29..7e5c273 100644
--- a/setup.py
+++ b/setup.py
@@ -101,6 +101,7 @@ setup(
'django-mellon',
'django-tenant-schemas',
'prometheus_client',
+ 'djangorestframework>=3.1, <3.7',
],
zip_safe=False,
cmdclass={
diff --git a/tests/test_health_api.py b/tests/test_health_api.py
new file mode 100644
index 0000000..d923bf9
--- /dev/null
+++ b/tests/test_health_api.py
@@ -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']
diff --git a/tox.ini b/tox.ini
index a614726..594fbdd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -38,6 +38,7 @@ deps:
cssselect
WebTest
django-mellon
+ django-webtest<1.9.3
celery<4
Markdown<3
django18: django-tables2<1.1
@@ -48,6 +49,7 @@ deps:
passerelle: python-memcached
http://git.entrouvert.org/debian/django-tenant-schemas.git/snapshot/django-tenant-schemas-master.tar.gz
httmock
+ requests
commands =
./getlasso.sh
hobo: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests/}