netbox-cisco-ise 0.1.3__tar.gz → 0.1.5__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 (21) hide show
  1. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/PKG-INFO +7 -1
  2. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/README.md +6 -0
  3. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/__init__.py +5 -2
  4. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/ise_client.py +16 -4
  5. netbox_cisco_ise-0.1.5/netbox_cisco_ise/templates/netbox_cisco_ise/device_tab.html +24 -0
  6. netbox_cisco_ise-0.1.5/netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab_content.html +216 -0
  7. netbox_cisco_ise-0.1.5/netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab_content.html +193 -0
  8. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/urls.py +2 -1
  9. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/views.py +83 -34
  10. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise.egg-info/PKG-INFO +7 -1
  11. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise.egg-info/SOURCES.txt +3 -0
  12. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/pyproject.toml +1 -1
  13. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/LICENSE +0 -0
  14. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/navigation.py +0 -0
  15. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab.html +0 -0
  16. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab.html +0 -0
  17. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise/templates/netbox_cisco_ise/settings.html +0 -0
  18. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise.egg-info/dependency_links.txt +0 -0
  19. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise.egg-info/requires.txt +0 -0
  20. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/netbox_cisco_ise.egg-info/top_level.txt +0 -0
  21. {netbox_cisco_ise-0.1.3 → netbox_cisco_ise-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-cisco-ise
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: NetBox plugin for Cisco ISE integration - endpoint tracking, NAD management, and session visibility
5
5
  Author-email: sieteunoseis <jeremy.worden@gmail.com>
6
6
  License: Apache-2.0
@@ -273,6 +273,12 @@ Contributions are welcome! Please:
273
273
  2. Create a feature branch
274
274
  3. Submit a pull request
275
275
 
276
+ ## Support
277
+
278
+ If you find this plugin helpful, consider supporting development:
279
+
280
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/automatebldrs)
281
+
276
282
  ## Related Projects
277
283
 
278
284
  - [netbox-catalyst-center](https://github.com/sieteunoseis/netbox-catalyst-center) - Catalyst Center integration for NetBox
@@ -241,6 +241,12 @@ Contributions are welcome! Please:
241
241
  2. Create a feature branch
242
242
  3. Submit a pull request
243
243
 
244
+ ## Support
245
+
246
+ If you find this plugin helpful, consider supporting development:
247
+
248
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/automatebldrs)
249
+
244
250
  ## Related Projects
245
251
 
246
252
  - [netbox-catalyst-center](https://github.com/sieteunoseis/netbox-catalyst-center) - Catalyst Center integration for NetBox
@@ -7,7 +7,7 @@ Shows endpoint identity, profiling data, active session status, and network acce
7
7
 
8
8
  from netbox.plugins import PluginConfig
9
9
 
10
- __version__ = "0.1.3"
10
+ __version__ = "0.1.5"
11
11
 
12
12
 
13
13
  class CiscoISEConfig(PluginConfig):
@@ -49,7 +49,10 @@ class CiscoISEConfig(PluginConfig):
49
49
  # {"manufacturer": "cisco", "device_type": ".*phone.*", "lookup": "endpoint"}, # Cisco phones by MAC
50
50
  # ]
51
51
  "device_mappings": [
52
- {"manufacturer": r"cisco", "lookup": "nad"}, # Default: Cisco devices as NADs
52
+ {
53
+ "manufacturer": r"cisco",
54
+ "lookup": "nad",
55
+ }, # Default: Cisco devices as NADs
53
56
  ],
54
57
  }
55
58
 
@@ -275,7 +275,9 @@ class ISEClient:
275
275
  cached["cached"] = True
276
276
  return cached
277
277
 
278
- result = self._make_ers_request("/networkdevice", params={"filter": f"ipaddress.EQ.{ip_address}"})
278
+ result = self._make_ers_request(
279
+ "/networkdevice", params={"filter": f"ipaddress.EQ.{ip_address}"}
280
+ )
279
281
 
