1 | # py312 |
2 | # requirements: |
3 | # pynetbox==7.9.0 |
4 |
|
5 |
|
6 | from typing import List, Literal, Optional, TypedDict |
7 |
|
8 | import pynetbox |
9 |
|
10 |
|
11 | class netbox(TypedDict): |
12 | url: str |
13 | token: str |
14 |
|
15 |
|
16 | DynMultiselect_site = List[str] |
17 | DynMultiselect_role = List[str] |
18 |
|
19 |
|
20 | def api(auth: netbox): |
21 | # pynetbox adds /api itself; the resource URL may already end with it. |
22 | return pynetbox.api( |
23 | auth["url"].rstrip("/").removesuffix("/api"), token=auth["token"] |
24 | ) |
25 |
|
26 |
|
27 | def keep(**fields): |
28 | # The run form sends "" / [] / {} for untouched fields: drop them. |
29 | return {k: v for k, v in fields.items() if v not in (None, "", [], {})} |
30 |
|
31 |
|
32 | def page(records): |
33 | # One page (limit/offset), as NetBox's count plus the raw records. |
34 | results = [dict(r) for r in records] |
35 | return {"count": len(records), "results": results} |
36 |
|
37 |
|
38 | def options(endpoint, key="id", **filters): |
39 | return [ |
40 | {"value": str(getattr(r, key)), "label": r.display} |
41 | for r in endpoint.filter(brief=1, limit=1000, offset=0, **filters) |
42 | ] |
43 |
|
44 |
|
45 | # Dropdown of the instance's sites, by slug: the first 1000 (NetBox MAX_PAGE_SIZE); set the value by expression beyond that. |
46 | def site(auth: netbox): |
47 | return options(api(auth).dcim.sites, "slug") |
48 |
|
49 |
|
50 | # Dropdown of the instance's device roles, by slug. |
51 | def role(auth: netbox): |
52 | return options(api(auth).dcim.device_roles, "slug") |
53 |
|
54 |
|
55 | def main( |
56 | auth: netbox, |
57 | site: Optional[DynMultiselect_site] = None, |
58 | role: Optional[DynMultiselect_role] = None, |
59 | status: Optional[ |
60 | List[ |
61 | Literal[ |
62 | "offline", |
63 | "active", |
64 | "planned", |
65 | "staged", |
66 | "failed", |
67 | "inventory", |
68 | "decommissioning", |
69 | ] |
70 | ] |
71 | ] = None, |
72 | tag: Optional[List[str]] = None, |
73 | name: Optional[str] = None, |
74 | custom_field_filters: Optional[dict] = None, |
75 | q: Optional[str] = None, |
76 | limit: Optional[int] = None, |
77 | offset: Optional[int] = None, |
78 | ): |
79 | """ |
80 | List Devices |
81 | List devices filtered by sites, roles, status, tags (slugs), exact name or custom fields ({tier: "edge"} becomes cf_tier=edge). Returns NetBox's count and one page of raw devices, including platform and custom_fields. |
82 | """ |
83 | cf = { |
84 | k if k.startswith("cf_") else f"cf_{k}": v |
85 | for k, v in (custom_field_filters or {}).items() |
86 | } |
87 | records = api(auth).dcim.devices.filter( |
88 | **keep(site=site, role=role, status=status, tag=tag, name=name, q=q, **cf), |
89 | # limit=0 means NetBox's MAX_PAGE_SIZE, which also caps an explicit 1000. |
90 | limit=50 if limit is None else limit or 1000, |
91 | offset=offset or 0, |
92 | ) |
93 | return page(records) |
94 | |