Skip to content

Commit f4eb81a

Browse files
authored
Merge pull request #144 from slauger/feature/mock-nitro-api
Add Mock NITRO API Server for Integration Testing
2 parents 805a982 + c98aedc commit f4eb81a

55 files changed

Lines changed: 3110 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lint-and-type-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
run: |
3737
python -m pip install --upgrade pip
3838
pip install -e .
39-
pip install ruff black mypy types-requests pytest
39+
pip install ruff black mypy types-requests pytest flask
4040
4141
- name: Run ruff (linting)
4242
run: |
@@ -60,7 +60,7 @@ jobs:
6060
- name: Run tests
6161
run: |
6262
echo "Running pytest..."
63-
pytest tests/ -v --tb=short
63+
pytest tests/ -vv --tb=short
6464
continue-on-error: false
6565

6666
summary:

.github/workflows/pyinstaller.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: |
3333
python -m pip install --upgrade pip
3434
pip install -e .
35-
pip install ruff black mypy types-requests pytest
35+
pip install ruff black mypy types-requests pytest flask
3636
3737
- name: Run ruff (linting)
3838
run: |
@@ -55,7 +55,7 @@ jobs:
5555
- name: Run tests
5656
run: |
5757
echo "Running pytest..."
58-
pytest tests/ -v --tb=short
58+
pytest tests/ -vv --tb=short
5959
continue-on-error: false
6060

6161
build:

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
run: |
4545
python -m pip install --upgrade pip
4646
pip install -e .
47-
pip install ruff black mypy types-requests pytest
47+
pip install ruff black mypy types-requests pytest flask
4848
4949
- name: Run ruff (linting)
5050
run: |
@@ -67,7 +67,7 @@ jobs:
6767
- name: Run tests
6868
run: |
6969
echo "Running pytest..."
70-
pytest tests/ -v --tb=short
70+
pytest tests/ -vv --tb=short
7171
continue-on-error: false
7272

7373
- name: Test CLI

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ pip install .
6969
pip install -e ".[dev]"
7070
```
7171

72+
### Pre-built Binaries (Standalone)
73+
74+
For systems without Python or pip, download standalone executables from the [Releases page](https://github.com/slauger/check_netscaler/releases):
75+
76+
- **Linux**: `check_netscaler-linux`
77+
- **macOS**: `check_netscaler-macos`
78+
- **Windows**: `check_netscaler-windows.exe`
79+
80+
These binaries are built with PyInstaller and include all dependencies. No Python installation required.
81+
82+
```bash
83+
# Linux/macOS example
84+
chmod +x check_netscaler-linux
85+
./check_netscaler-linux --help
86+
87+
# Windows example
88+
check_netscaler-windows.exe --help
89+
```
90+
7291
## Quick Start
7392

7493
```bash

check_netscaler/commands/license.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def _get_license_files(self) -> List[str]:
120120

121121
# Get all .lic files from /nsconfig/license
122122
try:
123-
data = self.client.get_config("systemfile", args="filelocation:/nsconfig/license")
123+
data = self.client.get_config(
124+
"systemfile", url_options="args=filelocation:/nsconfig/license"
125+
)
124126

