Skip to content

Commit f7d8294

Browse files
authored
feat: Add ZABBIX_URL, prioritize env over config. (#277)
1 parent 0ab13f9 commit f7d8294

File tree

11 files changed

+332
-121
lines changed

11 files changed

+332
-121
lines changed

Diff for: CHANGELOG

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
<!-- released start -->
99

10-
<!-- ## [Unreleased] -->
10+
## [Unreleased]
11+
12+
### Added
13+
14+
- Environment variable `ZABBIX_URL` to specify the URL for the Zabbix API.
15+
16+
### Changed
17+
18+
- Authentication info from environment variables now take priority over the configuration file.
1119

1220
## [3.4.2](https://github.com/unioslo/zabbix-cli/releases/tag/3.4.2) - 2024-12-16
1321

Diff for: docs/guide/authentication.md

+52-18
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
Zabbix-cli provides several ways to authenticate. They are tried in the following order:
44

5-
1. [Token - Config file](#config-file)
65
1. [Token - Environment variables](#environment-variables)
6+
1. [Token - Config file](#config-file)
77
1. [Token - Auth token file](#auth-token-file)
8+
1. [Password - Environment variables](#environment-variables_1)
89
1. [Password - Config file](#config-file_1)
910
1. [Password - Auth file](#auth-file)
10-
1. [Password - Environment variables](#environment-variables_1)
1111
1. [Password - Prompt](#prompt)
1212

1313
## Token
@@ -17,6 +17,14 @@ The application supports authenticating with an API or session token. API tokens
1717
!!! info "Session vs API token"
1818
Semantically, a session token and API token are the same thing from an API authentication perspective. They are both sent as the `auth` parameter in the Zabbix API requests.
1919

20+
### Environment variables
21+
22+
The API token can be set as an environment variable:
23+
24+
```bash
25+
export ZABBIX_API_TOKEN="API TOKEN"
26+
```
27+
2028
### Config file
2129

2230
The token can be set directly in the config file:
@@ -26,14 +34,6 @@ The token can be set directly in the config file:
2634
auth_token = "API_TOKEN"
2735
```
2836

29-
### Environment variables
30-
31-
The API token can be set as an environment variable:
32-
33-
```bash
34-
export ZABBIX_API_TOKEN="API TOKEN"
35-
```
36-
3737
### Auth token file
3838

3939
The application can store and reuse session tokens between runs. This feature is enabled by default and configurable via the following options:
@@ -62,6 +62,15 @@ When `allow_insecure_auth_file` is set to `false`, the application will attempt
6262

6363
The application supports authenticating with a username and password. The password can be set in the config file, an auth file, as environment variables, or prompted for when starting the application.
6464

65+
### Environment variables
66+
67+
The username and password can be set as environment variables:
68+
69+
```bash
70+
export ZABBIX_USERNAME="Admin"
71+
export ZABBIX_PASSWORD="zabbix"
72+
```
73+
6574
### Config file
6675

6776
The password can be set directly in the config file:
@@ -87,20 +96,45 @@ The location of the auth file file can be changed in the config file:
8796
auth_file = "~/.zabbix-cli_auth"
8897
```
8998

90-
### Environment variables
99+
### Prompt
91100

92-
The username and password can be set as environment variables:
101+
When all other authentication methods fail, the application will prompt for a username and password. The default username in the prompt can be configured:
93102

94-
```bash
95-
export ZABBIX_USERNAME="Admin"
96-
export ZABBIX_PASSWORD="zabbix"
103+
```toml
104+
[api]
105+
username = "Admin"
97106
```
98107

99-
### Prompt
108+
## URL
100109

101-
When all other authentication methods fail, the application will prompt for a username and password. The default username in the prompt can be configured:
110+
The URL of the Zabbix API can be set in the config file, as an environment variable, or prompted for when starting the application.
111+
112+
They are processed in the following order:
113+
114+
1. [Environment variables](#environment-variables_2)
115+
1. [Config file](#config-file_2)
116+
1. [Prompt](#prompt_1)
117+
118+
The URL should not include `/api_jsonrpc.php`.
119+
120+
### Config file
121+
122+
The URL of the Zabbix API can be set in the config file:
102123

103124
```toml
125+
104126
[api]
105-
username = "Admin"
127+
url = "http://zabbix.example.com"
128+
```
129+
130+
### Environment variables
131+
132+
The URL can also be set as an environment variable:
133+
134+
```bash
135+
export ZABBIX_URL="http://zabbix.example.com"
106136
```
137+
138+
### Prompt
139+
140+
When all other methods fail, the application will prompt for the URL of the Zabbix API.

Diff for: pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ test = [
6161
"pytest-cov>=5.0.0",
6262
"inline-snapshot>=0.13.0",
6363
"freezegun>=1.5.1",
64+
"pytest-httpserver>=1.1.0",
6465
]
6566
docs = [
6667
"mkdocs>=1.5.3",

Diff for: tests/conftest.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
import typer
1010
from packaging.version import Version
11+
from pytest_httpserver import HTTPServer
1112
from typer.testing import CliRunner
1213
from zabbix_cli.app import StatefulApp
1314
from zabbix_cli.config.model import Config
@@ -98,14 +99,30 @@ def config(tmp_path: Path) -> Iterator[Config]:
9899

99100

100101
@pytest.fixture(name="zabbix_client")
101-
def zabbix_client(monkeypatch: pytest.MonkeyPatch) -> Iterator[ZabbixAPI]:
102+
def zabbix_client() -> Iterator[ZabbixAPI]:
102103
config = Config.sample_config()
103104
client = ZabbixAPI.from_config(config)
105+
yield client
104106

105-
# Patch the version check
106-
monkeypatch.setattr(client, "api_version", lambda: Version("7.0.0"))
107107

108-
yield client
108+
@pytest.fixture(name="zabbix_client_mock_version")
109+
def zabbix_client_mock_version(
110+
zabbix_client: ZabbixAPI, monkeypatch: pytest.MonkeyPatch
111+
) -> Iterator[ZabbixAPI]:
112+
monkeypatch.setattr(zabbix_client, "api_version", lambda: Version("7.0.0"))
113+
yield zabbix_client
114+
115+
116+
def add_httpserver_version_endpoint(
117+
httpserver: HTTPServer, version: Version, id: int = 0
118+
) -> None:
119+
"""Add an endpoint emulating the Zabbix apiiinfo.version method."""
120+
httpserver.expect_oneshot_request(
121+
"/api_jsonrpc.php",
122+
json={"jsonrpc": "2.0", "method": "apiinfo.version", "params": {}, "id": id},
123+
method="POST",
124+
headers={"Content-Type": "application/json-rpc"},
125+
).respond_with_json({"jsonrpc": "2.0", "result": str(version), "id": id})
109126

110127

111128
@pytest.fixture(name="force_color")

Diff for: tests/pyzabbix/test_client.py

+119-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
from __future__ import annotations
22

33
from typing import Any
4+
from typing import Literal
45

56
import pytest
67
from inline_snapshot import snapshot
8+
from packaging.version import Version
9+
from pytest_httpserver import HTTPServer
710
from zabbix_cli.exceptions import ZabbixAPILoginError
811
from zabbix_cli.exceptions import ZabbixAPILogoutError
912
from zabbix_cli.pyzabbix.client import ZabbixAPI
1013
from zabbix_cli.pyzabbix.client import add_param
1114
from zabbix_cli.pyzabbix.client import append_param
1215

16+
from tests.conftest import add_httpserver_version_endpoint
17+
1318

1419
@pytest.mark.parametrize(
1520
"inp, key, value, expect",
@@ -75,14 +80,13 @@ def test_add_param(inp: Any, subkey: str, value: Any, expect: dict[str, Any]) ->
7580
# Check in-place modification
7681

7782

78-
def test_login_fails(zabbix_client: ZabbixAPI) -> None:
79-
zabbix_client.set_url("http://some-url-that-will-fail.gg")
80-
assert zabbix_client.url == snapshot(
81-
"http://some-url-that-will-fail.gg/api_jsonrpc.php"
82-
)
83+
def test_login_fails(zabbix_client_mock_version: ZabbixAPI) -> None:
84+
client = zabbix_client_mock_version
85+
client.set_url("http://some-url-that-will-fail.gg")
86+
assert client.url == snapshot("http://some-url-that-will-fail.gg/api_jsonrpc.php")
8387

8488
with pytest.raises(ZabbixAPILoginError) as exc_info:
85-
zabbix_client.login(user="username", password="password")
89+
client.login(user="username", password="password")
8690

8791
assert exc_info.exconly() == snapshot(
8892
"zabbix_cli.exceptions.ZabbixAPILoginError: Failed to log in to Zabbix: Failed to send request to http://some-url-that-will-fail.gg/api_jsonrpc.php (user.login) with params {'username': 'username', 'password': 'password'}"
@@ -92,19 +96,121 @@ def test_login_fails(zabbix_client: ZabbixAPI) -> None:
9296
)
9397

9498

95-
def test_logout_fails(zabbix_client: ZabbixAPI) -> None:
99+
def test_logout_fails(zabbix_client_mock_version: ZabbixAPI) -> None:
96100
"""Test that we get the correct exception type when login fails
97101
due to a connection error."""
98-
zabbix_client.set_url("http://some-url-that-will-fail.gg")
99-
assert zabbix_client.url == snapshot(
100-
"http://some-url-that-will-fail.gg/api_jsonrpc.php"
101-
)
102+
client = zabbix_client_mock_version
103+
client.set_url("http://some-url-that-will-fail.gg")
104+
assert client.url == snapshot("http://some-url-that-will-fail.gg/api_jsonrpc.php")
102105

103-
zabbix_client.auth = "authtoken123456789"
106+
client.auth = "authtoken123456789"
104107

105108
with pytest.raises(ZabbixAPILogoutError) as exc_info:
106-
zabbix_client.logout()
109+
client.logout()
107110

108111
assert exc_info.exconly() == snapshot(
109112
"zabbix_cli.exceptions.ZabbixAPILogoutError: Failed to log out of Zabbix: Failed to send request to http://some-url-that-will-fail.gg/api_jsonrpc.php (user.logout) with params {}"
110113
)
114+
115+
116+
@pytest.mark.parametrize(
117+
"inp, expect",
118+
[
119+
pytest.param(
120+
"http://localhost",
121+
"http://localhost/api_jsonrpc.php",
122+
id="localhost-no-slash",
123+
),
124+
pytest.param(
125+
"http://localhost/",
126+
"http://localhost/api_jsonrpc.php",
127+
id="localhost-with-slash",
128+
),
129+
pytest.param(
130+
"http://localhost/api_jsonrpc.php",
131+
"http://localhost/api_jsonrpc.php",
132+
id="localhost-full-url",
133+
),
134+
pytest.param(
135+
"http://localhost/api_jsonrpc.php/",
136+
"http://localhost/api_jsonrpc.php",
137+
id="localhost-full-url-with-slash",
138+
),
139+
pytest.param(
140+
"http://example.com",
141+
"http://example.com/api_jsonrpc.php",
142+
id="tld-no-slash",
143+
),
144+
pytest.param(
145+
"http://example.com/",
146+
"http://example.com/api_jsonrpc.php",
147+
id="tld-with-slash",
148+
),
149+
pytest.param(
150+
"http://example.com/api_jsonrpc.php",
151+
"http://example.com/api_jsonrpc.php",
152+
id="tld-full-url",
153+
),
154+
pytest.param(
155+
"http://example.com/api_jsonrpc.php/",
156+
"http://example.com/api_jsonrpc.php",
157+
id="tld-full-url-with-slash",
158+
),
159+
],
160+
)
161+
def test_client_server_url(inp: str, expect: str) -> None:
162+
zabbix_client = ZabbixAPI(server=inp)
163+
assert zabbix_client.url == expect
164+
165+
166+
AuthMethod = Literal["header", "body"]
167+
168+
169+
@pytest.mark.parametrize(
170+
"version,expect_method",
171+
[
172+
pytest.param(Version("5.0.0"), "body", id="5.0.0"),
173+
pytest.param(Version("5.2.0"), "body", id="5.2.0"),
174+
pytest.param(Version("6.0.0"), "body", id="6.0.0"),
175+
pytest.param(Version("6.2.0"), "body", id="6.2.0"),
176+
pytest.param(Version("6.4.0"), "header", id="6.4.0"),
177+
pytest.param(Version("7.0.0"), "header", id="7.0.0"),
178+
pytest.param(Version("7.2.0"), "header", id="7.2.0"),
179+
],
180+
)
181+
def test_client_auth_method(
182+
zabbix_client: ZabbixAPI,
183+
httpserver: HTTPServer,
184+
version: Version,
185+
expect_method: AuthMethod,
186+
) -> None:
187+
# Add endpoint for version check
188+
zabbix_client.set_url(httpserver.url_for("/api_jsonrpc.php"))
189+
190+
# Set a mock token we can use for testing
191+
zabbix_client.auth = "token123"
192+
193+
# Add endpoint that returns the parametrized version
194+
add_httpserver_version_endpoint(httpserver, version, id=0)
195+
196+
assert zabbix_client.version == version
197+
198+
data = {"jsonrpc": "2.0", "method": "fake.method", "params": {}, "id": 1}
199+
headers = {}
200+
201+
# We expect auth token to be in header on >= 6.4.0
202+
if expect_method == "header":
203+
headers["Authorization"] = f"Bearer {zabbix_client.auth}"
204+
else:
205+
data["auth"] = zabbix_client.auth
206+
207+
httpserver.expect_oneshot_request(
208+
"/api_jsonrpc.php",
209+
json=data,
210+
headers=headers,
211+
method="POST",
212+
).respond_with_json({"jsonrpc": "2.0", "result": "authtoken123456789", "id": 1})
213+
214+
# Will fail if the auth method is not set correctly
215+
resp = zabbix_client.do_request("fake.method")
216+
assert resp.result == "authtoken123456789"

0 commit comments

Comments
 (0)