From fd76590e4bf1443d22a71286e56fa09318566fcd Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Sat, 3 Oct 2015 19:47:32 -0400 Subject: [PATCH] Add delete command to CLI Add the ability to delete dashboards based on the provided yaml file. We also removed the assert_dashboard_exists function, as it didn't really save us code. Change-Id: I417a72fcc5252b36cadfe8881b4f5ca6acb7c753 Signed-off-by: Paul Belanger --- doc/source/grafana-dashboard.rst | 7 +++++ grafana_dashboards/builder.py | 8 ++++++ grafana_dashboards/cmd.py | 12 +++++++++ grafana_dashboards/grafana/dashboard.py | 29 +++++++++++---------- tests/cmd/test_delete.py | 34 +++++++++++++++++++++++++ tests/test_builder.py | 29 ++++++++++++++++----- tests/test_grafana.py | 25 +++++++++++------- 7 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 tests/cmd/test_delete.py diff --git a/doc/source/grafana-dashboard.rst b/doc/source/grafana-dashboard.rst index d652831..4ded481 100644 --- a/doc/source/grafana-dashboard.rst +++ b/doc/source/grafana-dashboard.rst @@ -26,6 +26,13 @@ OPTIONS COMMANDS ======== +Delete Command +-------------- + +``grafana-dashboard`` [options] delete + +Delete each specified dashboard from the parsed yaml files. + Update Command -------------- diff --git a/grafana_dashboards/builder.py b/grafana_dashboards/builder.py index f6333a6..c6a9b5d 100644 --- a/grafana_dashboards/builder.py +++ b/grafana_dashboards/builder.py @@ -54,6 +54,14 @@ class Builder(object): url = 'http://localhost:8080' return Grafana(url, key) + def delete_dashboard(self, path): + self.load_files(path) + dashboards = self.parser.data.get('dashboard', {}) + for name in dashboards: + LOG.debug('Deleting grafana dashboard %s', name) + self.grafana.dashboard.delete(name) + self.cache.set(name, '') + def load_files(self, path): files_to_process = [] if os.path.isdir(path): diff --git a/grafana_dashboards/cmd.py b/grafana_dashboards/cmd.py index 01380f2..22681da 100644 --- a/grafana_dashboards/cmd.py +++ b/grafana_dashboards/cmd.py @@ -27,6 +27,11 @@ LOG = logging.getLogger(__name__) class Client(object): + def delete(self): + LOG.info('Deleting dashboards in %s', self.args.path) + builder = Builder(self.config) + builder.delete_dashboard(self.args.path) + def main(self): self.parse_arguments() self.read_config() @@ -51,6 +56,12 @@ class Client(object): subparsers = parser.add_subparsers( title='commands') + parser_delete = subparsers.add_parser('delete') + parser_delete.add_argument( + 'path', help='colon-separated list of paths to YAML files or' + ' directories') + parser_delete.set_defaults(func=self.delete) + parser_update = subparsers.add_parser('update') parser_update.add_argument( 'path', help='colon-separated list of paths to YAML files or' @@ -87,6 +98,7 @@ class Client(object): def validate(self): LOG.info('Validating dashboards in %s', self.args.path) builder = Builder(self.config) + try: builder.load_files(self.args.path) print('SUCCESS!') diff --git a/grafana_dashboards/grafana/dashboard.py b/grafana_dashboards/grafana/dashboard.py index ad6d8f7..68ca57f 100644 --- a/grafana_dashboards/grafana/dashboard.py +++ b/grafana_dashboards/grafana/dashboard.py @@ -28,17 +28,6 @@ class Dashboard(object): self.url = url self.session = session - 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(self, name, data, overwrite=False): """Create a new dashboard @@ -62,9 +51,23 @@ class Dashboard(object): res = self.session.post( self.url, data=json.dumps(dashboard)) - res.raise_for_status() - self.assert_dashboard_exists(name) + if not self.is_dashboard(name): + raise Exception('dashboard[%s] does not exist' % name) + + def delete(self, name): + """Delete a dashboard + + :param name: URL friendly title of the dashboard + :type name: str + + :raises Exception: if dashboard failed to delete + + """ + url = urljoin(self.url, name) + self.session.delete(url) + if self.is_dashboard(name): + raise Exception('dashboard[%s] failed to delete' % name) def get(self, name): """Get a dashboard diff --git a/tests/cmd/test_delete.py b/tests/cmd/test_delete.py new file mode 100644 index 0000000..ff94316 --- /dev/null +++ b/tests/cmd/test_delete.py @@ -0,0 +1,34 @@ +# Copyright 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from testtools import matchers + +from tests.cmd.base import TestCase + + +class TestCaseDelete(TestCase): + + def test_delete_without_path(self): + required = [ + '.*?^usage: grafana-dashboards delete \[-h\] path', + '.*?^grafana-dashboards delete: error: (too few arguments|the ' + 'following arguments are required: path)', + ] + stdout, stderr = self.shell('delete', exitcodes=[2]) + for r in required: + self.assertThat( + (stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) diff --git a/tests/test_builder.py b/tests/test_builder.py index c2a266d..81417e0 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -26,17 +26,34 @@ class TestCaseBuilder(TestCase): super(TestCaseBuilder, self).setUp() self.builder = builder.Builder(self.config) - @mock.patch('grafana_dashboards.grafana.Dashboard.create') - def test_update_dashboard(self, mock_grafana): - dashboard = os.path.join( + @mock.patch('grafana_dashboards.grafana.Dashboard.delete') + def test_delete_dashboard(self, mock_grafana): + path = os.path.join( os.path.dirname(__file__), 'fixtures/builder/dashboard-0001.yaml') - self.builder.update_dashboard(dashboard) - # Cache is empty, so we should update grafana. + # Create a dashboard. + self._update_dashboard(path) + # Create a new builder to avoid duplicate dashboards. + builder2 = builder.Builder(self.config) + # Delete same dashboard, ensure we delete it from grafana. + builder2.delete_dashboard(path) self.assertEqual(mock_grafana.call_count, 1) + @mock.patch('grafana_dashboards.grafana.Dashboard.create') + def test_update_dashboard(self, mock_grafana): + path = os.path.join( + os.path.dirname(__file__), 'fixtures/builder/dashboard-0001.yaml') + + # Create a dashboard. + self._update_dashboard(path) # Create a new builder to avoid duplicate dashboards. builder2 = builder.Builder(self.config) # Update again with same dashboard, ensure we don't update grafana. - builder2.update_dashboard(dashboard) + builder2.update_dashboard(path) + self.assertEqual(mock_grafana.call_count, 0) + + @mock.patch('grafana_dashboards.grafana.Dashboard.create') + def _update_dashboard(self, path, mock_grafana): + self.builder.update_dashboard(path) + # Cache is empty, so we should update grafana. self.assertEqual(mock_grafana.call_count, 1) diff --git a/tests/test_grafana.py b/tests/test_grafana.py index f626cee..b837e3f 100644 --- a/tests/test_grafana.py +++ b/tests/test_grafana.py @@ -58,15 +58,6 @@ class TestCaseGrafana(TestCase): self.assertIn('Authorization', headers) self.assertEqual(headers['Authorization'], 'Bearer %s' % apikey) - @requests_mock.Mocker() - 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.dashboard.assert_dashboard_exists, - 'new-dashboard') - @requests_mock.Mocker() def test_create_dashboard_new(self, mock_requests): def post_callback(request, context): @@ -120,3 +111,19 @@ class TestCaseGrafana(TestCase): data=data['dashboard'], overwrite=False) self.assertEqual(mock_requests.call_count, 1) + + @requests_mock.Mocker() + def test_delete_dashboard(self, mock_requests): + mock_requests.delete('/api/dashboards/db/new-dashboard') + mock_requests.get( + '/api/dashboards/db/new-dashboard', json=DASHBOARD_NOT_FOUND, + status_code=404) + self.grafana.dashboard.delete('new-dashboard') + + @requests_mock.Mocker() + def test_delete_dashboard_failure(self, mock_requests): + mock_requests.delete('/api/dashboards/db/new-dashboard') + mock_requests.get( + '/api/dashboards/db/new-dashboard', json=CREATE_NEW_DASHBOARD) + self.assertRaises( + Exception, self.grafana.dashboard.delete, name='new-dashboard')