netdoc-sdk 0.2.4__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/workflows/ci.yml +7 -10
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.pre-commit-config.yaml +1 -0
- netdoc_sdk-0.3.0/.release-please-manifest.json +3 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/CHANGELOG.md +14 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/PKG-INFO +1 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/poetry.lock +35 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/pyproject.toml +4 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/client.py +117 -22
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/core.py +9 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/discovery.py +54 -6
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/inventory.py +1 -1
- netdoc_sdk-0.3.0/tests/integration/core/test_001_filters.py +32 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_002_tenant.py +2 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_003_user.py +2 -1
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_003_discovery_run.py +6 -2
- netdoc_sdk-0.3.0/tests/integration/discovery/test_004_discovery_job.py +391 -0
- netdoc_sdk-0.2.4/.release-please-manifest.json +0 -3
- netdoc_sdk-0.2.4/tests/integration/discovery/test_004_discovery_job.py +0 -36
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.editorconfig +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/CODEOWNERS +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/workflows/cd.yml +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.gitignore +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.markdownlint.yaml +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.pylintrc +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.snyk +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.yamlfmt +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/CONTRIBUTING.md +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/LICENSE +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/Makefile +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/README.md +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/release-please-config.json +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/__init__.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/exceptions.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/__init__.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/snapshots.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/conftest.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_001_authentication.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_004_token.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_005_profile.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_006_audit_log.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_001_credentials.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_002_collector.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/inventory/test_001_site.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/inventory/test_002_canonical_device.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/conftest.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_001_normalize_base_url.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_002_headers.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_003_api_path.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_004_clean_params.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_005_serialize_body.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_006_context_manager.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_007_error_mapping.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_008_network_errors.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_009_query_params.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_010_response_parsing.py +0 -0
- {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_011_from_credentials.py +0 -0
|
@@ -12,11 +12,11 @@ jobs:
|
|
|
12
12
|
tests:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
|
-
- name: Checkout
|
|
15
|
+
- name: Checkout (SDK)
|
|
16
16
|
uses: actions/checkout@v6
|
|
17
17
|
with:
|
|
18
18
|
path: netdoc-sdk
|
|
19
|
-
- name: Checkout backend
|
|
19
|
+
- name: Checkout (backend)
|
|
20
20
|
uses: actions/checkout@v6
|
|
21
21
|
with:
|
|
22
22
|
repository: NetDocLab/netdoc
|
|
@@ -29,23 +29,20 @@ jobs:
|
|
|
29
29
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
30
30
|
- name: Install Poetry
|
|
31
31
|
run: pip install --no-cache-dir poetry
|
|
32
|
-
working-directory: netdoc-sdk
|
|
33
32
|
- name: Display Poetry version
|
|
34
33
|
run: poetry --version
|
|
35
|
-
working-directory: netdoc-sdk
|
|
36
34
|
- name: Configure Poetry
|
|
37
35
|
run: poetry config virtualenvs.in-project true
|
|
38
|
-
|
|
39
|
-
- name: Install
|
|
36
|
+
- name: Install SDK
|
|
40
37
|
run: make install
|
|
41
38
|
working-directory: netdoc-sdk
|
|
42
|
-
- name: Code linting (make lint)
|
|
39
|
+
- name: Code linting on SDK (make lint)
|
|
43
40
|
run: make lint
|
|
44
41
|
working-directory: netdoc-sdk
|
|
45
|
-
- name: Run checks (pre-commit)
|
|
42
|
+
- name: Run checks on SDK (pre-commit)
|
|
46
43
|
run: make check
|
|
47
44
|
working-directory: netdoc-sdk
|
|
48
|
-
- name: Run tests
|
|
45
|
+
- name: Run tests on SDK (make coverage)
|
|
49
46
|
run: make coverage
|
|
50
47
|
working-directory: netdoc-sdk
|
|
51
48
|
snyk:
|
|
@@ -59,4 +56,4 @@ jobs:
|
|
|
59
56
|
env:
|
|
60
57
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
|
61
58
|
with:
|
|
62
|
-
args: --severity-threshold=high
|
|
59
|
+
args: --severity-threshold=high --all-projects
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/NetDocLab/netdoc-sdk/compare/v0.2.5...v0.3.0) (2026-06-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* intercept 429 and retry ([06c2e59](https://github.com/NetDocLab/netdoc-sdk/commit/06c2e590203cee937247b0bbc068c4ff165c053e))
|
|
9
|
+
|
|
10
|
+
## [0.2.5](https://github.com/NetDocLab/netdoc-sdk/compare/v0.2.4...v0.2.5) (2026-06-01)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* add parsed_output_count on DiscoveryRun model ([d808f17](https://github.com/NetDocLab/netdoc-sdk/commit/d808f1732b445a47babf1a2af5d5e9c85bc1c1f8))
|
|
16
|
+
|
|
3
17
|
## [0.2.4](https://github.com/NetDocLab/netdoc-sdk/compare/v0.2.3...v0.2.4) (2026-05-30)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -2314,6 +2314,25 @@ files = [
|
|
|
2314
2314
|
[package.dependencies]
|
|
2315
2315
|
pytest = ">=7.0.0"
|
|
2316
2316
|
|
|
2317
|
+
[[package]]
|
|
2318
|
+
name = "pytest-env"
|
|
2319
|
+
version = "1.6.0"
|
|
2320
|
+
description = "pytest plugin that allows you to add environment variables."
|
|
2321
|
+
optional = false
|
|
2322
|
+
python-versions = ">=3.10"
|
|
2323
|
+
groups = ["dev"]
|
|
2324
|
+
files = [
|
|
2325
|
+
{file = "pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3"},
|
|
2326
|
+
{file = "pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3"},
|
|
2327
|
+
]
|
|
2328
|
+
|
|
2329
|
+
[package.dependencies]
|
|
2330
|
+
pytest = ">=9.0.2"
|
|
2331
|
+
python-dotenv = ">=1.2.2"
|
|
2332
|
+
|
|
2333
|
+
[package.extras]
|
|
2334
|
+
testing = ["covdefaults (>=2.3)", "coverage (>=7.13.4)", "pytest-mock (>=3.15.1)"]
|
|
2335
|
+
|
|
2317
2336
|
[[package]]
|
|
2318
2337
|
name = "python-dateutil"
|
|
2319
2338
|
version = "2.9.0.post0"
|
|
@@ -2361,6 +2380,21 @@ platformdirs = ">=4.3.6,<5"
|
|
|
2361
2380
|
docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"]
|
|
2362
2381
|
testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"]
|
|
2363
2382
|
|
|
2383
|
+
[[package]]
|
|
2384
|
+
name = "python-dotenv"
|
|
2385
|
+
version = "1.2.2"
|
|
2386
|
+
description = "Read key-value pairs from a .env file and set them as environment variables"
|
|
2387
|
+
optional = false
|
|
2388
|
+
python-versions = ">=3.10"
|
|
2389
|
+
groups = ["dev"]
|
|
2390
|
+
files = [
|
|
2391
|
+
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
|
2392
|
+
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
|
2393
|
+
]
|
|
2394
|
+
|
|
2395
|
+
[package.extras]
|
|
2396
|
+
cli = ["click (>=5.0)"]
|
|
2397
|
+
|
|
2364
2398
|
[[package]]
|
|
2365
2399
|
name = "pyyaml"
|
|
2366
2400
|
version = "6.0.3"
|
|
@@ -2972,4 +3006,4 @@ brotli = ["brotli"]
|
|
|
2972
3006
|
[metadata]
|
|
2973
3007
|
lock-version = "2.1"
|
|
2974
3008
|
python-versions = ">=3.12,<3.14"
|
|
2975
|
-
content-hash = "
|
|
3009
|
+
content-hash = "ca847814e3a43c2471a8930a695b0b1032e13d2dc31f301988722148b7a8e4f3"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "netdoc-sdk"
|
|
3
3
|
description = "Network Documentation Platform - SDK"
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.3.0"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andrea Cavazzini", email = "cavazzini.andrea@gmail.com"},
|
|
7
7
|
{name = "Andrea Dainese", email = "andrea.dainese@gmail.com"},
|
|
@@ -58,6 +58,7 @@ pre-commit = "^4.5.1"
|
|
|
58
58
|
pytest = ">=7.4"
|
|
59
59
|
pytest-asyncio = "^1.3.0"
|
|
60
60
|
pytest-cov = ">=4.1"
|
|
61
|
+
pytest-env = "^1.6.0"
|
|
61
62
|
responses = "^0.26.0"
|
|
62
63
|
respx = "^0.23.1"
|
|
63
64
|
|
|
@@ -149,6 +150,7 @@ typeCheckingMode = "off"
|
|
|
149
150
|
|
|
150
151
|
[tool.pytest.ini_options]
|
|
151
152
|
DJANGO_SETTINGS_MODULE = "netdoc.settings.test"
|
|
153
|
+
env = ["DJANGO_ENV=test"]
|
|
152
154
|
minversion = "7.0"
|
|
153
155
|
addopts = "-v -ra --strict-markers --strict-config --cov=netdoc_sdk --cov-report=term-missing"
|
|
154
156
|
filterwarnings = [
|
|
@@ -178,6 +180,7 @@ exclude = [
|
|
|
178
180
|
"dist",
|
|
179
181
|
"env",
|
|
180
182
|
"migrations",
|
|
183
|
+
"scripts",
|
|
181
184
|
"venv",
|
|
182
185
|
]
|
|
183
186
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Async HTTP client for the NetDoc OpenAPI surface."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
3
5
|
from collections.abc import Iterable, Mapping
|
|
4
6
|
from typing import Any
|
|
5
7
|
|
|
@@ -39,7 +41,10 @@ from netdoc_sdk.models.discovery import (
|
|
|
39
41
|
CredentialCreate,
|
|
40
42
|
CredentialDetail,
|
|
41
43
|
CredentialUpdate,
|
|
44
|
+
DiscoveredDeviceSubmit,
|
|
42
45
|
DiscoveryJobClaim,
|
|
46
|
+
DiscoveryJobDetail,
|
|
47
|
+
DiscoveryJobStatusEnum,
|
|
43
48
|
DiscoveryRunDetail,
|
|
44
49
|
PaginatedCollectorList,
|
|
45
50
|
PaginatedCredentialList,
|
|
@@ -79,6 +84,7 @@ class NetDocClient:
|
|
|
79
84
|
cookies: Mapping[str, str] | None = None,
|
|
80
85
|
headers: Mapping[str, str] | None = None,
|
|
81
86
|
tenant_id: str | None = None,
|
|
87
|
+
max_retries: int = 5,
|
|
82
88
|
timeout: float = 30.0,
|
|
83
89
|
transport: httpx.AsyncBaseTransport | None = None,
|
|
84
90
|
):
|
|
@@ -88,6 +94,7 @@ class NetDocClient:
|
|
|
88
94
|
self.cookies = dict(cookies or {})
|
|
89
95
|
self.extra_headers = dict(headers or {})
|
|
90
96
|
self.tenant_id = tenant_id
|
|
97
|
+
self.max_retries = max_retries
|
|
91
98
|
self.timeout = timeout
|
|
92
99
|
self.token = token
|
|
93
100
|
self.transport = transport
|
|
@@ -196,25 +203,52 @@ class NetDocClient:
|
|
|
196
203
|
{expected_status} if isinstance(expected_status, int) else set(expected_status)
|
|
197
204
|
)
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
206
|
+
for attempt in range(self.max_retries + 1):
|
|
207
|
+
try:
|
|
208
|
+
response = await self.client.request(
|
|
209
|
+
content=content,
|
|
210
|
+
headers=request_headers,
|
|
211
|
+
json=self._serialize_body(json) if json is not None else None,
|
|
212
|
+
method=method,
|
|
213
|
+
params=self._clean_params(params),
|
|
214
|
+
url=self._api_path(path),
|
|
215
|
+
)
|
|
216
|
+
except (httpx.ConnectError, httpx.NetworkError) as exc:
|
|
217
|
+
raise ConnectionError(f'Failed to connect: {exc}') from exc
|
|
218
|
+
except httpx.TimeoutException as exc:
|
|
219
|
+
raise ConnectionError(f'Request timed out: {exc}') from exc
|
|
220
|
+
|
|
221
|
+
if expected is None:
|
|
222
|
+
ok = 200 <= response.status_code < 300
|
|
223
|
+
else:
|
|
224
|
+
ok = response.status_code in expected
|
|
225
|
+
|
|
226
|
+
if ok:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if response.status_code == 429 and attempt < self.max_retries:
|
|
230
|
+
# Retry with rate limit
|
|
231
|
+
default_retry_after = '1.0'
|
|
232
|
+
retry_after = response.headers.get('Retry-After')
|
|
233
|
+
if not retry_after:
|
|
234
|
+
logging.warning(f'Retry value not set, using {default_retry_after}')
|
|
235
|
+
retry_after = default_retry_after
|
|
236
|
+
try:
|
|
237
|
+
wait = float(retry_after)
|
|
238
|
+
except (ValueError, TypeError):
|
|
239
|
+
logging.warning(
|
|
240
|
+
f'Retry value of {retry_after} is not valid, using {default_retry_after}'
|
|
241
|
+
)
|
|
242
|
+
wait = float(default_retry_after)
|
|
243
|
+
logging.warning(
|
|
244
|
+
'Rate limited (attempt %d/%d), retrying in %.1fs',
|
|
245
|
+
attempt + 1,
|
|
246
|
+
self.max_retries,
|
|
247
|
+
wait,
|
|
248
|
+
)
|
|
249
|
+
await asyncio.sleep(wait)
|
|
250
|
+
continue
|
|
251
|
+
|
|
218
252
|
self._raise_for_error(response)
|
|
219
253
|
|
|
220
254
|
if response.status_code == 204 or not response.content:
|
|
@@ -457,11 +491,72 @@ class NetDocClient:
|
|
|
457
491
|
'POST', 'discovery-jobs/claim/', response_model=DiscoveryJobClaim
|
|
458
492
|
)
|
|
459
493
|
|
|
494
|
+
async def discoveryjob_complete(
|
|
495
|
+
self,
|
|
496
|
+
id: str,
|
|
497
|
+
claim_token: str,
|
|
498
|
+
data: JsonMapping | CollectorUpdate | None = None,
|
|
499
|
+
**fields: Any,
|
|
500
|
+
) -> DiscoveryJobDetail:
|
|
501
|
+
# TODO: could be no content
|
|
502
|
+
headers = {'X-Claim-Token': claim_token}
|
|
503
|
+
return await self._request(
|
|
504
|
+
'POST',
|
|
505
|
+
f'discovery-jobs/{id}/complete/',
|
|
506
|
+
json=self._serialize_body(data, **fields),
|
|
507
|
+
headers=headers,
|
|
508
|
+
response_model=DiscoveryJobDetail,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
async def discoveryjob_heartbeat(self, id: str, claim_token: str) -> DiscoveryJobDetail:
|
|
512
|
+
# TODO: could be no content
|
|
513
|
+
headers = {'X-Claim-Token': claim_token}
|
|
514
|
+
return await self._request(
|
|
515
|
+
'POST',
|
|
516
|
+
f'discovery-jobs/{id}/heartbeat/',
|
|
517
|
+
headers=headers,
|
|
518
|
+
response_model=DiscoveryJobDetail,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
async def discoveryjob_push_discovered_device(
|
|
522
|
+
self,
|
|
523
|
+
id: str,
|
|
524
|
+
claim_token: str,
|
|
525
|
+
data: JsonMapping | DiscoveredDeviceSubmit | None = None,
|
|
526
|
+
**fields: Any,
|
|
527
|
+
) -> DiscoveryJobDetail:
|
|
528
|
+
# TODO: could be no content
|
|
529
|
+
headers = {'X-Claim-Token': claim_token}
|
|
530
|
+
return await self._request(
|
|
531
|
+
'POST',
|
|
532
|
+
f'discovery-jobs/{id}/push-discovered-device/',
|
|
533
|
+
json=self._serialize_body(data, **fields),
|
|
534
|
+
headers=headers,
|
|
535
|
+
response_model=DiscoveryJobDetail,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
async def discoveryjob_status(
|
|
539
|
+
self, id: str, claim_token: str, status: DiscoveryJobStatusEnum | None = None
|
|
540
|
+
) -> DiscoveryJobDetail:
|
|
541
|
+
headers = {'X-Claim-Token': claim_token}
|
|
542
|
+
if status is not None:
|
|
543
|
+
return await self._request(
|
|
544
|
+
'PATCH',
|
|
545
|
+
f'discovery-jobs/{id}/status/',
|
|
546
|
+
headers=headers,
|
|
547
|
+
json={'status': status},
|
|
548
|
+
response_model=DiscoveryJobDetail,
|
|
549
|
+
)
|
|
550
|
+
return await self._request(
|
|
551
|
+
'GET',
|
|
552
|
+
f'discovery-jobs/{id}/status/',
|
|
553
|
+
headers=headers,
|
|
554
|
+
response_model=DiscoveryJobDetail,
|
|
555
|
+
)
|
|
556
|
+
|
|
460
557
|
# TODO
|
|
461
|
-
# claim
|
|
462
|
-
# heartbeat
|
|
463
558
|
# push_discovered_device
|
|
464
|
-
# status
|
|
559
|
+
# status --> implement both GET and PATCH to update the status (e.g. to mark complete or failed)
|
|
465
560
|
# complete
|
|
466
561
|
|
|
467
562
|
# ---------------------------------------------------------------------------
|
|
@@ -17,7 +17,7 @@ class Severity(Enum):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class APIModel(BaseModel):
|
|
20
|
-
"""Base model that tolerates additive API fields without dropping them."""
|
|
20
|
+
"""Base model that doesn't tolerates additive API fields without dropping them."""
|
|
21
21
|
|
|
22
22
|
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
|
23
23
|
|
|
@@ -63,7 +63,9 @@ class TenantDetail(APIModel):
|
|
|
63
63
|
id: UUID4Str
|
|
64
64
|
name: str
|
|
65
65
|
is_active: bool
|
|
66
|
+
log_retention_days: int
|
|
66
67
|
max_snapshots: int
|
|
68
|
+
snapshot_retention_days: int
|
|
67
69
|
created_at: datetime
|
|
68
70
|
updated_at: datetime
|
|
69
71
|
|
|
@@ -72,7 +74,9 @@ class TenantList(APIModel):
|
|
|
72
74
|
id: UUID4Str
|
|
73
75
|
name: str
|
|
74
76
|
is_active: bool
|
|
77
|
+
log_retention_days: int
|
|
75
78
|
max_snapshots: int
|
|
79
|
+
snapshot_retention_days: int
|
|
76
80
|
created_at: datetime
|
|
77
81
|
updated_at: datetime
|
|
78
82
|
|
|
@@ -83,13 +87,17 @@ PaginatedTenantList = PaginatedResponse[TenantList]
|
|
|
83
87
|
class TenantCreate(APIModel):
|
|
84
88
|
name: str
|
|
85
89
|
is_active: bool | None = None
|
|
90
|
+
log_retention_days: int | None = None
|
|
86
91
|
max_snapshots: int | None = None
|
|
92
|
+
snapshot_retention_days: int | None = None
|
|
87
93
|
|
|
88
94
|
|
|
89
95
|
class TenantUpdate(APIModel):
|
|
90
96
|
name: str | None = None
|
|
91
97
|
is_active: bool | None = None
|
|
98
|
+
log_retention_days: int | None = None
|
|
92
99
|
max_snapshots: int | None = None
|
|
100
|
+
snapshot_retention_days: int | None = None
|
|
93
101
|
|
|
94
102
|
|
|
95
103
|
# ---------------------------------------------------------------------------
|
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from uuid import UUID
|
|
6
6
|
|
|
7
|
-
from pydantic import
|
|
7
|
+
from pydantic import BaseModel, field_validator
|
|
8
8
|
|
|
9
9
|
from netdoc_sdk.models.core import APIModel, LogMessage, PaginatedResponse, UUID4Str
|
|
10
10
|
|
|
@@ -43,10 +43,10 @@ PaginatedCollectorList = PaginatedResponse[CollectorList]
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class CollectorUpdate(APIModel):
|
|
46
|
-
canonical_devices: list[UUID] =
|
|
47
|
-
sites: list[UUID] =
|
|
48
|
-
domain_range: list[str] =
|
|
49
|
-
network_range: list[str] =
|
|
46
|
+
canonical_devices: list[UUID] | None = None
|
|
47
|
+
sites: list[UUID] | None = None
|
|
48
|
+
domain_range: list[str] | None = None
|
|
49
|
+
network_range: list[str] | None = None
|
|
50
50
|
is_active: bool | None = None
|
|
51
51
|
|
|
52
52
|
|
|
@@ -126,6 +126,7 @@ class DiscoveryRunDetail(APIModel):
|
|
|
126
126
|
failed_job_count: int
|
|
127
127
|
job_count: int
|
|
128
128
|
raw_output_count: int
|
|
129
|
+
parsed_output_count: int
|
|
129
130
|
cancellation_requested_at: datetime | None
|
|
130
131
|
completed_at: datetime | None
|
|
131
132
|
created_at: datetime
|
|
@@ -144,6 +145,7 @@ class DiscoveryRunList(APIModel):
|
|
|
144
145
|
failed_job_count: int
|
|
145
146
|
job_count: int
|
|
146
147
|
raw_output_count: int
|
|
148
|
+
parsed_output_count: int
|
|
147
149
|
cancellation_requested_at: datetime | None
|
|
148
150
|
completed_at: datetime | None
|
|
149
151
|
created_at: datetime
|
|
@@ -165,13 +167,27 @@ PaginatedDiscoveryRunList = PaginatedResponse[DiscoveryRunList]
|
|
|
165
167
|
# complete
|
|
166
168
|
|
|
167
169
|
|
|
170
|
+
class DiscoveryJobStatusEnum(Enum):
|
|
171
|
+
"""Discovery job lifecycle statuses."""
|
|
172
|
+
|
|
173
|
+
QUEUED = 'queued'
|
|
174
|
+
CLAIMED = 'claimed'
|
|
175
|
+
RUNNING = 'running'
|
|
176
|
+
CANCELLING = 'cancelling'
|
|
177
|
+
CANCELLED = 'cancelled'
|
|
178
|
+
COMPLETED = 'completed'
|
|
179
|
+
FAILED = 'failed'
|
|
180
|
+
EXPIRED = 'expired'
|
|
181
|
+
|
|
182
|
+
|
|
168
183
|
class DiscoveryJobDetail(APIModel):
|
|
169
184
|
id: str
|
|
170
185
|
run: UUID4Str
|
|
171
186
|
collector: UUID4Str
|
|
172
187
|
collector_name: str
|
|
173
188
|
snapshot: UUID4Str
|
|
174
|
-
status:
|
|
189
|
+
status: DiscoveryJobStatusEnum
|
|
190
|
+
canonical_devices: list[dict] # TODO should be part of the inventory
|
|
175
191
|
attempt: int
|
|
176
192
|
max_attempts: int
|
|
177
193
|
idempotency_key: str
|
|
@@ -182,11 +198,27 @@ class DiscoveryJobDetail(APIModel):
|
|
|
182
198
|
timeout_at: datetime | None
|
|
183
199
|
created_at: datetime
|
|
184
200
|
updated_at: datetime
|
|
201
|
+
claim_token: str | None = (
|
|
202
|
+
None # TODO: should be on claim only | if this isn't passed back, in case of crash/restart of a collector, there's no way to get back the claim token --> maybe re-think the workflow to not require the claim token for heartbeats and completion updates? Or have a way to retrieve the claim token for active jobs for a collector?
|
|
203
|
+
)
|
|
185
204
|
|
|
186
205
|
|
|
187
206
|
PaginatedDiscoveryJobList = PaginatedResponse[DiscoveryJobDetail]
|
|
188
207
|
|
|
189
208
|
|
|
209
|
+
class InventoryMeta(BaseModel):
|
|
210
|
+
hostvars: dict[str, dict]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class InventoryAll(BaseModel):
|
|
214
|
+
hosts: list[str]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class DiscoveryJobInventory(BaseModel):
|
|
218
|
+
_meta: InventoryMeta
|
|
219
|
+
all: InventoryAll
|
|
220
|
+
|
|
221
|
+
|
|
190
222
|
class DiscoveryJobClaim(APIModel):
|
|
191
223
|
id: str
|
|
192
224
|
run: UUID4Str
|
|
@@ -194,6 +226,8 @@ class DiscoveryJobClaim(APIModel):
|
|
|
194
226
|
collector_name: str
|
|
195
227
|
snapshot: UUID4Str
|
|
196
228
|
status: str
|
|
229
|
+
canonical_devices: list[dict] # TODO should be part of the inventory
|
|
230
|
+
claim_token: str
|
|
197
231
|
attempt: int
|
|
198
232
|
max_attempts: int
|
|
199
233
|
idempotency_key: str
|
|
@@ -206,6 +240,20 @@ class DiscoveryJobClaim(APIModel):
|
|
|
206
240
|
created_at: datetime
|
|
207
241
|
updated_at: datetime
|
|
208
242
|
|
|
243
|
+
@field_validator('inventory')
|
|
244
|
+
@classmethod
|
|
245
|
+
def validate_inventory(cls, inventory: dict) -> dict:
|
|
246
|
+
DiscoveryJobInventory.model_validate(inventory)
|
|
247
|
+
return inventory # Return original inventory
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class DiscoveredDeviceSubmit(APIModel):
|
|
251
|
+
raw_payload: dict | None = None
|
|
252
|
+
# TODO: send also parsed_payload
|
|
253
|
+
idempotency_key: str
|
|
254
|
+
canonical_device: UUID4Str
|
|
255
|
+
attempt: int
|
|
256
|
+
|
|
209
257
|
|
|
210
258
|
# class DiscoveryJobHeartbeat(APIModel):
|
|
211
259
|
# claim_token: str
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.django_db
|
|
5
|
+
class TestFilter:
|
|
6
|
+
@pytest.mark.django_db(transaction=True)
|
|
7
|
+
async def test_filter_name(self, superuser_client):
|
|
8
|
+
await superuser_client.tenant_add(name='test-tenant-01', is_active=True, max_snapshots=3)
|
|
9
|
+
await superuser_client.tenant_add(name='test-tenant-02', is_active=True, max_snapshots=3)
|
|
10
|
+
await superuser_client.tenant_add(name='test-tenant-03', is_active=True, max_snapshots=3)
|
|
11
|
+
res = await superuser_client.tenant_list(name='test-tenant-01')
|
|
12
|
+
assert res.count == 1
|
|
13
|
+
|
|
14
|
+
@pytest.mark.django_db(transaction=True)
|
|
15
|
+
async def test_filter_name_in(self, superuser_client):
|
|
16
|
+
await superuser_client.tenant_add(name='test-tenant-01', is_active=True, max_snapshots=3)
|
|
17
|
+
await superuser_client.tenant_add(name='test-tenant-02', is_active=True, max_snapshots=3)
|
|
18
|
+
await superuser_client.tenant_add(name='test-03', is_active=True, max_snapshots=3)
|
|
19
|
+
res = await superuser_client.tenant_list(name_in='test-tenant')
|
|
20
|
+
assert res.count == 2
|
|
21
|
+
|
|
22
|
+
@pytest.mark.django_db(transaction=True)
|
|
23
|
+
async def test_filter_name_nonexistent(self, superuser_client):
|
|
24
|
+
await superuser_client.tenant_add(name='test-tenant-01', is_active=True, max_snapshots=3)
|
|
25
|
+
res = await superuser_client.tenant_list(name='nonexistent')
|
|
26
|
+
assert res.count == 0
|
|
27
|
+
|
|
28
|
+
@pytest.mark.django_db(transaction=True)
|
|
29
|
+
async def test_filter_name_in_nonexistent(self, superuser_client):
|
|
30
|
+
await superuser_client.tenant_add(name='test-tenant-01', is_active=True, max_snapshots=3)
|
|
31
|
+
res = await superuser_client.tenant_list(name_in='nonexistent')
|
|
32
|
+
assert res.count == 0
|
|
@@ -10,7 +10,8 @@ class TestTenant:
|
|
|
10
10
|
)
|
|
11
11
|
res = await superuser_client.tenant_list()
|
|
12
12
|
assert res.count == 1
|
|
13
|
-
await superuser_client.tenant_get(
|
|
13
|
+
await superuser_client.tenant_get(tenant.id)
|
|
14
|
+
await superuser_client.tenant_get(tenant.name)
|
|
14
15
|
await superuser_client.tenant_update(
|
|
15
16
|
id=tenant.id, name='test-new-tenant', is_active=False, max_snapshots=1
|
|
16
17
|
)
|
|
@@ -16,7 +16,8 @@ class TestUser:
|
|
|
16
16
|
)
|
|
17
17
|
res = await admin_client.user_list()
|
|
18
18
|
assert res.count == 2
|
|
19
|
-
await admin_client.user_get(
|
|
19
|
+
await admin_client.user_get(user.id)
|
|
20
|
+
await admin_client.user_get(user.username)
|
|
20
21
|
await admin_client.user_update(
|
|
21
22
|
id=user.id,
|
|
22
23
|
username='test-new-user',
|
|
@@ -28,10 +28,14 @@ class TestDiscoveryRun:
|
|
|
28
28
|
# Activate collector
|
|
29
29
|
await admin_client.collector_update(collector.id, is_active=True)
|
|
30
30
|
|
|
31
|
-
#
|
|
31
|
+
# Create run
|
|
32
32
|
discovery_run = await admin_client.discovery_add()
|
|
33
33
|
res = await admin_client.discovery_list()
|
|
34
34
|
assert res.count == 1
|
|
35
35
|
await admin_client.discovery_get(id=discovery_run.id)
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
# Verify jobs
|
|
38
|
+
await admin_client.discovery_jobs(id=discovery_run.id)
|
|
39
|
+
|
|
40
|
+
# Cancel the run
|
|
37
41
|
await admin_client.discovery_cancel(id=discovery_run.id)
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from netdoc_sdk.client import NetDocClient
|
|
4
|
+
|
|
5
|
+
RAW_SHOW_VERSION = """
|
|
6
|
+
Cisco IOS Software, Catalyst 4500 L3 Switch Software (cat4500e-ENTSERVICESK9-M), Version 12.2(54)SG1, RELEASE SOFTWARE (fc1)
|
|
7
|
+
Technical Support: http://www.cisco.com/techsupport
|
|
8
|
+
Copyright (c) 1986-2011 by Cisco Systems, Inc.
|
|
9
|
+
Compiled Thu 27-Jan-11 12:07 by prod_rel_team
|
|
10
|
+
Image text-base: 0x10000000, data-base: 0x12E16D24
|
|
11
|
+
|
|
12
|
+
ROM: 12.2(44r)SG9
|
|
13
|
+
Hobgoblin Revision 20, Fortooine Revision 1.22
|
|
14
|
+
|
|
15
|
+
router1 uptime is 2 years, 31 weeks, 6 days, 9 hours, 55 minutes
|
|
16
|
+
System returned to ROM by reload
|
|
17
|
+
System restarted at 09:09:22 UTC Tue Apr 9 2013
|
|
18
|
+
System image file is "bootflash:cat4500e-entservicesk9-mz.122-54.SG1.bin"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
This product contains cryptographic features and is subject to United
|
|
22
|
+
States and local country laws governing import, export, transfer and
|
|
23
|
+
use. Delivery of Cisco cryptographic products does not imply
|
|
24
|
+
third-party authority to import, export, distribute or use encryption.
|
|
25
|
+
Importers, exporters, distributors and users are responsible for
|
|
26
|
+
compliance with U.S. and local country laws. By using this product you
|
|
27
|
+
agree to comply with applicable laws and regulations. If you are unable
|
|
28
|
+
to comply with U.S. and local laws, return this product immediately.
|
|
29
|
+
|
|
30
|
+
A summary of U.S. laws governing Cisco cryptographic products may be found at:
|
|
31
|
+
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html
|
|
32
|
+
|
|
33
|
+
If you require further assistance please contact us by sending email to
|
|
34
|
+
export@cisco.com.
|
|
35
|
+
|
|
36
|
+
cisco WS-C4948E (MPC8548) processor (revision 4) with 1048576K bytes of memory.
|
|
37
|
+
Processor board ID CAT1451S15C
|
|
38
|
+
MPC8548 CPU at 1GHz, Cisco Catalyst 4948E
|
|
39
|
+
Last reset from Reload
|
|
40
|
+
17 Virtual Ethernet interfaces
|
|
41
|
+
48 Gigabit Ethernet interfaces
|
|
42
|
+
4 Ten Gigabit Ethernet interfaces
|
|
43
|
+
511K bytes of non-volatile configuration memory.
|
|
44
|
+
|
|
45
|
+
Configuration register is 0x2102
|
|
46
|
+
"""
|
|
47
|
+
RAW_SHOW_INTERFACES = """
|
|
48
|
+
GigabitEthernet0/0 is reset, line protocol is down (notconnect)
|
|
49
|
+
Hardware is iGbE, address is fa16.3e57.336f (bia fa16.3e57.336f)
|
|
50
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
51
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
52
|
+
Encapsulation ARPA, loopback not set
|
|
53
|
+
Keepalive set (10 sec)
|
|
54
|
+
Unknown, Unknown, link type is auto, media type is unknown media type
|
|
55
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
56
|
+
Auto-duplex, Auto-speed, link type is auto, media type is unknown
|
|
57
|
+
input flow-control is off, output flow-control is unsupported
|
|
58
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
59
|
+
Last input 1d21h, output 1d21h, output hang never
|
|
60
|
+
Last clearing of "show interface" counters never
|
|
61
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
62
|
+
Queueing strategy: fifo
|
|
63
|
+
Output queue: 0/0 (size/max)
|
|
64
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
65
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
66
|
+
324 packets input, 48614 bytes, 0 no buffer
|
|
67
|
+
Received 0 broadcasts (0 multicasts)
|
|
68
|
+
0 runts, 0 giants, 0 throttles
|
|
69
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
70
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
71
|
+
703 packets output, 62737 bytes, 0 underruns
|
|
72
|
+
0 output errors, 0 collisions, 2 interface resets
|
|
73
|
+
0 unknown protocol drops
|
|
74
|
+
0 babbles, 0 late collision, 0 deferred
|
|
75
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
76
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
77
|
+
GigabitEthernet0/1 is up, line protocol is up (connected)
|
|
78
|
+
Hardware is iGbE, address is fa16.3e4f.41cc (bia fa16.3e4f.41cc)
|
|
79
|
+
Description: to iosvl2-2
|
|
80
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
81
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
82
|
+
Encapsulation ARPA, loopback not set
|
|
83
|
+
Keepalive set (10 sec)
|
|
84
|
+
Auto Duplex, Auto Speed, link type is auto, media type is unknown media type
|
|
85
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
86
|
+
Auto-duplex, Auto-speed, link type is auto, media type is unknown
|
|
87
|
+
input flow-control is off, output flow-control is unsupported
|
|
88
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
89
|
+
Last input 1d21h, output 00:00:02, output hang never
|
|
90
|
+
Last clearing of "show interface" counters never
|
|
91
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
92
|
+
Queueing strategy: fifo
|
|
93
|
+
Output queue: 0/0 (size/max)
|
|
94
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
95
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
96
|
+
83 packets input, 14855 bytes, 0 no buffer
|
|
97
|
+
Received 0 broadcasts (0 multicasts)
|
|
98
|
+
0 runts, 0 giants, 0 throttles
|
|
99
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
100
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
101
|
+
15513 packets output, 2510810 bytes, 0 underruns
|
|
102
|
+
0 output errors, 0 collisions, 3 interface resets
|
|
103
|
+
0 unknown protocol drops
|
|
104
|
+
0 babbles, 0 late collision, 0 deferred
|
|
105
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
106
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
107
|
+
GigabitEthernet0/2 is up, line protocol is up (connected)
|
|
108
|
+
Hardware is iGbE, address is fa16.3ea3.3e49 (bia fa16.3ea3.3e49)
|
|
109
|
+
Description: to iosvl2-4
|
|
110
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
111
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
112
|
+
Encapsulation ARPA, loopback not set
|
|
113
|
+
Keepalive set (10 sec)
|
|
114
|
+
Unknown, Unknown, link type is auto, media type is unknown media type
|
|
115
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
116
|
+
Auto-duplex, Auto-speed, link type is auto, media type is unknown
|
|
117
|
+
input flow-control is off, output flow-control is unsupported
|
|
118
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
119
|
+
Last input 00:00:13, output 00:00:00, output hang never
|
|
120
|
+
Last clearing of "show interface" counters never
|
|
121
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
122
|
+
Queueing strategy: fifo
|
|
123
|
+
Output queue: 0/0 (size/max)
|
|
124
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
125
|
+
5 minute output rate 1000 bits/sec, 2 packets/sec
|
|
126
|
+
8677 packets input, 1698461 bytes, 0 no buffer
|
|
127
|
+
Received 0 broadcasts (0 multicasts)
|
|
128
|
+
0 runts, 0 giants, 0 throttles
|
|
129
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
130
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
131
|
+
420798 packets output, 29058795 bytes, 0 underruns
|
|
132
|
+
0 output errors, 0 collisions, 2 interface resets
|
|
133
|
+
0 unknown protocol drops
|
|
134
|
+
0 babbles, 0 late collision, 0 deferred
|
|
135
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
136
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
137
|
+
GigabitEthernet0/3 is up, line protocol is up (connected)
|
|
138
|
+
Hardware is iGbE, address is fa16.3e31.2c47 (bia fa16.3e31.2c47)
|
|
139
|
+
Description: to iosvl2-3
|
|
140
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
141
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
142
|
+
Encapsulation ARPA, loopback not set
|
|
143
|
+
Keepalive set (10 sec)
|
|
144
|
+
Unknown, Unknown, link type is auto, media type is unknown media type
|
|
145
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
146
|
+
Auto-duplex, Auto-speed, link type is auto, media type is unknown
|
|
147
|
+
input flow-control is off, output flow-control is unsupported
|
|
148
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
149
|
+
Last input 00:00:28, output 00:00:00, output hang never
|
|
150
|
+
Last clearing of "show interface" counters never
|
|
151
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
152
|
+
Queueing strategy: fifo
|
|
153
|
+
Output queue: 0/0 (size/max)
|
|
154
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
155
|
+
5 minute output rate 1000 bits/sec, 2 packets/sec
|
|
156
|
+
8638 packets input, 1689698 bytes, 0 no buffer
|
|
157
|
+
Received 0 broadcasts (0 multicasts)
|
|
158
|
+
0 runts, 0 giants, 0 throttles
|
|
159
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
160
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
161
|
+
420819 packets output, 29060539 bytes, 0 underruns
|
|
162
|
+
0 output errors, 0 collisions, 2 interface resets
|
|
163
|
+
0 unknown protocol drops
|
|
164
|
+
0 babbles, 0 late collision, 0 deferred
|
|
165
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
166
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
167
|
+
GigabitEthernet1/0 is up, line protocol is up (connected)
|
|
168
|
+
Hardware is iGbE, address is fa16.3ec8.50ab (bia fa16.3ec8.50ab)
|
|
169
|
+
Description: to iosvl2-3
|
|
170
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
171
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
172
|
+
Encapsulation ARPA, loopback not set
|
|
173
|
+
Keepalive set (10 sec)
|
|
174
|
+
Unknown, Unknown, link type is auto, media type is unknown media type
|
|
175
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
176
|
+
Auto-duplex, Auto-speed, link type is auto, media type is unknown
|
|
177
|
+
input flow-control is off, output flow-control is unsupported
|
|
178
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
179
|
+
Last input 00:00:26, output 00:00:00, output hang never
|
|
180
|
+
Last clearing of "show interface" counters never
|
|
181
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
182
|
+
Queueing strategy: fifo
|
|
183
|
+
Output queue: 0/0 (size/max)
|
|
184
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
185
|
+
5 minute output rate 2000 bits/sec, 2 packets/sec
|
|
186
|
+
8627 packets input, 1690235 bytes, 0 no buffer
|
|
187
|
+
Received 0 broadcasts (0 multicasts)
|
|
188
|
+
0 runts, 0 giants, 0 throttles
|
|
189
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
190
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
191
|
+
420790 packets output, 29056035 bytes, 0 underruns
|
|
192
|
+
0 output errors, 0 collisions, 2 interface resets
|
|
193
|
+
0 unknown protocol drops
|
|
194
|
+
0 babbles, 0 late collision, 0 deferred
|
|
195
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
196
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
197
|
+
Port-channel1 is down, line protocol is down (notconnect)
|
|
198
|
+
Hardware is EtherChannel, address is fa16.3e4f.41cc (bia fa16.3e4f.41cc)
|
|
199
|
+
MTU 1500 bytes, BW 100000 Kbit/sec, DLY 100 usec,
|
|
200
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
201
|
+
Encapsulation ARPA, loopback not set
|
|
202
|
+
Keepalive set (10 sec)
|
|
203
|
+
Auto-duplex, Auto-speed, media type is unknown
|
|
204
|
+
input flow-control is off, output flow-control is unsupported
|
|
205
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
206
|
+
Last input 1d21h, output never, output hang never
|
|
207
|
+
Last clearing of "show interface" counters never
|
|
208
|
+
Input queue: 0/2000/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
209
|
+
Queueing strategy: fifo
|
|
210
|
+
Output queue: 0/40 (size/max)
|
|
211
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
212
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
213
|
+
85 packets input, 7037 bytes, 0 no buffer
|
|
214
|
+
Received 0 broadcasts (0 multicasts)
|
|
215
|
+
0 runts, 0 giants, 0 throttles
|
|
216
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
217
|
+
0 input packets with dribble condition detected
|
|
218
|
+
0 packets output, 0 bytes, 0 underruns
|
|
219
|
+
0 output errors, 0 collisions, 0 interface resets
|
|
220
|
+
0 unknown protocol drops
|
|
221
|
+
0 babbles, 0 late collision, 0 deferred
|
|
222
|
+
0 lost carrier, 0 no carrier
|
|
223
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
224
|
+
Loopback0 is up, line protocol is up
|
|
225
|
+
Hardware is Loopback
|
|
226
|
+
Description: Loopback
|
|
227
|
+
MTU 1514 bytes, BW 8000000 Kbit/sec, DLY 5000 usec,
|
|
228
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
229
|
+
Encapsulation LOOPBACK, loopback not set
|
|
230
|
+
Keepalive set (10 sec)
|
|
231
|
+
Last input never, output never, output hang never
|
|
232
|
+
Last clearing of "show interface" counters never
|
|
233
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
234
|
+
Queueing strategy: fifo
|
|
235
|
+
Output queue: 0/0 (size/max)
|
|
236
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
237
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
238
|
+
0 packets input, 0 bytes, 0 no buffer
|
|
239
|
+
Received 0 broadcasts (0 IP multicasts)
|
|
240
|
+
0 runts, 0 giants, 0 throttles
|
|
241
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored, 0 abort
|
|
242
|
+
0 packets output, 0 bytes, 0 underruns
|
|
243
|
+
0 output errors, 0 collisions, 0 interface resets
|
|
244
|
+
0 unknown protocol drops
|
|
245
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
246
|
+
Vlan1 is up, line protocol is up
|
|
247
|
+
Hardware is Ethernet SVI, address is fa16.3e57.8001 (bia fa16.3e57.8001)
|
|
248
|
+
Description: OOB Management
|
|
249
|
+
Internet address is 10.255.0.16/16
|
|
250
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
251
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
252
|
+
Encapsulation ARPA, loopback not set
|
|
253
|
+
Keepalive not supported
|
|
254
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
255
|
+
Last input never, output never, output hang never
|
|
256
|
+
Last clearing of "show interface" counters never
|
|
257
|
+
Input queue: 0/75/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
258
|
+
Queueing strategy: fifo
|
|
259
|
+
Output queue: 0/40 (size/max)
|
|
260
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
261
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
262
|
+
0 packets input, 0 bytes, 0 no buffer
|
|
263
|
+
Received 0 broadcasts (0 IP multicasts)
|
|
264
|
+
0 runts, 0 giants, 0 throttles
|
|
265
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
266
|
+
4 packets output, 240 bytes, 0 underruns
|
|
267
|
+
0 output errors, 0 interface resets
|
|
268
|
+
0 unknown protocol drops
|
|
269
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
270
|
+
GigabitEthernet0/2 is administratively down, line protocol is down
|
|
271
|
+
Hardware is ASR1001, address is 78da.6eaf.3b82 (bia 78da.6eaf.3b82)
|
|
272
|
+
Description: Port
|
|
273
|
+
MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec,
|
|
274
|
+
reliability 255/255, txload 1/255, rxload 1/255
|
|
275
|
+
Encapsulation ARPA, loopback not set
|
|
276
|
+
Keepalive not supported
|
|
277
|
+
Full Duplex, 1000Mbps, link type is auto, media type is unknown media type
|
|
278
|
+
output flow-control is unsupported, input flow-control is unsupported
|
|
279
|
+
ARP type: ARPA, ARP Timeout 04:00:00
|
|
280
|
+
Last input never, output never, output hang never
|
|
281
|
+
Last clearing of "show interface" counters never
|
|
282
|
+
Input queue: 0/375/0/0 (size/max/drops/flushes); Total output drops: 0
|
|
283
|
+
Queueing strategy: fifo
|
|
284
|
+
Output queue: 0/40 (size/max)
|
|
285
|
+
5 minute input rate 0 bits/sec, 0 packets/sec
|
|
286
|
+
5 minute output rate 0 bits/sec, 0 packets/sec
|
|
287
|
+
0 packets input, 0 bytes, 0 no buffer
|
|
288
|
+
Received 0 broadcasts (0 IP multicasts)
|
|
289
|
+
0 runts, 0 giants, 0 throttles
|
|
290
|
+
0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
|
|
291
|
+
0 watchdog, 0 multicast, 0 pause input
|
|
292
|
+
0 packets output, 0 bytes, 0 underruns
|
|
293
|
+
0 output errors, 0 collisions, 1 interface resets
|
|
294
|
+
0 unknown protocol drops
|
|
295
|
+
0 babbles, 0 late collision, 0 deferred
|
|
296
|
+
0 lost carrier, 0 no carrier, 0 pause output
|
|
297
|
+
0 output buffer failures, 0 output buffers swapped out
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@pytest.mark.django_db
|
|
302
|
+
class TestDiscoveryRun:
|
|
303
|
+
@pytest.mark.django_db(transaction=True)
|
|
304
|
+
async def test_discovery_job(self, admin_client, live_server):
|
|
305
|
+
collector_username = 'test-collector-user'
|
|
306
|
+
collector_password = 'test-password'
|
|
307
|
+
|
|
308
|
+
# Add collector user
|
|
309
|
+
await admin_client.user_add(
|
|
310
|
+
username=collector_username, password=collector_password, role='collector'
|
|
311
|
+
)
|
|
312
|
+
collector_client = await NetDocClient.from_credentials(
|
|
313
|
+
base_url=live_server.url,
|
|
314
|
+
username=collector_username,
|
|
315
|
+
password=collector_password,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Create canonical device
|
|
319
|
+
canonical_device = await admin_client.canonicaldevice_add(
|
|
320
|
+
label='r1.example.com',
|
|
321
|
+
discovery_mode='netmiko:cisco:ios:ssh',
|
|
322
|
+
is_discoverable=True,
|
|
323
|
+
identifiers={'hostname': 'r1'},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Create collector (heartbeat)
|
|
327
|
+
collector = await collector_client.collector_heartbeat(
|
|
328
|
+
name='collector@host.example.com', version='0.1.0'
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Activate collector
|
|
332
|
+
await admin_client.collector_update(collector.id, is_active=True)
|
|
333
|
+
|
|
334
|
+
# Create run
|
|
335
|
+
discovery_run = await admin_client.discovery_add()
|
|
336
|
+
res = await admin_client.discovery_list()
|
|
337
|
+
assert res.count == 1
|
|
338
|
+
await admin_client.discovery_get(id=discovery_run.id)
|
|
339
|
+
|
|
340
|
+
# Verify jobs
|
|
341
|
+
await admin_client.discovery_jobs(id=discovery_run.id)
|
|
342
|
+
|
|
343
|
+
# Claim job
|
|
344
|
+
res = await collector_client.discoveryjob_claim()
|
|
345
|
+
job_id = res.id
|
|
346
|
+
claim_token = res.claim_token
|
|
347
|
+
idempotency_key = res.idempotency_key
|
|
348
|
+
|
|
349
|
+
# Job heartbeat
|
|
350
|
+
await collector_client.discoveryjob_heartbeat(id=job_id, claim_token=claim_token)
|
|
351
|
+
|
|
352
|
+
# Get job status
|
|
353
|
+
# TODO: await admin_client.discoveryjob_status(id=job_id)
|
|
354
|
+
# TODO: should be GET only (not PATCH)
|
|
355
|
+
|
|
356
|
+
# Push discovered devices
|
|
357
|
+
payload = {
|
|
358
|
+
'canonical_device': canonical_device.id,
|
|
359
|
+
'command_name': 'show version',
|
|
360
|
+
'raw_payload': {
|
|
361
|
+
'show version': RAW_SHOW_VERSION,
|
|
362
|
+
'show interfaces': RAW_SHOW_INTERFACES,
|
|
363
|
+
},
|
|
364
|
+
'idempotency_key': idempotency_key,
|
|
365
|
+
}
|
|
366
|
+
await collector_client.discoveryjob_push_discovered_device(
|
|
367
|
+
id=job_id, claim_token=claim_token, **payload
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Complete job
|
|
371
|
+
payload = {
|
|
372
|
+
'status': 'completed',
|
|
373
|
+
'log_messages': [
|
|
374
|
+
{
|
|
375
|
+
'severity': 'INFO',
|
|
376
|
+
'timestamp': '2026-05-17T09:36:45.244045+00:00',
|
|
377
|
+
'message': "Running netmiko command 'show ip route vrf Mgmt-vrf' on mgmtswitch1.example.com",
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
'severity': 'WARNING',
|
|
381
|
+
'timestamp': '2026-05-17T09:36:45.387987+00:00',
|
|
382
|
+
'message': "Cannot parse command 'show ip route vrf Mgmt-vrf'",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
'severity': 'INFO',
|
|
386
|
+
'timestamp': '2026-05-17T09:36:45.390002+00:00',
|
|
387
|
+
'message': 'Discovery completed (failed on 0 hosts)',
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
}
|
|
391
|
+
await collector_client.discoveryjob_complete(id=job_id, claim_token=claim_token, **payload)
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from netdoc_sdk.client import NetDocClient
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.mark.django_db
|
|
7
|
-
class TestDiscoveryRun:
|
|
8
|
-
@pytest.mark.django_db(transaction=True)
|
|
9
|
-
async def test_discovery_run(self, admin_client, live_server):
|
|
10
|
-
collector_username = 'test-collector-user'
|
|
11
|
-
collector_password = 'test-password'
|
|
12
|
-
|
|
13
|
-
# Add collector user
|
|
14
|
-
await admin_client.user_add(
|
|
15
|
-
username=collector_username, password=collector_password, role='collector'
|
|
16
|
-
)
|
|
17
|
-
collector_client = await NetDocClient.from_credentials(
|
|
18
|
-
base_url=live_server.url,
|
|
19
|
-
username=collector_username,
|
|
20
|
-
password=collector_password,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
# Create collector (heartbeat)
|
|
24
|
-
collector = await collector_client.collector_heartbeat(
|
|
25
|
-
name='collector@host.example.com', version='0.1.0'
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# Activate collector
|
|
29
|
-
await admin_client.collector_update(collector.id, is_active=True)
|
|
30
|
-
|
|
31
|
-
# Test functions
|
|
32
|
-
await collector_client.discoveryjob_claim()
|
|
33
|
-
# TODO heartbeat
|
|
34
|
-
# TODO push_discovered_device
|
|
35
|
-
# TODO status
|
|
36
|
-
# TODO complete
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/inventory/test_002_canonical_device.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|