Skip to content

Commit

Permalink
Permit invocation without configuration file for ad hoc operations
Browse files Browse the repository at this point in the history
In this mode, the Grafana URL can optionally be defined using the
environment variable `GRAFANA_URL`.
  • Loading branch information
amotl committed Apr 23, 2024
1 parent 2d7f44d commit 4c17003
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 81 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

# History

## Unreleased
* Permit invocation without configuration file for ad hoc operations.
In this mode, the Grafana URL can optionally be defined using the
environment variable `GRAFANA_URL`.

## 0.2.0 (2022-02-05)
* Migrated from grafana_api to grafana_client

Expand Down
102 changes: 67 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,63 @@ Currently, there is no up-to-date version on PyPI, so we recommend to
install directly from the repository.


## Configuration
## Ad Hoc Usage

The configuration is stored in a YAML file. In order to connect to Grafana, you
will need an authentication token for Grafana.
You can use `grafana-import` in ad hoc mode without a configuration file.

### Getting started

In order to do some orientation flights, start a Grafana instance using Podman
or Docker.
```shell
docker run --rm -it --name=grafana --publish=3000:3000 \
--env='GF_SECURITY_ADMIN_PASSWORD=admin' grafana/grafana:latest
```

Define Grafana endpoint.
```shell
export GRAFANA_URL=http://admin:admin@localhost:3000
```

### Import
Import a dashboard from a JSON file into the `Applications` folder in Grafana.
```shell
grafana-import import -i grafana-dashboard.json -f Applications -o
```
Please note the import action preserves the version history.

### Export
Export the dashboard titled `my-first-dashboard` to the default export directory.
```bash
grafana-import export -d "my-first-dashboard" --pretty
```

### Delete
Delete the dashboard titled `my-first-dashboard` from folder `Applications`.
```bash
grafana-import remove -f Applications -d "my-first-dashboard"
```


## Usage with Configuration File

You can also use `grafana-import` with a configuration file. In this way, you
can manage and use different Grafana connection profiles, and also use presets
for application-wide configuration settings.

The configuration is stored in a YAML file. In order to use it optimally,
build a directory structure like this:
```
grafana-import/
- conf/grafana-import.yml
Path to your main configuration file.
- exports/
Path where exported dashboards will be stored.
- imports/
Path where dashboards are imported from.
```

Then, enter into your directory, and type in your commands.

The configuration file uses two sections, `general`, and `grafana`.

Expand Down Expand Up @@ -71,19 +124,17 @@ server URL.
</details>
## Usage
## Authentication
build a directory structure:
- grafana-import/
- conf/grafana-import.yml
where your main configuration file is
- exports/
where your exported dashboards will be stored.
- imports/
where your dashboards to import are stored.
In order to connect to Grafana, you can use either vanilla credentials
(username/password), or an authentication token. Because `grafana-import`
uses `grafana-client`, the same features for defining authentication
settings can be used. See also [grafana-client authentication variants].

Then, enter into your directory, and type in your commands.
Please note the import action preserves the version history.
[grafana-client authentication variants]: https://github.com/panodata/grafana-client/#authentication


## Help

`grafana-import --help`
```shell
Expand Down Expand Up @@ -117,6 +168,8 @@ optional arguments:
path to config files.
-d DASHBOARD_NAME, --dashboard_name DASHBOARD_NAME
name of dashboard to export.
-u GRAFANA_URL, --grafana_url GRAFANA_URL
Grafana URL to connect to.
-g GRAFANA_LABEL, --grafana_label GRAFANA_LABEL
label in the config file that represents the grafana to
connect to.
Expand All @@ -134,27 +187,6 @@ optional arguments:
```


## Examples

### Import
Import a dashboard from a JSON file to the folder `Applications` in Grafana.
```shell
grafana-import -i my-first-dashboard_202104011548.json -f Applications -o
```

### Export
Export the dashboard `my-first-dashboard` to the default export directory.
```bash
grafana-import -d "my-first-dashboard" -p export
```

### Delete
Delete the dashboard `my-first-dashboard` from folder `Applications`.
```bash
grafana-import -f Applications -d "my-first-dashboard" remove
```


## Contributing

Contributions are welcome, and they are greatly appreciated. You can contribute
Expand Down
15 changes: 10 additions & 5 deletions grafana_import/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def main():
parser.add_argument('-d', '--dashboard_name'
, help='name of dashboard to export.')

parser.add_argument('-u', '--grafana_url'
, help='Grafana URL to connect to.'
, required=False)

parser.add_argument('-g', '--grafana_label'
, help='label in the config file that represents the grafana to connect to.'
, default='default')
Expand Down Expand Up @@ -148,14 +152,15 @@ def main():
if args.base_path is not None:
base_path = inArgs.base_path

config_file = os.path.join(base_path, CONFIG_NAME)
if args.config_file is not None:
if args.config_file is None:
config = {"general": {"debug": False}}
else:
config_file = os.path.join(base_path, CONFIG_NAME)
if not re.search(r'^(\.|\/)?/', config_file):
config_file = os.path.join(base_path,args.config_file)
else:
config_file = args.config_file

config = load_yaml_config(config_file)
config = load_yaml_config(config_file)

if args.verbose is None:
if 'debug' in config['general']:
Expand Down Expand Up @@ -192,7 +197,7 @@ def main():
if 'export_suffix' not in config['general'] or config['general']['export_suffix'] is None:
config['general']['export_suffix'] = "_%Y%m%d%H%M%S"