280
282
  return self._process_nad_result(result, cache_key)
281
283
 
@@ -295,7 +297,9 @@ class ISEClient:
295
297
  cached["cached"] = True
296
298
  return cached
297
299
 
298
- result = self._make_ers_request("/networkdevice", params={"filter": f"name.CONTAINS.{name}"})
300
+ result = self._make_ers_request(
301
+ "/networkdevice", params={"filter": f"name.CONTAINS.{name}"}
302
+ )
299
303
 
300
304
  return self._process_nad_result(result, cache_key)
301
305
 
@@ -308,7 +312,11 @@ class ISEClient:
308
312
  resources = search_result.get("resources", [])
309
313
 
310
314
  if not resources:
311
- return {"error": "Network device not found in ISE", "not_found": True, "is_nad": False}
315
+ return {
316
+ "error": "Network device not found in ISE",
317
+ "not_found": True,
318
+ "is_nad": False,
319
+ }
312
320
 
313
321
  # Get full NAD details
314
322
  nad_id = resources[0].get("id")
@@ -357,7 +365,11 @@ class ISEClient:
357
365
  "ro_community": bool(snmp_settings.get("roCommunity")),
358
366
  "polling_interval": snmp_settings.get("pollingInterval"),
359
367
  },
360
- "trustsec_enabled": bool(trustsec_settings.get("deviceAuthenticationSettings", {}).get("sgaDeviceId")),
368
+ "trustsec_enabled": bool(
369
+ trustsec_settings.get("deviceAuthenticationSettings", {}).get(
370
+ "sgaDeviceId"
371
+ )
372
+ ),
361
373
  "coA_port": nad.get("coaPort"),
362
374
  "cached": False,
363
375
  }
