diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 0000000..31eee18 --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,8 @@ +:title: API reference + +API Reference +============= + +.. automodule:: grafana_dashboards.grafana + :members: + :undoc-members: diff --git a/doc/source/conf.py b/doc/source/conf.py index 25fda3e..713eec0 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -22,13 +22,11 @@ sys.path.insert(0, os.path.abspath('../..')) # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', - #'sphinx.ext.intersphinx', 'oslosphinx' ] -# autodoc generation is a bit aggressive and a nuisance when doing heavy -# text edit cycles. -# execute "export SPHINX_DEBUG=1" in your terminal to disable +# Also document __init__ +autoclass_content = 'both' # The suffix of source filenames. source_suffix = '.rst' diff --git a/doc/source/index.rst b/doc/source/index.rst index 0403aee..b62586e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ Contents usage contributing grafana-dashboard + api Indices and tables ================== diff --git a/grafana_dashboards/builder.py b/grafana_dashboards/builder.py index b56ed68..1d6aabd 100644 --- a/grafana_dashboards/builder.py +++ b/grafana_dashboards/builder.py @@ -63,10 +63,10 @@ class Builder(object): def update_dashboard(self, path): self.load_files(path) dashboards = self.parser.data.get('dashboard', {}) - for item in dashboards: - data, md5 = self.parser.get_dashboard(item) - if self.cache.has_changed(item, md5): - self.grafana.create_dashboard(data, overwrite=True) - self.cache.set(item, md5) + for name in dashboards: + data, md5 = self.parser.get_dashboard(name) + if self.cache.has_changed(name, md5): + self.grafana.create_dashboard(name, data, overwrite=True) + self.cache.set(name, md5) else: - LOG.debug("'%s' has not changed" % item) + LOG.debug("'%s' has not changed" % name) diff --git a/grafana_dashboards/grafana.py b/grafana_dashboards/grafana.py index 1ff6619..288c19e 100644 --- a/grafana_dashboards/grafana.py +++ b/grafana_dashboards/grafana.py @@ -13,17 +13,33 @@ # under the License. import json -import requests + try: from urllib.parse import urljoin except ImportError: from urlparse import urljoin +import requests +from requests import exceptions + class Grafana(object): + 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.headers.update({ + 'Content-Type': 'application/json', + }) # NOTE(pabelanger): Grafana 2.1.0 added basic auth support so now the # api key is optional. if key: @@ -31,14 +47,73 @@ class Grafana(object): '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': data, 'overwrite': overwrite, } - headers = { - 'Content-Type': 'application/json', - } + if not overwrite and self.is_dashboard(name): + raise Exception('dashboard[%s] already exists' % name) + res = self.session.post( - self.url, data=json.dumps(dashboard), headers=headers) + self.url, data=json.dumps(dashboard)) + 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 diff --git a/tests/test_grafana.py b/tests/test_grafana.py index beb0f7c..f05f0b4 100644 --- a/tests/test_grafana.py +++ b/tests/test_grafana.py @@ -17,12 +17,35 @@ from testtools import TestCase 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): def setUp(self): super(TestCaseGrafana, self).setUp() self.url = 'http://localhost' + self.grafana = Grafana(self.url) def test_init(self): grafana = Grafana(self.url) @@ -36,16 +59,63 @@ class TestCaseGrafana(TestCase): self.assertEqual(headers['Authorization'], 'Bearer %s' % apikey) @requests_mock.Mocker() - def test_create_dashboard_apikey(self, mock_requests): - grafana = Grafana(self.url) - mock_requests.register_uri('POST', '/api/dashboards/db') + def test_assert_dashboard_exists_failure(self, mock_requests): + mock_requests.get( + '/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 = { "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) - headers = mock_requests.last_request.headers - self.assertIn('Content-Type', headers) - self.assertEqual(headers['Content-Type'], 'application/json')