From 88b89bb476cd34d046119dcf83e46bb7b143aa69 Mon Sep 17 00:00:00 2001 From: Vitalii_Rymar Date: Tue, 8 Oct 2024 17:34:54 +0300 Subject: [PATCH] Implemented import dashboards from a directory --- HISTORY.md | 1 + README.md | 12 ++++++-- grafana_import/cli.py | 69 ++++++++++++++++++++++++++++--------------- tests/test_cli.py | 15 ++++------ 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 408187c..f8fcd19 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,7 @@ - Fixed folder argument issue - Fixed import dashboards into a folder - Added keep-uid argument to preserve the dashboard uid provided in file +- Added an option to import dashboards from a directory Thanks, @vrymar. diff --git a/README.md b/README.md index 4e460f3..20d1ae8 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ Watch the input dashboard for changes on disk, and re-upload it, when changed. grafana-import import --overwrite --reload -i gd-prometheus.py ``` +### Import dashboards from a directory +Import all dashboards from provided directory +```shell +grafana-import import -i "./dashboards_folder" +``` + ### Export Export the dashboard titled `my-first-dashboard` to the default export directory. ```bash @@ -198,19 +204,19 @@ jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main #### Usage Render dashboard defined in [Grafonnet]/[Jsonnet]. ```shell -grafana-import import --overwrite -i /path/to/faro.jsonnet +grafana-import import --overwrite -i ./path/to/faro.jsonnet ``` ### grafana-dashboard Render dashboard defined using [grafana-dashboard]. ```shell -grafana-import import --overwrite -i /path/to/gd-dashboard.py +grafana-import import --overwrite -i ./path/to/gd-dashboard.py ``` ### grafanalib Render dashboard defined using [grafanalib]. ```shell -grafana-import import --overwrite -i /path/to/gl-dashboard.py +grafana-import import --overwrite -i ./path/to/gl-dashboard.py ``` diff --git a/grafana_import/cli.py b/grafana_import/cli.py index e9cdb8c..cf93c86 100644 --- a/grafana_import/cli.py +++ b/grafana_import/cli.py @@ -46,7 +46,7 @@ def save_dashboard(config, args, base_path, dashboard_name, dashboard, action): try: output = open(output_file, "w") except OSError as e: - print("File {0} error: {1}.".format(output_file, e.strerror)) + logger.error("File {0} error: {1}.".format(output_file, e.strerror)) sys.exit(2) content = None @@ -56,7 +56,7 @@ def save_dashboard(config, args, base_path, dashboard_name, dashboard, action): content = json.dumps(dashboard["dashboard"]) output.write(content) output.close() - print(f"OK: Dashboard '{dashboard_name}' {action} to: {output_file}") + logger.info(f"OK: Dashboard '{dashboard_name}' {action} to: {output_file}") class myArgs: @@ -212,7 +212,7 @@ def main(): if args.action == "exporter" and ( "dashboard_name" not in config["general"] or config["general"]["dashboard_name"] is None ): - print("ERROR: no dashboard has been specified.") + logger.error("ERROR: no dashboard has been specified.") sys.exit(1) config["check_folder"] = False @@ -238,29 +238,46 @@ def main(): try: grafana_api = Grafana.Grafana(**params) except Exception as e: - print(f"ERROR: {e}") + logger.error(f"ERROR: {e}") sys.exit(1) # Import if args.action == "import": if args.dashboard_file is None: - print("ERROR: no file to import provided!") + logger.error("ERROR: no file to import provided!") sys.exit(1) # Compute effective input file path. import_path = "" import_file = args.dashboard_file + import_files = [] + if not re.search(r"^(?:(?:/)|(?:\.?\./))", import_file): import_path = base_path - if "imports_path" in config["general"]: - import_path = os.path.join(import_path, config["general"]["imports_path"]) - import_file = os.path.join(import_path, import_file) - - def process_dashboard(): + if "import_path" in config["general"]: + import_path = os.path.join(import_path, config["general"]["import_path"]) + import_file = os.path.join(import_path, import_file) + import_files.append(import_file) + else: + if os.path.isfile(import_file): + logger.info(f"The path is a file: '{import_file}'") + import_file = os.path.join(import_path, import_file) + import_files.append(import_file) + + if os.path.isdir(import_file): + logger.info(f"The path is a directory: '{import_file}'") + import_files = [ + os.path.join(import_file, f) + for f in os.listdir(import_file) + if os.path.isfile(os.path.join(import_file, f)) + ] + logger.info(f"Found the following files: '{import_files}' in dir '{import_file}'") + + def process_dashboard(file_path): try: - dash = read_dashboard_file(import_file) + dash = read_dashboard_file(file_path) except Exception as ex: - msg = f"Failed to load dashboard from: {import_file}. Reason: {ex}" + msg = f"Failed to load dashboard from: {file_path}. Reason: {ex}" logger.exception(msg) raise IOError(msg) from ex @@ -280,13 +297,17 @@ def process_dashboard(): logger.error(msg) raise IOError(msg) - try: - process_dashboard() - except Exception: - sys.exit(1) + for file in import_files: + print(f"Processing file: {file}") + try: + process_dashboard(file) + except Exception as e: + logger.error(f"Failed to process file {file}. Reason: {str(e)}") + continue if args.reload: - watchdog_service(import_file, process_dashboard) + for file in import_files: + watchdog_service(import_file, process_dashboard(file)) sys.exit(0) @@ -295,19 +316,19 @@ def process_dashboard(): dashboard_name = config["general"]["dashboard_name"] try: grafana_api.remove_dashboard(dashboard_name) - print(f"OK: Dashboard removed: {dashboard_name}") + logger.info(f"OK: Dashboard removed: {dashboard_name}") sys.exit(0) except Grafana.GrafanaDashboardNotFoundError as exp: - print(f"KO: Dashboard not found in folder '{exp.folder}': {exp.dashboard}") + logger.info(f"KO: Dashboard not found in folder '{exp.folder}': {exp.dashboard}") sys.exit(1) except Grafana.GrafanaFolderNotFoundError as exp: - print(f"KO: Folder not found: {exp.folder}") + logger.info(f"KO: Folder not found: {exp.folder}") sys.exit(1) except GrafanaApi.GrafanaBadInputError as exp: - print(f"KO: Removing dashboard failed: {dashboard_name}. Reason: {exp}") + logger.info(f"KO: Removing dashboard failed: {dashboard_name}. Reason: {exp}") sys.exit(1) except Exception: - print("ERROR: Dashboard '{0}' remove exception '{1}'".format(dashboard_name, traceback.format_exc())) + logger.info("ERROR: Dashboard '{0}' remove exception '{1}'".format(dashboard_name, traceback.format_exc())) sys.exit(1) # Export @@ -316,10 +337,10 @@ def process_dashboard(): try: dash = grafana_api.export_dashboard(dashboard_name) except (Grafana.GrafanaFolderNotFoundError, Grafana.GrafanaDashboardNotFoundError): - print("KO: Dashboard name not found: {0}".format(dashboard_name)) + logger.info("KO: Dashboard name not found: {0}".format(dashboard_name)) sys.exit(1) except Exception: - print("ERROR: Dashboard '{0}' export exception '{1}'".format(dashboard_name, traceback.format_exc())) + logger.info("ERROR: Dashboard '{0}' export exception '{1}'".format(dashboard_name, traceback.format_exc())) sys.exit(1) if dash is not None: diff --git a/tests/test_cli.py b/tests/test_cli.py index ba9bd07..ef0cb67 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -45,7 +45,7 @@ def test_import_dashboard_success(mocked_grafana, mocked_responses, tmp_path, ca @pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"]) -def test_export_dashboard_success(mocked_grafana, mocked_responses, capsys, use_settings): +def test_export_dashboard_success(mocked_grafana, mocked_responses, caplog, use_settings): """ Verify "export dashboard" works. """ @@ -66,12 +66,11 @@ def test_export_dashboard_success(mocked_grafana, mocked_responses, capsys, use_ m.stop() assert ex.match("0") - out, err = capsys.readouterr() - assert re.match(r"OK: Dashboard 'foobar' exported to: ./foobar_\d+.json", out) + assert re.match(r".*OK: Dashboard 'foobar' exported to: ./foobar_\d+.json.*", caplog.text, re.DOTALL) @pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"]) -def test_export_dashboard_notfound(mocked_grafana, mocked_responses, capsys, use_settings): +def test_export_dashboard_notfound(mocked_grafana, mocked_responses, caplog, use_settings): """ Verify "export dashboard" fails appropriately when addressed dashboard does not exist. """ @@ -88,12 +87,11 @@ def test_export_dashboard_notfound(mocked_grafana, mocked_responses, capsys, use main() assert ex.match("1") - out, err = capsys.readouterr() - assert "Dashboard name not found: foobar" in out + assert "Dashboard name not found: foobar" in caplog.text @pytest.mark.parametrize("use_settings", [True, False], ids=["config-yes", "config-no"]) -def test_remove_dashboard_success(mocked_grafana, mocked_responses, capsys, use_settings): +def test_remove_dashboard_success(mocked_grafana, mocked_responses, caplog, use_settings): """ Verify "remove dashboard" works. """ @@ -110,5 +108,4 @@ def test_remove_dashboard_success(mocked_grafana, mocked_responses, capsys, use_ main() assert ex.match("0") - out, err = capsys.readouterr() - assert "OK: Dashboard removed: foobar" in out + assert "OK: Dashboard removed: foobar" in caplog.text