@@ -0,0 +1,24 @@
1
+ {% extends 'dcim/device/base.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block content %}
5
+ {% if loading %}
6
+ <div id="ise-content"
7
+ hx-get="{% url 'plugins:netbox_cisco_ise:device_content' pk=object.pk %}"
8
+ hx-trigger="load"
9
+ hx-swap="innerHTML">
10
+ <div class="d-flex justify-content-center align-items-center py-5">
11
+ <div class="text-center">
12
+ <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
13
+ <span class="visually-hidden">Loading...</span>
14
+ </div>
15
+ <p class="text-muted mb-0">Loading Cisco ISE data...</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ {% elif error %}
20
+ <div class="alert alert-warning">
21
+ <i class="mdi mdi-alert"></i> {{ error }}
22
+ </div>
23
+ {% endif %}
24
+ {% endblock %}
@@ -0,0 +1,216 @@
1
+ {% load helpers %}
2
+
3
+ <div class="row">
4
+ <div class="col-12">
5
+ {% if error %}
6
+ <div class="alert alert-warning">
7
+ <i class="mdi mdi-alert"></i> {{ error }}
8
+ </div>
9
+ {% elif ise_data %}
10
+ <div class="row">
11
+ <!-- Session Status Card -->
12
+ <div class="col-md-4 mb-3">
13
+ <div class="card h-100">
14
+ <div class="card-header">
15
+ <h5 class="card-title mb-0">
16
+ <i class="mdi mdi-lan-connect"></i> Session Status
17
+ </h5>
18
+ </div>
19
+ <div class="card-body text-center">
20
+ {% if session_data.connected %}
21
+ <div class="p-3 mb-2 bg-success rounded">
22
+ <span class="text-white fs-3 fw-bold">
23
+ <i class="mdi mdi-check-circle"></i> Connected
24
+ </span>
25
+ </div>
26
+ {% if session_data.framed_ip_address %}
27
+ <div class="mt-2">
28
+ <span class="text-muted">IP Address:</span>
29
+ <code class="ms-2">{{ session_data.framed_ip_address }}</code>
30
+ </div>
31
+ {% endif %}
32
+ {% if session_data.authorization_profile %}
33
+ <div class="mt-1">
34
+ <span class="text-muted">Authorization:</span>
35
+ <span class="badge bg-info text-dark ms-2">{{ session_data.authorization_profile }}</span>
36
+ </div>
37
+ {% endif %}
38
+ {% if session_data.acct_session_time %}
39
+ <div class="mt-1 text-muted small">
40
+ Session time: {{ session_data.acct_session_time }} seconds
41
+ </div>
42
+ {% endif %}
43
+ {% else %}
44
+ <div class="p-3 mb-2 bg-secondary rounded">
45
+ <span class="text-white fs-3 fw-bold">
46
+ <i class="mdi mdi-lan-disconnect"></i> Disconnected
47
+ </span>
48
+ </div>
49
+ <div class="mt-2 text-muted">
50
+ No active session found
51
+ </div>
52
+ {% endif %}
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Endpoint Identity Card -->
58
+ <div class="col-md-4 mb-3">
59
+ <div class="card h-100">
60
+ <div class="card-header">
61
+ <h5 class="card-title mb-0">
62
+ <i class="mdi mdi-account-badge"></i> Endpoint Identity
63
+ </h5>
64
+ </div>
65
+ <div class="card-body">
66
+ <table class="table table-sm table-borderless mb-0">
67
+ <tr>
68
+ <th class="text-muted" style="width: 40%">MAC Address:</th>
69
+ <td><code>{{ ise_data.mac_address }}</code></td>
70
+ </tr>
71
+ <tr>
72
+ <th class="text-muted">Profile:</th>
73
+ <td>{{ ise_data.profile_name|default:"N/A" }}</td>
74
+ </tr>
75
+ <tr>
76
+ <th class="text-muted">Identity Group:</th>
77
+ <td>{{ ise_data.group_name|default:"N/A" }}</td>
78
+ </tr>
79
+ <tr>
80
+ <th class="text-muted">Static Assignment:</th>
81
+ <td>
82
+ {% if ise_data.static_group_assignment %}
83
+ <span class="badge bg-success text-white">Yes</span>
84
+ {% else %}
85
+ <span class="badge bg-secondary text-white">No</span>
86
+ {% endif %}
87
+ </td>
88
+ </tr>
89
+ {% if ise_data.portal_user %}
90
+ <tr>
91
+ <th class="text-muted">Portal User:</th>
92
+ <td>{{ ise_data.portal_user }}</td>
93
+ </tr>
94
+ {% endif %}
95
+ </table>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Connection Details Card -->
101
+ <div class="col-md-4 mb-3">
102
+ <div class="card h-100">
103
+ <div class="card-header">
104
+ <h5 class="card-title mb-0">
105
+ <i class="mdi mdi-router-wireless"></i> Connection Details
106
+ </h5>
107
+ </div>
108
+ <div class="card-body">
109
+ {% if session_data.connected %}
110
+ <table class="table table-sm table-borderless mb-0">
111
+ {% if session_data.nas_ip_address %}
112
+ <tr>
113
+ <th class="text-muted" style="width: 40%">Connected To:</th>
114
+ <td><code>{{ session_data.nas_ip_address }}</code></td>
115
+ </tr>
116
+ {% endif %}
117
+ {% if session_data.nas_port_id %}
118
+ <tr>
119
+ <th class="text-muted">Port:</th>
120
+ <td>{{ session_data.nas_port_id }}</td>
121
+ </tr>
122
+ {% endif %}
123
+ {% if session_data.vlan %}
124
+ <tr>
125
+ <th class="text-muted">VLAN:</th>
126
+ <td>{{ session_data.vlan }}</td>
127
+ </tr>
128
+ {% endif %}
129
+ {% if session_data.ssid %}
130
+ <tr>
131
+ <th class="text-muted">SSID:</th>
132
+ <td>{{ session_data.ssid }}</td>
133
+ </tr>
134
+ {% endif %}
135
+ {% if session_data.auth_method %}
136
+ <tr>
137
+ <th class="text-muted">Auth Method:</th>
138
+ <td>{{ session_data.auth_method }}</td>
139
+ </tr>
140
+ {% endif %}
141
+ {% if session_data.security_group %}
142
+ <tr>
143
+ <th class="text-muted">Security Group:</th>
144
+ <td><span class="badge bg-primary text-white">{{ session_data.security_group }}</span></td>
145
+ </tr>
146
+ {% endif %}
147
+ </table>
148
+ {% else %}
149
+ <div class="text-center text-muted py-4">
150
+ <i class="mdi mdi-lan-disconnect mdi-48px"></i>
151
+ <p class="mt-2">No active connection</p>
152
+ </div>
153
+ {% endif %}
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Custom Attributes -->
160
+ {% if ise_data.custom_attributes %}
161
+ <div class="row">
162
+ <div class="col-12 mb-3">
163
+ <div class="card">
164
+ <div class="card-header">
165
+ <h5 class="card-title mb-0">
166
+ <i class="mdi mdi-tag-multiple"></i> Custom Attributes
167
+ </h5>
168
+ </div>
169
+ <div class="card-body">
170
+ <table class="table table-sm table-hover">
171
+ <thead>
172
+ <tr>
173
+ <th>Attribute</th>
174
+ <th>Value</th>
175
+ </tr>
176
+ </thead>
177
+ <tbody>
178
+ {% for key, value in ise_data.custom_attributes.items %}
179
+ <tr>
180
+ <td>{{ key }}</td>
181
+ <td>{{ value }}</td>
182
+ </tr>
183
+ {% endfor %}
184
+ </tbody>
185
+ </table>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ {% endif %}
191
+
192
+ <!-- Cache indicator and external link -->
193
+ <div class="mt-3 d-flex justify-content-between align-items-center">
194
+ <div>
195
+ {% if ise_data.cached %}
196
+ <span class="text-muted small">
197
+ <i class="mdi mdi-cached"></i> Data from cache
198
+ </span>
199
+ {% endif %}
200
+ </div>
201
+ <div>
202
+ {% if ise_url %}
203
+ <a href="{{ ise_url }}" target="_blank" class="btn btn-outline-primary btn-sm">
204
+ <i class="mdi mdi-open-in-new"></i> Open Cisco ISE
205
+ </a>
206
+ {% endif %}
207
+ </div>
208
+ </div>
209
+
210
+ {% else %}
211
+ <div class="alert alert-info">
212
+ <i class="mdi mdi-information"></i> No endpoint data available from ISE.
213
+ </div>
214
+ {% endif %}
215
+ </div>
216
+ </div>
@@ -0,0 +1,193 @@
1
+ {% load helpers %}
2
+
3
+ <div class="row">
4
+ <div class="col-12">
5
+ {% if error %}
6
+ <div class="alert alert-warning">
7
+ <i class="mdi mdi-alert"></i> {{ error }}
8
+ </div>
9
+ {% elif ise_data %}
10
+ <div class="row">
11
+ <!-- NAD Registration Status Card -->
12
+ <div class="col-md-4 mb-3">
13
+ <div class="card h-100">
14
+ <div class="card-header">
15
+ <h5 class="card-title mb-0">
16
+ <i class="mdi mdi-check-decagram"></i> ISE Registration
17
+ </h5>
18
+ </div>
19
+ <div class="card-body text-center">
20
+ <div class="p-3 mb-2 bg-success rounded">
21
+ <span class="text-white fs-3 fw-bold">
22
+ <i class="mdi mdi-check-circle"></i> Registered
23
+ </span>
24
+ </div>
25
+ {% if ise_data.name %}
26
+ <div class="mt-2">
27
+ <span class="text-muted">NAD Name:</span>
28
+ <strong class="ms-2">{{ ise_data.name }}</strong>
29
+ </div>
30
+ {% endif %}
31
+ {% if ise_data.profile_name %}
32
+ <div class="mt-1">
33
+ <span class="text-muted">Profile:</span>
34
+ <span class="badge bg-info text-dark ms-2">{{ ise_data.profile_name }}</span>
35
+ </div>
36
+ {% endif %}
37
+ </div>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- Device Details Card -->
42
+ <div class="col-md-4 mb-3">
43
+ <div class="card h-100">
44
+ <div class="card-header">
45
+ <h5 class="card-title mb-0">
46
+ <i class="mdi mdi-router"></i> Device Details
47
+ </h5>
48
+ </div>
49
+ <div class="card-body">
50
+ <table class="table table-sm table-borderless mb-0">
51
+ {% if ise_data.ip_addresses %}
52
+ <tr>
53
+ <th class="text-muted" style="width: 40%">IP Address(es):</th>
54
+ <td>
55
+ {% for ip in ise_data.ip_addresses %}
56
+ <code>{{ ip }}</code>{% if not forloop.last %}, {% endif %}
57
+ {% endfor %}
58
+ </td>
59
+ </tr>
60
+ {% endif %}
61
+ {% if ise_data.model_name %}
62
+ <tr>
63
+ <th class="text-muted">Model:</th>
64
+ <td>{{ ise_data.model_name }}</td>
65
+ </tr>
66
+ {% endif %}
67
+ {% if ise_data.software_version %}
68
+ <tr>
69
+ <th class="text-muted">Software:</th>
70
+ <td>{{ ise_data.software_version }}</td>
71
+ </tr>
72
+ {% endif %}
73
+ {% if ise_data.description %}
74
+ <tr>
75
+ <th class="text-muted">Description:</th>
76
+ <td>{{ ise_data.description }}</td>
77
+ </tr>
78
+ {% endif %}
79
+ {% if ise_data.coA_port %}
80
+ <tr>
81
+ <th class="text-muted">CoA Port:</th>
82
+ <td>{{ ise_data.coA_port }}</td>
83
+ </tr>
84
+ {% endif %}
85
+ </table>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Authentication Settings Card -->
91
+ <div class="col-md-4 mb-3">
92
+ <div class="card h-100">
93
+ <div class="card-header">
94
+ <h5 class="card-title mb-0">
95
+ <i class="mdi mdi-shield-key"></i> Authentication Settings
96
+ </h5>
97
+ </div>
98
+ <div class="card-body">
99
+ <table class="table table-sm table-borderless mb-0">
100
+ <tr>
101
+ <th class="text-muted" style="width: 50%">RADIUS:</th>
102
+ <td>
103
+ {% if ise_data.authentication_settings.radius_enabled %}
104
+ <span class="badge bg-success text-white">Enabled</span>
105
+ {% else %}
106
+ <span class="badge bg-warning text-dark">Disabled</span>
107
+ {% endif %}
108
+ </td>
109
+ </tr>
110
+ <tr>
111
+ <th class="text-muted">TACACS+:</th>
112
+ <td>
113
+ {% if ise_data.tacacs_settings.enabled %}
114
+ <span class="badge bg-success text-white">Enabled</span>
115
+ {% else %}
116
+ <span class="badge bg-warning text-dark">Disabled</span>
117
+ {% endif %}
118
+ </td>
119
+ </tr>
120
+ <tr>
121
+ <th class="text-muted">TrustSec:</th>
122
+ <td>
123
+ {% if ise_data.trustsec_enabled %}
124
+ <span class="badge bg-success text-white">Enabled</span>
125
+ {% else %}
126
+ <span class="badge bg-warning text-dark">Disabled</span>
127
+ {% endif %}
128
+ </td>
129
+ </tr>
130
+ {% if ise_data.snmp_settings.version %}
131
+ <tr>
132
+ <th class="text-muted">SNMP:</th>
133
+ <td>{{ ise_data.snmp_settings.version }}</td>
134
+ </tr>
135
+ {% endif %}
136
+ </table>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Network Device Groups -->
143
+ {% if ise_data.groups %}
144
+ <div class="row">
145
+ <div class="col-12 mb-3">
146
+ <div class="card">
147
+ <div class="card-header">
148
+ <h5 class="card-title mb-0">
149
+ <i class="mdi mdi-folder-network"></i> Network Device Groups
150
+ </h5>
151
+ </div>
152
+ <div class="card-body">
153
+ <div class="row">
154
+ {% for category, value in ise_data.groups.items %}
155
+ <div class="col-md-3 col-sm-6 mb-2">
156
+ <div class="border rounded p-2">
157
+ <div class="text-muted small">{{ category }}</div>
158
+ <div class="fw-bold">{{ value }}</div>
159
+ </div>
160
+ </div>
161
+ {% endfor %}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ {% endif %}
168
+
169
+ <!-- Cache indicator and external link -->
170
+ <div class="mt-3 d-flex justify-content-between align-items-center">
171
+ <div>
172
+ {% if ise_data.cached %}
173
+ <span class="text-muted small">
174
+ <i class="mdi mdi-cached"></i> Data from cache
175
+ </span>
176
+ {% endif %}
177
+ </div>
178
+ <div>
179
+ {% if ise_url %}
180
+ <a href="{{ ise_url }}" target="_blank" class="btn btn-outline-primary btn-sm">
181
+ <i class="mdi mdi-open-in-new"></i> Open Cisco ISE
182
+ </a>
183
+ {% endif %}
184
+ </div>
185
+ </div>
186
+
187
+ {% else %}
188
+ <div class="alert alert-info">
189
+ <i class="mdi mdi-information"></i> This device is not registered as a Network Access Device in ISE.
190
+ </div>
191
+ {% endif %}
192
+ </div>
193
+ </div>
@@ -4,9 +4,10 @@ URL routing for NetBox Cisco ISE Plugin
4
4
 
5
5
  from django.urls import path
6
6
 
7
- from .views import ISESettingsView, TestConnectionView
7
+ from .views import DeviceISEContentView, ISESettingsView, TestConnectionView
8
8
 
9
9
  urlpatterns = [
10
10
  path("settings/", ISESettingsView.as_view(), name="settings"),
11
11
  path("test-connection/", TestConnectionView.as_view(), name="test_connection"),
12
+ path("device/<int:pk>/content/", DeviceISEContentView.as_view(), name="device_content"),
12
13
  ]
@@ -9,8 +9,10 @@ import re
9
9
 
10
10
  from dcim.models import Device
11
11
  from django.conf import settings
12
- from django.http import JsonResponse
12
+ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
13
+ from django.http import HttpResponse, JsonResponse
13
14
  from django.shortcuts import render
15
+ from django.template.loader import render_to_string
14
16
  from django.views import View
15
17
  from netbox.views import generic
16
18
  from utilities.views import ViewTab, register_model_view
@@ -51,10 +53,18 @@ def get_device_lookup_method(device):
51
53
 
52
54
  # Get device info for matching
53
55
  manufacturer = device.device_type.manufacturer
54
- manufacturer_slug = manufacturer.slug.lower() if manufacturer and manufacturer.slug else ""
55
- manufacturer_name = manufacturer.name.lower() if manufacturer and manufacturer.name else ""
56
- device_type_slug = device.device_type.slug.lower() if device.device_type.slug else ""
57
- device_type_model = device.device_type.model.lower() if device.device_type.model else ""
56
+ manufacturer_slug = (
57
+ manufacturer.slug.lower() if manufacturer and manufacturer.slug else ""
58
+ )
59
+ manufacturer_name = (
60
+ manufacturer.name.lower() if manufacturer and manufacturer.name else ""
61
+ )
62
+ device_type_slug = (
63
+ device.device_type.slug.lower() if device.device_type.slug else ""
64
+ )
65
+ device_type_model = (
66
+ device.device_type.model.lower() if device.device_type.model else ""
67
+ )
58
68
 
59
69
  # Check each mapping
60
70
  for mapping in mappings:
@@ -66,12 +76,15 @@ def get_device_lookup_method(device):
66
76
  manufacturer_match = False
67
77
  if manufacturer_pattern:
68
78
  try:
69
- if re.search(manufacturer_pattern, manufacturer_slug, re.IGNORECASE) or re.search(
70
- manufacturer_pattern, manufacturer_name, re.IGNORECASE
71
- ):
79
+ if re.search(
80
+ manufacturer_pattern, manufacturer_slug, re.IGNORECASE
81
+ ) or re.search(manufacturer_pattern, manufacturer_name, re.IGNORECASE):
72
82
  manufacturer_match = True
73
83
  except re.error:
74
- if manufacturer_pattern in manufacturer_slug or manufacturer_pattern in manufacturer_name:
84
+ if (
85
+ manufacturer_pattern in manufacturer_slug
86
+ or manufacturer_pattern in manufacturer_name
87
+ ):
75
88
  manufacturer_match = True
76
89
 
77
90
  if not manufacturer_match:
@@ -81,12 +94,15 @@ def get_device_lookup_method(device):
81
94
  if device_type_pattern:
82
95
  device_type_match = False
83
96
  try:
84
- if re.search(device_type_pattern, device_type_slug, re.IGNORECASE) or re.search(
85
- device_type_pattern, device_type_model, re.IGNORECASE
86
- ):
97
+ if re.search(
98
+ device_type_pattern, device_type_slug, re.IGNORECASE
99
+ ) or re.search(device_type_pattern, device_type_model, re.IGNORECASE):
87
100
  device_type_match = True
88
101
  except re.error:
89
- if device_type_pattern in device_type_slug or device_type_pattern in device_type_model:
102
+ if (
103
+ device_type_pattern in device_type_slug
104
+ or device_type_pattern in device_type_model
105
+ ):
90
106
  device_type_match = True
91
107
 
92
108
  if not device_type_match:
@@ -119,10 +135,10 @@ def should_show_ise_tab(device):
119
135
 
120
136
  @register_model_view(Device, name="cisco_ise", path="cisco-ise")
121
137
  class DeviceISEView(generic.ObjectView):
122
- """Display Cisco ISE endpoint/NAD details for a Device."""
138
+ """Display Cisco ISE endpoint/NAD details for a Device with async loading."""
123
139
 
124
140
  queryset = Device.objects.all()
125
- template_name = "netbox_cisco_ise/endpoint_tab.html"
141
+ template_name = "netbox_cisco_ise/device_tab.html"
126
142
 
127
143
  tab = ViewTab(
128
144
  label="Cisco ISE",
@@ -133,8 +149,31 @@ class DeviceISEView(generic.ObjectView):
133
149
  )
134
150
 
135
151
  def get(self, request, pk):
136
- """Handle GET request for the ISE tab."""
137
- device = Device.objects.select_related("device_type__manufacturer").prefetch_related("interfaces").get(pk=pk)
152
+ """Render initial tab with loading spinner - content loads via htmx."""
153
+ device = Device.objects.get(pk=pk)
154
+ return render(
155
+ request,
156
+ self.template_name,
157
+ {
158
+ "object": device,
159
+ "tab": self.tab,
160
+ "loading": True,
161
+ },
162
+ )
163
+
164
+
165
+ class DeviceISEContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
166
+ """HTMX endpoint that returns ISE content for async loading."""
167
+
168
+ permission_required = "dcim.view_device"
169
+
170
+ def get(self, request, pk):
171
+ """Fetch ISE data and return HTML content."""
172
+ device = (
173
+ Device.objects.select_related("device_type__manufacturer")
174
+ .prefetch_related("interfaces")
175
+ .get(pk=pk)
176
+ )
138
177
 
139
178
  client = get_client()
140
179
  config = settings.PLUGINS_CONFIG.get("netbox_cisco_ise", {})
@@ -156,7 +195,10 @@ class DeviceISEView(generic.ObjectView):
156
195
  if "error" not in ise_data:
157
196
  # Also get session data for connected endpoints
158
197
  session_data = client.get_active_session_by_mac(mac_address)
159
- if "error" in session_data and session_data.get("connected") is False:
198
+ if (
199
+ "error" in session_data
200
+ and session_data.get("connected") is False
201
+ ):
160
202
  # Not connected is fine, just no active session
161
203
  pass
162
204
  else:
@@ -177,8 +219,14 @@ class DeviceISEView(generic.ObjectView):
177
219
  ise_data = client.get_network_device_by_ip(management_ip)
178
220
 
179
221
  # If IP lookup failed or no IP, try hostname
180
- if ("error" in ise_data or not ise_data) and device.name:
181
- ise_data = client.get_network_device_by_name(device.name)
222
+ # Use VC name for virtual chassis members (original hostname)
223
+ lookup_hostname = (
224
+ device.virtual_chassis.name
225
+ if device.virtual_chassis
226
+ else device.name
227
+ )
228
+ if ("error" in ise_data or not ise_data) and lookup_hostname:
229
+ ise_data = client.get_network_device_by_name(lookup_hostname)
182
230
 
183
231
  if "error" in ise_data:
184
232
  error = ise_data.get("error")
@@ -191,21 +239,22 @@ class DeviceISEView(generic.ObjectView):
191
239
 
192
240
  # Choose template based on lookup type
193
241
  if ise_data.get("is_nad"):
194
- template = "netbox_cisco_ise/nad_tab.html"
242
+ template = "netbox_cisco_ise/nad_tab_content.html"
195
243
  else:
196
- template = self.template_name
197
-
198
- return render(
199
- request,
200
- template,
201
- {
202
- "object": device,
203
- "tab": self.tab,
204
- "ise_data": ise_data,
205
- "session_data": session_data,
206
- "error": error,
207
- "ise_url": ise_url,
208
- },
244
+ template = "netbox_cisco_ise/endpoint_tab_content.html"
245
+
246
+ return HttpResponse(
247
+ render_to_string(
248
+ template,
249
+ {
250
+ "object": device,
251
+ "ise_data": ise_data,
252
+ "session_data": session_data,
253
+ "error": error,
254
+ "ise_url": ise_url,
255
+ },
256
+ request=request,
257
+ )
209
258
  )
210
259
 
211
260
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-cisco-ise
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: NetBox plugin for Cisco ISE integration - endpoint tracking, NAD management, and session visibility
5
5
  Author-email: sieteunoseis <jeremy.worden@gmail.com>
6
6
  License: Apache-2.0
@@ -273,6 +273,12 @@ Contributions are welcome! Please:
273
273
  2. Create a feature branch
274
274
  3. Submit a pull request
275
275
 
276
+ ## Support
277
+
278
+ If you find this plugin helpful, consider supporting development:
279
+
280
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/automatebldrs)
281
+
276
282
  ## Related Projects
277
283
 
278
284
  - [netbox-catalyst-center](https://github.com/sieteunoseis/netbox-catalyst-center) - Catalyst Center integration for NetBox
@@ -11,6 +11,9 @@ netbox_cisco_ise.egg-info/SOURCES.txt
11
11
  netbox_cisco_ise.egg-info/dependency_links.txt
12
12
  netbox_cisco_ise.egg-info/requires.txt
13
13
  netbox_cisco_ise.egg-info/top_level.txt
14
+ netbox_cisco_ise/templates/netbox_cisco_ise/device_tab.html
14
15
  netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab.html
16
+ netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab_content.html
15
17
  netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab.html
18
+ netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab_content.html
16
19
  netbox_cisco_ise/templates/netbox_cisco_ise/settings.html
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "netbox-cisco-ise"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "NetBox plugin for Cisco ISE integration - endpoint tracking, NAD management, and session visibility"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}