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.
Files changed (56) hide show
  1. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/workflows/ci.yml +7 -10
  2. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.pre-commit-config.yaml +1 -0
  3. netdoc_sdk-0.3.0/.release-please-manifest.json +3 -0
  4. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/CHANGELOG.md +14 -0
  5. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/PKG-INFO +1 -1
  6. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/poetry.lock +35 -1
  7. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/pyproject.toml +4 -1
  8. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/client.py +117 -22
  9. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/core.py +9 -1
  10. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/discovery.py +54 -6
  11. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/inventory.py +1 -1
  12. netdoc_sdk-0.3.0/tests/integration/core/test_001_filters.py +32 -0
  13. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_002_tenant.py +2 -1
  14. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_003_user.py +2 -1
  15. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_003_discovery_run.py +6 -2
  16. netdoc_sdk-0.3.0/tests/integration/discovery/test_004_discovery_job.py +391 -0
  17. netdoc_sdk-0.2.4/.release-please-manifest.json +0 -3
  18. netdoc_sdk-0.2.4/tests/integration/discovery/test_004_discovery_job.py +0 -36
  19. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.editorconfig +0 -0
  20. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/CODEOWNERS +0 -0
  21. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.github/workflows/cd.yml +0 -0
  22. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.gitignore +0 -0
  23. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.markdownlint.yaml +0 -0
  24. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.pylintrc +0 -0
  25. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.snyk +0 -0
  26. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/.yamlfmt +0 -0
  27. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/CONTRIBUTING.md +0 -0
  28. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/LICENSE +0 -0
  29. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/Makefile +0 -0
  30. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/README.md +0 -0
  31. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/release-please-config.json +0 -0
  32. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/__init__.py +0 -0
  33. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/exceptions.py +0 -0
  34. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/__init__.py +0 -0
  35. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/src/netdoc_sdk/models/snapshots.py +0 -0
  36. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/conftest.py +0 -0
  37. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_001_authentication.py +0 -0
  38. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_004_token.py +0 -0
  39. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_005_profile.py +0 -0
  40. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/core/test_006_audit_log.py +0 -0
  41. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_001_credentials.py +0 -0
  42. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/discovery/test_002_collector.py +0 -0
  43. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/inventory/test_001_site.py +0 -0
  44. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/integration/inventory/test_002_canonical_device.py +0 -0
  45. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/conftest.py +0 -0
  46. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_001_normalize_base_url.py +0 -0
  47. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_002_headers.py +0 -0
  48. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_003_api_path.py +0 -0
  49. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_004_clean_params.py +0 -0
  50. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_005_serialize_body.py +0 -0
  51. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_006_context_manager.py +0 -0
  52. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_007_error_mapping.py +0 -0
  53. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_008_network_errors.py +0 -0
  54. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_009_query_params.py +0 -0
  55. {netdoc_sdk-0.2.4 → netdoc_sdk-0.3.0}/tests/unit/test_010_response_parsing.py +0 -0
  56. {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
- working-directory: netdoc-sdk
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
@@ -7,6 +7,7 @@ exclude: >-
7
7
  \.venv/.*|
8
8
  \.vscode/.*|
9
9
  build/.*|
10
+ CHANGELOG.md|
10
11
  dist/.*|
11
12
  env/.*|
12
13
  venv/.*|
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netdoc-sdk
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: Network Documentation Platform - SDK
5
5
  Project-URL: homepage, https://github.com/NetDocLab/netdoc-sdk
6
6
  Project-URL: documentation, https://github.com/NetDocLab/netdoc-sdk/wiki
@@ -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 = "f7c41355008cd6c23ff019485f4d907924c4c241828571cecf0587513f4d921f"
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.2.4"
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
- try:
200
- response = await self.client.request(
201
- content=content,
202
- headers=request_headers,
203
- json=self._serialize_body(json) if json is not None else None,
204
- method=method,
205
- params=self._clean_params(params),
206
- url=self._api_path(path),
207
- )
208
- except (httpx.ConnectError, httpx.NetworkError) as exc:
209
- raise ConnectionError(f'Failed to connect: {exc}') from exc
210
- except httpx.TimeoutException as exc:
211
- raise ConnectionError(f'Request timed out: {exc}') from exc
212
-
213
- if expected is None:
214
- ok = 200 <= response.status_code < 300
215
- else:
216
- ok = response.status_code in expected
217
- if not ok:
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 Field
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] = Field(default_factory=list)
47
- sites: list[UUID] = Field(default_factory=list)
48
- domain_range: list[str] = Field(default_factory=list)
49
- network_range: list[str] = Field(default_factory=list)
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: str
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
@@ -55,7 +55,7 @@ class SiteCreate(APIModel):
55
55
 
56
56
 
57
57
  class SiteUpdate(APIModel):
58
- name: str
58
+ name: str | None = None
59
59
  site_type: SiteTypeEnum | None = None
60
60
  address: str | None = None
61
61
  city: str | None = None
@@ -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(id=tenant.id)
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(id=user.id)
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
- # Test functions
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
- # TODO: await admin_client.discovery_jobs(id=discovery_run.id)
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,3 +0,0 @@
1
- {
2
- ".": "0.2.4"
3
- }
@@ -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