Make sure dashboard exists after we create it
The long term goal here is to eventually create python-grafana from our embedded grafana.py file. So, most of this code just makes the Grafana object more user friendly. However, we've also added some validation around create_dashboard, we added some checks before we create and after. To ensure we actually created our new dashboards. Finally, add some sphinx docs since everybody loves documentation. Change-Id: Icbba403afe5208fbef1855118d4c3f4293461e00 Signed-off-by: Paul Belanger <pabelanger@redhat.com>
This commit is contained in:
parent
5e88ddde1e
commit
30f7d21f3f
8
doc/source/api.rst
Normal file
8
doc/source/api.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
:title: API reference
|
||||||
|
|
||||||
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: grafana_dashboards.grafana
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
@ -22,13 +22,11 @@ sys.path.insert(0, os.path.abspath('../..'))
|
|||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
#'sphinx.ext.intersphinx',
|
|
||||||
'oslosphinx'
|
'oslosphinx'
|
||||||
]
|
]
|
||||||
|
|
||||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
# Also document __init__
|
||||||
# text edit cycles.
|
autoclass_content = 'both'
|
||||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
# The suffix of source filenames.
|
||||||
source_suffix = '.rst'
|
source_suffix = '.rst'
|
||||||
|
@ -13,6 +13,7 @@ Contents
|
|||||||
usage
|
usage
|
||||||
contributing
|
contributing
|
||||||
grafana-dashboard
|
grafana-dashboard
|
||||||
|
api
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
@ -48,10 +48,10 @@ class Builder(object):
|
|||||||
def update_dashboard(self, path):
|
def update_dashboard(self, path):
|
||||||
self.parser.parse(path)
|
self.parser.parse(path)
|
||||||
dashboards = self.parser.data.get('dashboard', {})
|
dashboards = self.parser.data.get('dashboard', {})
|
||||||
for item in dashboards:
|
for name in dashboards:
|
||||||
data, md5 = self.parser.get_dashboard(item)
|
data, md5 = self.parser.get_dashboard(name)
|
||||||
if self.cache.has_changed(item, md5):
|
if self.cache.has_changed(name, md5):
|
||||||
self.grafana.create_dashboard(data, overwrite=True)
|
self.grafana.create_dashboard(name, data, overwrite=True)
|
||||||
self.cache.set(item, md5)
|
self.cache.set(name, md5)
|
||||||
else:
|
else:
|
||||||
LOG.debug("'%s' has not changed" % item)
|
LOG.debug("'%s' has not changed" % name)
|
||||||
|
@ -13,17 +13,33 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urlparse import urljoin
|
from urlparse import urljoin
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests import exceptions
|
||||||
|
|
||||||
|
|
||||||
class Grafana(object):
|
class Grafana(object):
|
||||||
|
|
||||||
def __init__(self, url, key=None):
|
def __init__(self, url, key=None):
|
||||||
self.url = urljoin(url, 'api/dashboards/db')
|
"""Create object for grafana instance
|
||||||
|
|
||||||
|
:param url: URL for Grafana server
|
||||||
|
:type url: str
|
||||||
|
:param key: API token used for authenticate
|
||||||
|
:type key: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.url = urljoin(url, 'api/dashboards/db/')
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
# NOTE(pabelanger): Grafana 2.1.0 added basic auth support so now the
|
# NOTE(pabelanger): Grafana 2.1.0 added basic auth support so now the
|
||||||
# api key is optional.
|
# api key is optional.
|
||||||
if key:
|
if key:
|
||||||
@ -31,14 +47,73 @@ class Grafana(object):
|
|||||||
'Authorization': 'Bearer %s' % key,
|
'Authorization': 'Bearer %s' % key,
|
||||||
})
|
})
|
||||||
|
|
||||||
def create_dashboard(self, data, overwrite=False):
|
def assert_dashboard_exists(self, name):
|
||||||
|
"""Raise an exception if dashboard does not exist
|
||||||
|
|
||||||
|
:param name: URL friendly title of the dashboard
|
||||||
|
:type name: str
|
||||||
|
:raises Exception: if dashboard does not exist
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.is_dashboard(name):
|
||||||
|
raise Exception('dashboard[%s] does not exist' % name)
|
||||||
|
|
||||||
|
def create_dashboard(self, name, data, overwrite=False):
|
||||||
|
"""Create a new dashboard
|
||||||
|
|
||||||
|
:param name: URL friendly title of the dashboard
|
||||||
|
:type name: str
|
||||||
|
:param data: Dashboard model
|
||||||
|
:type data: dict
|
||||||
|
:param overwrite: Overwrite existing dashboard with newer version or
|
||||||
|
with the same dashboard title
|
||||||
|
:type overwrite: bool
|
||||||
|
|
||||||
|
:raises Exception: if dashboard already exists
|
||||||
|
|
||||||
|
"""
|
||||||
dashboard = {
|
dashboard = {
|
||||||
'dashboard': data,
|
'dashboard': data,
|
||||||
'overwrite': overwrite,
|
'overwrite': overwrite,
|
||||||
}
|
}
|
||||||
headers = {
|
if not overwrite and self.is_dashboard(name):
|
||||||
'Content-Type': 'application/json',
|
raise Exception('dashboard[%s] already exists' % name)
|
||||||
}
|
|
||||||
res = self.session.post(
|
res = self.session.post(
|
||||||
self.url, data=json.dumps(dashboard), headers=headers)
|
self.url, data=json.dumps(dashboard))
|
||||||
|
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
self.assert_dashboard_exists(name)
|
||||||
|
|
||||||
|
def get_dashboard(self, name):
|
||||||
|
"""Get a dashboard
|
||||||
|
|
||||||
|
:param name: URL friendly title of the dashboard
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:rtype: dict or None
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = urljoin(self.url, name)
|
||||||
|
try:
|
||||||
|
res = self.session.get(url)
|
||||||
|
res.raise_for_status()
|
||||||
|
except exceptions.HTTPError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def is_dashboard(self, name):
|
||||||
|
"""Check if a dashboard exists
|
||||||
|
|
||||||
|
:param name: URL friendly title of the dashboard
|
||||||
|
:type name: str
|
||||||
|
|
||||||
|
:returns: True if dashboard exists
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.get_dashboard(name)
|
||||||
|
if res and res['meta']['slug'] == name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -17,12 +17,35 @@ from testtools import TestCase
|
|||||||
|
|
||||||
from grafana_dashboards.grafana import Grafana
|
from grafana_dashboards.grafana import Grafana
|
||||||
|
|
||||||
|
CREATE_NEW_DASHBOARD = {
|
||||||
|
"meta": {
|
||||||
|
"canSave": True,
|
||||||
|
"created": "0001-01-01T00:00:00Z",
|
||||||
|
"canStar": True,
|
||||||
|
"expires": "0001-01-01T00:00:00Z",
|
||||||
|
"slug": "new-dashboard",
|
||||||
|
"type": "db",
|
||||||
|
"canEdit": True
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"rows": [],
|
||||||
|
"id": 1,
|
||||||
|
"version": 0,
|
||||||
|
"title": "New dashboard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DASHBOARD_NOT_FOUND = {
|
||||||
|
"message": "Dashboard not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestCaseGrafana(TestCase):
|
class TestCaseGrafana(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCaseGrafana, self).setUp()
|
super(TestCaseGrafana, self).setUp()
|
||||||
self.url = 'http://localhost'
|
self.url = 'http://localhost'
|
||||||
|
self.grafana = Grafana(self.url)
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
grafana = Grafana(self.url)
|
grafana = Grafana(self.url)
|
||||||
@ -36,16 +59,63 @@ class TestCaseGrafana(TestCase):
|
|||||||
self.assertEqual(headers['Authorization'], 'Bearer %s' % apikey)
|
self.assertEqual(headers['Authorization'], 'Bearer %s' % apikey)
|
||||||
|
|
||||||
@requests_mock.Mocker()
|
@requests_mock.Mocker()
|
||||||
def test_create_dashboard_apikey(self, mock_requests):
|
def test_assert_dashboard_exists_failure(self, mock_requests):
|
||||||
grafana = Grafana(self.url)
|
mock_requests.get(
|
||||||
mock_requests.register_uri('POST', '/api/dashboards/db')
|
'/api/dashboards/db/new-dashboard', json=DASHBOARD_NOT_FOUND,
|
||||||
|
status_code=404)
|
||||||
|
self.assertRaises(
|
||||||
|
Exception, self.grafana.assert_dashboard_exists, 'new-dashboard')
|
||||||
|
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_create_dashboard_new(self, mock_requests):
|
||||||
|
def post_callback(request, context):
|
||||||
|
mock_requests.get(
|
||||||
|
'/api/dashboards/db/new-dashboard', json=CREATE_NEW_DASHBOARD)
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock_requests.post('/api/dashboards/db/', json=post_callback)
|
||||||
|
mock_requests.get(
|
||||||
|
'/api/dashboards/db/new-dashboard', json=DASHBOARD_NOT_FOUND,
|
||||||
|
status_code=404)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "New dashboard",
|
"title": "New dashboard",
|
||||||
}
|
},
|
||||||
|
"slug": 'new-dashboard',
|
||||||
}
|
}
|
||||||
grafana.create_dashboard(data)
|
self.grafana.create_dashboard(
|
||||||
|
name=data['slug'], data=data['dashboard'])
|
||||||
|
self.assertEqual(mock_requests.call_count, 3)
|
||||||
|
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_create_dashboard_overwrite(self, mock_requests):
|
||||||
|
mock_requests.post('/api/dashboards/db/')
|
||||||
|
mock_requests.get(
|
||||||
|
'/api/dashboards/db/new-dashboard', json=CREATE_NEW_DASHBOARD)
|
||||||
|
data = {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "New dashboard",
|
||||||
|
},
|
||||||
|
"slug": 'new-dashboard',
|
||||||
|
}
|
||||||
|
self.grafana.create_dashboard(
|
||||||
|
name=data['slug'], data=data['dashboard'], overwrite=True)
|
||||||
|
self.assertEqual(mock_requests.call_count, 2)
|
||||||
|
|
||||||
|
@requests_mock.Mocker()
|
||||||
|
def test_create_dashboard_existing(self, mock_requests):
|
||||||
|
mock_requests.post('/api/dashboards/db/')
|
||||||
|
mock_requests.get(
|
||||||
|
'/api/dashboards/db/new-dashboard', json=CREATE_NEW_DASHBOARD)
|
||||||
|
data = {
|
||||||
|
"dashboard": {
|
||||||
|
"title": "New dashboard",
|
||||||
|
},
|
||||||
|
"slug": 'new-dashboard',
|
||||||
|
}
|
||||||
|
self.assertRaises(
|
||||||
|
Exception, self.grafana.create_dashboard, name=data['slug'],
|
||||||
|
data=data['dashboard'], overwrite=False)
|
||||||
|
|
||||||
self.assertEqual(mock_requests.call_count, 1)
|
self.assertEqual(mock_requests.call_count, 1)
|
||||||
headers = mock_requests.last_request.headers
|
|
||||||
self.assertIn('Content-Type', headers)
|
|
||||||
self.assertEqual(headers['Content-Type'], 'application/json')
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user