125127
if "systemfile" not in data:
126128
return []
@@ -151,7 +153,8 @@ def _check_license_file(self, filename: str, warning_days: int, critical_days: i
151153
try:
152154
# Get license file content
153155
data = self.client.get_config(
154-
"systemfile", args=f"filelocation:/nsconfig/license,filename:{filename}"
156+
"systemfile",
157+
url_options=f"args=filelocation:/nsconfig/license,filename:{filename}",
155158
)
156159

157160
if "systemfile" not in data:

check_netscaler/commands/ntp.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -230,65 +230,68 @@ def _check_thresholds(self, ntp_info: Dict, thresholds: Dict) -> tuple:
230230
offset = float(ntp_info["synced_offset"])
231231
offset_text = f"Offset {ntp_info['synced_offset']} secs"
232232

233-
if thresholds["offset_critical"] is not None:
234-
if abs(offset) >= thresholds["offset_critical"]:
235-
worst_status = STATE_CRITICAL
236-
offset_text += " (CRITICAL)"
237-
elif thresholds["offset_warning"] is not None:
238-
if abs(offset) >= thresholds["offset_warning"]:
239-
worst_status = STATE_WARNING
240-
offset_text += " (WARNING)"
233+
if (
234+
thresholds["offset_critical"] is not None
235+
and abs(offset) >= thresholds["offset_critical"]
236+
):
237+
worst_status = STATE_CRITICAL
238+
offset_text += " (CRITICAL)"
239+
elif (
240+
thresholds["offset_warning"] is not None and abs(offset) >= thresholds["offset_warning"]
241+
):
242+
worst_status = STATE_WARNING
243+
offset_text += " (WARNING)"
241244

242245
messages.append(offset_text)
243246

244247
# Check jitter
245248
jitter = ntp_info["synced_jitter"]
246249
jitter_text = f"jitter={jitter}"
247250

248-
if thresholds["jitter_critical"] is not None:
249-
if jitter >= thresholds["jitter_critical"]:
250-
if worst_status != STATE_CRITICAL:
251-
worst_status = STATE_CRITICAL
252-
jitter_text += " (CRITICAL)"
253-
elif thresholds["jitter_warning"] is not None:
254-
if jitter >= thresholds["jitter_warning"]:
255-
if worst_status == STATE_OK:
256-
worst_status = STATE_WARNING
257-
jitter_text += " (WARNING)"
251+
if thresholds["jitter_critical"] is not None and jitter >= thresholds["jitter_critical"]:
252+
if worst_status != STATE_CRITICAL:
253+
worst_status = STATE_CRITICAL
254+
jitter_text += " (CRITICAL)"
255+
elif thresholds["jitter_warning"] is not None and jitter >= thresholds["jitter_warning"]:
256+
if worst_status == STATE_OK:
257+
worst_status = STATE_WARNING
258+
jitter_text += " (WARNING)"
258259

259260
messages.append(jitter_text)
260261

261262
# Check stratum (higher is worse)
262263
stratum = ntp_info["synced_stratum"]
263264
stratum_text = f"stratum={stratum}"
264265

265-
if thresholds["stratum_critical"] is not None:
266-
if stratum > thresholds["stratum_critical"]:
267-
if worst_status != STATE_CRITICAL:
268-
worst_status = STATE_CRITICAL
269-
stratum_text += " (CRITICAL)"
270-
elif thresholds["stratum_warning"] is not None:
271-
if stratum > thresholds["stratum_warning"]:
272-
if worst_status == STATE_OK:
273-
worst_status = STATE_WARNING
274-
stratum_text += " (WARNING)"
266+
if thresholds["stratum_critical"] is not None and stratum > thresholds["stratum_critical"]:
267+
if worst_status != STATE_CRITICAL:
268+
worst_status = STATE_CRITICAL
269+
stratum_text += " (CRITICAL)"
270+
elif thresholds["stratum_warning"] is not None and stratum > thresholds["stratum_warning"]:
271+
if worst_status == STATE_OK:
272+
worst_status = STATE_WARNING
273+
stratum_text += " (WARNING)"
275274

276275
messages.append(stratum_text)
277276

278277
# Check truechimers (lower is worse)
279278
truechimers = ntp_info["truechimers"]
280279
truechimers_text = f"truechimers={truechimers}"
281280

282-
if thresholds["truechimers_critical"] is not None:
283-
if truechimers <= thresholds["truechimers_critical"]:
284-
if worst_status != STATE_CRITICAL:
285-
worst_status = STATE_CRITICAL
286-
truechimers_text += " (CRITICAL)"
287-
elif thresholds["truechimers_warning"] is not None:
288-
if truechimers <= thresholds["truechimers_warning"]:
289-
if worst_status == STATE_OK:
290-
worst_status = STATE_WARNING
291-
truechimers_text += " (WARNING)"
281+
if (
282+
thresholds["truechimers_critical"] is not None
283+
and truechimers <= thresholds["truechimers_critical"]
284+
):
285+
if worst_status != STATE_CRITICAL:
286+
worst_status = STATE_CRITICAL
287+
truechimers_text += " (CRITICAL)"
288+
elif (
289+
thresholds["truechimers_warning"] is not None
290+
and truechimers <= thresholds["truechimers_warning"]
291+
):
292+
if worst_status == STATE_OK:
293+
worst_status = STATE_WARNING
294+
truechimers_text += " (WARNING)"
292295

293296
messages.append(truechimers_text)
294297

check_netscaler/commands/sslcert.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def execute(self) -> CheckResult:
5151

5252
# Get SSL certificates
5353
objecttype = getattr(self.args, "objecttype", None) or "sslcertkey"
54-
data = self.client.get_config(objecttype)
54+
objectname = getattr(self.args, "objectname", None)
55+
data = self.client.get_config(objecttype, objectname)
5556

5657
if objecttype not in data:
5758
return CheckResult(

docs/commands/sslcert.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ check_netscaler -C sslcert --limit "^test-"
6262

6363
### Check single certificate
6464

65+
Use `--objectname` to check a specific certificate by its exact name:
66+
67+
```bash
68+
check_netscaler -C sslcert -n wildcard.example.com
69+
```
70+
71+
Or use `--filter` with a regex pattern:
72+
6573
```bash
6674
check_netscaler -C sslcert --filter "^wildcard.example.com$"
6775
```

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dev = [
3939
"ruff>=0.0.285",
4040
"mypy>=1.5.0",
4141
"types-requests>=2.31.0",
42+
"flask>=2.3.0",
4243
]
4344

4445
[project.scripts]

tests/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Pytest configuration and shared fixtures
33
"""
44

5+
import time
6+
57
import pytest
68

79

@@ -15,3 +17,34 @@ def sample_args():
1517
def sample_args_with_auth():
1618
"""Sample CLI arguments with authentication"""
1719
return ["-H", "192.168.1.1", "-C", "state", "-u", "admin", "-p", "secret", "-s"]
20+
21+
22+
@pytest.fixture
23+
def mock_nitro_server():
24+
"""
25+
Pytest fixture for Mock NITRO API Server
26+
27+
Usage:
28+
def test_something(mock_nitro_server):
29+
url = mock_nitro_server.get_url()
30+
# Test against mock server...
31+
"""
32+
import socket
33+
34+
from tests.mocks.nitro_server import MockNITROServer
35+
36+
# Find a free port
37+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
38+
s.bind(("", 0))
39+
s.listen(1)
40+
port = s.getsockname()[1]
41+
42+
server = MockNITROServer(port=port)
43+
server.start(threaded=True)
44+
45+
# Give server time to start
46+
time.sleep(1)
47+
48+
yield server
49+
50+
server.stop()

0 commit comments

Comments
 (0)