params = grafana_settings(config=config, label=args.grafana_label)
params = grafana_settings(url=args.grafana_url, config=config, label=args.grafana_label)
params.update({
'overwrite': args.overwrite,
'allow_new': args.allow_new,
Expand Down
41 changes: 22 additions & 19 deletions grafana_import/grafana.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,29 @@ class Grafana(object):
dashboards: t.List[t.Any] = []

#***********************************************
def __init__( *args, **kwargs ):
self = args[0]
def __init__(self, **kwargs ):

config = { }
config['protocol'] = kwargs.get('protocol', 'http')
config['host'] = kwargs.get('host', 'localhost')
config['port'] = kwargs.get('port', 3000)
config['token'] = kwargs.get('token', None)
if config['token'] is None:
raise GrafanaClient.GrafanaBadInputError('grafana token is not defined')

config['verify_ssl'] = kwargs.get('verify_ssl', True)
# Configure Grafana connectivity.
if "url" in kwargs:
self.grafana_api = GrafanaApi.GrafanaApi.from_url(kwargs["url"])
else:
config = {}
config['protocol'] = kwargs.get('protocol', 'http')
config['host'] = kwargs.get('host', 'localhost')
config['port'] = kwargs.get('port', 3000)
config['token'] = kwargs.get('token', None)
if config['token'] is None:
raise GrafanaClient.GrafanaBadInputError('Grafana authentication token missing')

config['verify_ssl'] = kwargs.get('verify_ssl', True)

self.grafana_api = GrafanaApi.GrafanaApi(
auth=config['token'],
host=config['host'],
protocol=config['protocol'],
port=config['port'],
verify=config['verify_ssl'],
)

self.search_api_limit = kwargs.get('search_api_limit', 5000)
#* set the default destination folder for dash
Expand All @@ -90,14 +101,6 @@ def __init__( *args, **kwargs ):
#* allow to create new dashboard with same name in specified folder.
self.allow_new = kwargs.get('allow_new', False)

#* build an aapi object
self.grafana_api = GrafanaApi.GrafanaApi(
auth=config['token'],
host=config['host'],
protocol=config['protocol'],
port=config['port'],
verify=config['verify_ssl'],
)
#* try to connect to the API
try:
res = self.grafana_api.health.check()
Expand Down
37 changes: 31 additions & 6 deletions grafana_import/util.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import typing as t

import yaml

ConfigType = t.Dict[str, t.Any]
SettingsType = t.Dict[str, t.Union[str, int, bool]]


def load_yaml_config(config_file: str) -> ConfigType:
Expand All @@ -26,23 +28,47 @@ def load_yaml_config(config_file: str) -> ConfigType:
raise ValueError(f"Reading configuration file failed: {ex}") from ex


def grafana_settings(config: ConfigType, label: str) -> t.Dict[str, t.Union[str, int, bool]]:
def grafana_settings(
url: t.Union[str, None],
config: t.Union[ConfigType, None],
label: t.Union[str, None]) -> SettingsType:
"""
Acquire Grafana connection profile settings, and application settings.
"""

params: SettingsType

# Grafana connectivity.
if url or "GRAFANA_URL" in os.environ:
params = {"url": url or os.environ["GRAFANA_URL"]}
elif config is not None:
params = grafana_settings_from_config_section(config=config, label=label)

# Additional application parameters.
params.update({
"search_api_limit": config.get("grafana", {}).get("search_api_limit", 5000),
"folder": config.get("general", {}).get("grafana_folder", "General"),
})
return params


def grafana_settings_from_config_section(config: ConfigType, label: t.Union[str, None]) -> SettingsType:
"""
Extract Grafana connection profile from configuration dictionary, by label.
The configuration contains multiple connection profiles within the `grafana`
section. In order to address a specific profile, this function accepts a
`label` string.
"""
if not label or label not in config["grafana"]:
if not label or not config.get("grafana", {}).get(label):
raise ValueError(f"Invalid Grafana configuration label: {label}")

# ** init default conf from grafana with set label.
# Initialize default configuration from Grafana by label.
# FIXME: That is certainly a code smell.
# Q: Has it been introduced later in order to support multiple connection profiles?
# Q: Is it needed to update the original `config` dict, or can it just be omitted?
config["grafana"] = config["grafana"][label]

# ************
if "token" not in config["grafana"]:
raise ValueError(f"Authentication token missing in Grafana configuration at: {label}")

Expand All @@ -52,7 +78,6 @@ def grafana_settings(config: ConfigType, label: str) -> t.Dict[str, t.Union[str,
"port": config["grafana"].get("port", "3000"),
"token": config["grafana"].get("token"),
"verify_ssl": config["grafana"].get("verify_ssl", True),
"search_api_limit": config["grafana"].get("search_api_limit", 5000),
"folder": config["general"].get("grafana_folder", "General"),
}

return params
22 changes: 19 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import typing as t
from importlib.resources import files

import pytest
Expand Down Expand Up @@ -27,6 +29,15 @@ def niquests_patch_all():
modules["requests.packages.urllib3"] = urllib3


@pytest.fixture(scope="session", autouse=True)
def reset_environment():
"""
Make sure relevant environment variables do not leak into the test suite.
"""
if "GRAFANA_URL" in os.environ:
del os.environ["GRAFANA_URL"]


@pytest.fixture
def mocked_responses():
"""
Expand All @@ -50,7 +61,7 @@ def config():

@pytest.fixture
def settings(config):
return grafana_settings(config, label="default")
return grafana_settings(url=None, config=config, label="default")


@pytest.fixture(autouse=True)
Expand All @@ -60,5 +71,10 @@ def reset_grafana_importer():


@pytest.fixture
def gio(settings) -> Grafana:
return Grafana(**settings)
def gio_factory(settings) -> t.Callable:
def mkgrafana(use_settings: bool = True) -> Grafana:
if use_settings:
return Grafana(**settings)
else:
return Grafana(url="http://localhost:3000")
return mkgrafana
Loading

0 comments on commit 4c17003

Please sign in to comment.