netbox-cisco-ise 0.1.4__py3-none-any.whl → 0.1.7__py3-none-any.whl

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.
@@ -5,9 +5,13 @@ Display Cisco Identity Services Engine (ISE) endpoint and NAD information in Dev
5
5
  Shows endpoint identity, profiling data, active session status, and network access device details.
6
6
  """
7
7
 
8
+ import logging
9
+
8
10
  from netbox.plugins import PluginConfig
9
11
 
10
- __version__ = "0.1.4"
12
+ __version__ = "0.1.7"
13
+
14
+ logger = logging.getLogger(__name__)
11
15
 
12
16
 
13
17
  class CiscoISEConfig(PluginConfig):
@@ -54,7 +58,80 @@ class CiscoISEConfig(PluginConfig):
54
58
  "lookup": "nad",
55
59
  }, # Default: Cisco devices as NADs
56
60
  ],
61
+ # Endpoint mappings (requires netbox-endpoints plugin)
62
+ # Format: list of dicts with manufacturer (regex), endpoint_type (regex, optional)
63
+ # All endpoints use MAC lookup since they're endpoint devices
64
+ #
65
+ # Example:
66
+ # "endpoint_mappings": [
67
+ # {"manufacturer": "vocera"}, # All Vocera endpoints
68
+ # {"manufacturer": "cisco", "endpoint_type": ".*phone.*"}, # Cisco phones
69
+ # ]
70
+ # If empty, shows tab for ALL endpoints with a MAC address
71
+ "endpoint_mappings": [],
57
72
  }
58
73
 
74
+ def ready(self):
75
+ """Register endpoint view if netbox_endpoints is available."""
76
+ super().ready()
77
+ self._register_endpoint_views()
78
+
79
+ def _register_endpoint_views(self):
80
+ """Register Cisco ISE tab for Endpoints if plugin is installed."""
81
+ import sys
82
+
83
+ # Quick check if netbox_endpoints is available
84
+ if "netbox_endpoints" not in sys.modules:
85
+ try:
86
+ import importlib.util
87
+
88
+ if importlib.util.find_spec("netbox_endpoints") is None:
89
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
90
+ return
91
+ except Exception:
92
+ logger.debug("netbox_endpoints not available, skipping endpoint view registration")
93
+ return
94
+
95
+ try:
96
+ from django.shortcuts import render
97
+ from netbox.views import generic
98
+ from netbox_endpoints.models import Endpoint
99
+ from utilities.views import ViewTab, register_model_view
100
+
101
+ from .views import should_show_ise_tab_endpoint
102
+
103
+ @register_model_view(Endpoint, name="cisco_ise", path="cisco-ise")
104
+ class EndpointISEView(generic.ObjectView):
105
+ """Display Cisco ISE endpoint details for a netbox Endpoint."""
106
+
107
+ queryset = Endpoint.objects.all()
108
+ template_name = "netbox_cisco_ise/netbox_endpoint_tab.html"
109
+
110
+ tab = ViewTab(
111
+ label="Cisco ISE",
112
+ weight=9001,
113
+ permission="netbox_endpoints.view_endpoint",
114
+ hide_if_empty=False,
115
+ visible=should_show_ise_tab_endpoint,
116
+ )
117
+
118
+ def get(self, request, pk):
119
+ endpoint = Endpoint.objects.get(pk=pk)
120
+ return render(
121
+ request,
122
+ self.template_name,
123
+ {
124
+ "object": endpoint,
125
+ "tab": self.tab,
126
+ "loading": True,
127
+ },
128
+ )
129
+
130
+ logger.info("Registered Cisco ISE tab for Endpoint model")
131
+ except ImportError:
132
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
133
+ except Exception as e:
134
+ logger.warning(f"Could not register endpoint views: {e}")
135
+
59
136
 
60
137
  config = CiscoISEConfig
@@ -275,9 +275,7 @@ class ISEClient:
275
275
  cached["cached"] = True
276
276
  return cached
277
277
 
278
- result = self._make_ers_request(
279
- "/networkdevice", params={"filter": f"ipaddress.EQ.{ip_address}"}
280
- )
278
+ result = self._make_ers_request("/networkdevice", params={"filter": f"ipaddress.EQ.{ip_address}"})
281
279
 
282
280
  return self._process_nad_result(result, cache_key)
283
281
 
@@ -297,9 +295,7 @@ class ISEClient:
297
295
  cached["cached"] = True
298
296
  return cached
299
297
 
300
- result = self._make_ers_request(
301
- "/networkdevice", params={"filter": f"name.CONTAINS.{name}"}
302
- )
298
+ result = self._make_ers_request("/networkdevice", params={"filter": f"name.CONTAINS.{name}"})
303
299
 
304
300
  return self._process_nad_result(result, cache_key)
305
301
 
@@ -365,11 +361,7 @@ class ISEClient:
365
361
  "ro_community": bool(snmp_settings.get("roCommunity")),
366
362
  "polling_interval": snmp_settings.get("pollingInterval"),
367
363
  },
368
- "trustsec_enabled": bool(
369
- trustsec_settings.get("deviceAuthenticationSettings", {}).get(
370
- "sgaDeviceId"
371
- )
372
- ),
364
+ "trustsec_enabled": bool(trustsec_settings.get("deviceAuthenticationSettings", {}).get("sgaDeviceId")),
373
365
  "coA_port": nad.get("coaPort"),
374
366
  "cached": False,
375
367
  }
@@ -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>
@@ -0,0 +1,24 @@
1
+ {% extends 'netbox_endpoints/endpoint.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block content %}
5
+ {% if loading %}
6
+ <div id="ise-content"
7
+ hx-get="{% url 'plugins:netbox_cisco_ise:endpoint_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 %}
netbox_cisco_ise/urls.py CHANGED
@@ -4,9 +4,18 @@ 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 ENDPOINTS_PLUGIN_INSTALLED, 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
  ]
14
+
15
+ # Add endpoint URLs if netbox_endpoints is installed
16
+ if ENDPOINTS_PLUGIN_INSTALLED:
17
+ from .views import EndpointISEContentView
18
+
19
+ urlpatterns.append(
20
+ path("endpoint/<int:pk>/content/", EndpointISEContentView.as_view(), name="endpoint_content"),
21
+ )
netbox_cisco_ise/views.py CHANGED
@@ -9,14 +9,24 @@ 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
17
19
 
18
20
  from .ise_client import get_client
19
21
 
22
+ # Check if netbox_endpoints plugin is installed
23
+ try:
24
+ from netbox_endpoints.models import Endpoint
25
+
26
+ ENDPOINTS_PLUGIN_INSTALLED = True
27
+ except ImportError:
28
+ ENDPOINTS_PLUGIN_INSTALLED = False
29
+
20
30
 
21
31
  def is_valid_mac(value):
22
32
  """Check if a value looks like a MAC address."""
@@ -51,18 +61,10 @@ def get_device_lookup_method(device):
51
61
 
52
62
  # Get device info for matching
53
63
  manufacturer = device.device_type.manufacturer
54
- manufacturer_slug = (
55
- manufacturer.slug.lower() if manufacturer and manufacturer.slug else ""
56
- )
57
- manufacturer_name = (
58
- manufacturer.name.lower() if manufacturer and manufacturer.name else ""
59
- )
60
- device_type_slug = (
61
- device.device_type.slug.lower() if device.device_type.slug else ""
62
- )
63
- device_type_model = (
64
- device.device_type.model.lower() if device.device_type.model else ""
65
- )
64
+ manufacturer_slug = manufacturer.slug.lower() if manufacturer and manufacturer.slug else ""
65
+ manufacturer_name = manufacturer.name.lower() if manufacturer and manufacturer.name else ""
66
+ device_type_slug = device.device_type.slug.lower() if device.device_type.slug else ""
67
+ device_type_model = device.device_type.model.lower() if device.device_type.model else ""
66
68
 
67
69
  # Check each mapping
68
70
  for mapping in mappings:
@@ -74,15 +76,12 @@ def get_device_lookup_method(device):
74
76
  manufacturer_match = False
75
77
  if manufacturer_pattern:
76
78
  try:
77
- if re.search(
78
- manufacturer_pattern, manufacturer_slug, re.IGNORECASE
79
- ) or re.search(manufacturer_pattern, manufacturer_name, re.IGNORECASE):
79
+ if re.search(manufacturer_pattern, manufacturer_slug, re.IGNORECASE) or re.search(
80
+ manufacturer_pattern, manufacturer_name, re.IGNORECASE
81
+ ):
80
82
  manufacturer_match = True
81
83
  except re.error:
82
- if (
83
- manufacturer_pattern in manufacturer_slug
84
- or manufacturer_pattern in manufacturer_name
85
- ):
84
+ if manufacturer_pattern in manufacturer_slug or manufacturer_pattern in manufacturer_name:
86
85
  manufacturer_match = True
87
86
 
88
87
  if not manufacturer_match:
@@ -92,15 +91,12 @@ def get_device_lookup_method(device):
92
91
  if device_type_pattern:
93
92
  device_type_match = False
94
93
  try:
95
- if re.search(
96
- device_type_pattern, device_type_slug, re.IGNORECASE
97
- ) or re.search(device_type_pattern, device_type_model, re.IGNORECASE):
94
+ if re.search(device_type_pattern, device_type_slug, re.IGNORECASE) or re.search(
95
+ device_type_pattern, device_type_model, re.IGNORECASE
96
+ ):
98
97
  device_type_match = True
99
98
  except re.error:
100
- if (
101
- device_type_pattern in device_type_slug
102
- or device_type_pattern in device_type_model
103
- ):
99
+ if device_type_pattern in device_type_slug or device_type_pattern in device_type_model:
104
100
  device_type_match = True
105
101
 
106
102
  if not device_type_match:
@@ -133,10 +129,10 @@ def should_show_ise_tab(device):
133
129
 
134
130
  @register_model_view(Device, name="cisco_ise", path="cisco-ise")
135
131
  class DeviceISEView(generic.ObjectView):
136
- """Display Cisco ISE endpoint/NAD details for a Device."""
132
+ """Display Cisco ISE endpoint/NAD details for a Device with async loading."""
137
133
 
138
134
  queryset = Device.objects.all()
139
- template_name = "netbox_cisco_ise/endpoint_tab.html"
135
+ template_name = "netbox_cisco_ise/device_tab.html"
140
136
 
141
137
  tab = ViewTab(
142
138
  label="Cisco ISE",
@@ -147,13 +143,28 @@ class DeviceISEView(generic.ObjectView):
147
143
  )
148
144
 
149
145
  def get(self, request, pk):
150
- """Handle GET request for the ISE tab."""
151
- device = (
152
- Device.objects.select_related("device_type__manufacturer")
153
- .prefetch_related("interfaces")
154
- .get(pk=pk)
146
+ """Render initial tab with loading spinner - content loads via htmx."""
147
+ device = Device.objects.get(pk=pk)
148
+ return render(
149
+ request,
150
+ self.template_name,
151
+ {
152
+ "object": device,
153
+ "tab": self.tab,
154
+ "loading": True,
155
+ },
155
156
  )
156
157
 
158
+
159
+ class DeviceISEContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
160
+ """HTMX endpoint that returns ISE content for async loading."""
161
+
162
+ permission_required = "dcim.view_device"
163
+
164
+ def get(self, request, pk):
165
+ """Fetch ISE data and return HTML content."""
166
+ device = Device.objects.select_related("device_type__manufacturer").prefetch_related("interfaces").get(pk=pk)
167
+
157
168
  client = get_client()
158
169
  config = settings.PLUGINS_CONFIG.get("netbox_cisco_ise", {})
159
170
 
@@ -174,10 +185,7 @@ class DeviceISEView(generic.ObjectView):
174
185
  if "error" not in ise_data:
175
186
  # Also get session data for connected endpoints
176
187
  session_data = client.get_active_session_by_mac(mac_address)
177
- if (
178
- "error" in session_data
179
- and session_data.get("connected") is False
180
- ):
188
+ if "error" in session_data and session_data.get("connected") is False:
181
189
  # Not connected is fine, just no active session
182
190
  pass
183
191
  else:
@@ -199,11 +207,7 @@ class DeviceISEView(generic.ObjectView):
199
207
 
200
208
  # If IP lookup failed or no IP, try hostname
201
209
  # Use VC name for virtual chassis members (original hostname)
202
- lookup_hostname = (
203
- device.virtual_chassis.name
204
- if device.virtual_chassis
205
- else device.name
206
- )
210
+ lookup_hostname = device.virtual_chassis.name if device.virtual_chassis else device.name
207
211
  if ("error" in ise_data or not ise_data) and lookup_hostname:
208
212
  ise_data = client.get_network_device_by_name(lookup_hostname)
209
213
 
@@ -218,21 +222,22 @@ class DeviceISEView(generic.ObjectView):
218
222
 
219
223
  # Choose template based on lookup type
220
224
  if ise_data.get("is_nad"):
221
- template = "netbox_cisco_ise/nad_tab.html"
225
+ template = "netbox_cisco_ise/nad_tab_content.html"
222
226
  else:
223
- template = self.template_name
224
-
225
- return render(
226
- request,
227
- template,
228
- {
229
- "object": device,
230
- "tab": self.tab,
231
- "ise_data": ise_data,
232
- "session_data": session_data,
233
- "error": error,
234
- "ise_url": ise_url,
235
- },
227
+ template = "netbox_cisco_ise/endpoint_tab_content.html"
228
+
229
+ return HttpResponse(
230
+ render_to_string(
231
+ template,
232
+ {
233
+ "object": device,
234
+ "ise_data": ise_data,
235
+ "session_data": session_data,
236
+ "error": error,
237
+ "ise_url": ise_url,
238
+ },
239
+ request=request,
240
+ )
236
241
  )
237
242
 
238
243
 
@@ -276,3 +281,130 @@ class TestConnectionView(View):
276
281
  return JsonResponse(result, status=400)
277
282
 
278
283
  return JsonResponse(result)
284
+
285
+
286
+ # Endpoint-specific functions for netbox_endpoints plugin
287
+ def should_show_ise_tab_endpoint(endpoint):
288
+ """
289
+ Determine if the ISE tab should be visible for this endpoint.
290
+
291
+ Shows tab if endpoint has a MAC address and matches configured endpoint_mappings
292
+ (or if endpoint_mappings is empty, show for all endpoints with MAC).
293
+ """
294
+ if not ENDPOINTS_PLUGIN_INSTALLED:
295
+ return False
296
+
297
+ # Must have MAC address for ISE lookup
298
+ if not endpoint.mac_address:
299
+ return False
300
+
301
+ config = settings.PLUGINS_CONFIG.get("netbox_cisco_ise", {})
302
+ mappings = config.get("endpoint_mappings", [])
303
+
304
+ # If no mappings configured, show for all endpoints with MAC
305
+ if not mappings:
306
+ return True
307
+
308
+ # Check if endpoint matches any mapping
309
+ if not endpoint.endpoint_type:
310
+ return False
311
+
312
+ manufacturer = endpoint.endpoint_type.manufacturer
313
+ manufacturer_slug = manufacturer.slug.lower() if manufacturer and manufacturer.slug else ""
314
+ manufacturer_name = manufacturer.name.lower() if manufacturer and manufacturer.name else ""
315
+ endpoint_type_slug = endpoint.endpoint_type.slug.lower() if endpoint.endpoint_type.slug else ""
316
+ endpoint_type_model = endpoint.endpoint_type.model.lower() if endpoint.endpoint_type.model else ""
317
+
318
+ for mapping in mappings:
319
+ manufacturer_pattern = mapping.get("manufacturer", "").lower()
320
+ endpoint_type_pattern = mapping.get("endpoint_type", "").lower()
321
+
322
+ # Check manufacturer match
323
+ manufacturer_match = False
324
+ if manufacturer_pattern:
325
+ try:
326
+ if re.search(manufacturer_pattern, manufacturer_slug, re.IGNORECASE) or re.search(
327
+ manufacturer_pattern, manufacturer_name, re.IGNORECASE
328
+ ):
329
+ manufacturer_match = True
330
+ except re.error:
331
+ if manufacturer_pattern in manufacturer_slug or manufacturer_pattern in manufacturer_name:
332
+ manufacturer_match = True
333
+
334
+ if not manufacturer_match:
335
+ continue
336
+
337
+ # Check endpoint_type match if specified
338
+ if endpoint_type_pattern:
339
+ endpoint_type_match = False
340
+ try:
341
+ if re.search(endpoint_type_pattern, endpoint_type_slug, re.IGNORECASE) or re.search(
342
+ endpoint_type_pattern, endpoint_type_model, re.IGNORECASE
343
+ ):
344
+ endpoint_type_match = True
345
+ except re.error:
346
+ if endpoint_type_pattern in endpoint_type_slug or endpoint_type_pattern in endpoint_type_model:
347
+ endpoint_type_match = True
348
+
349
+ if not endpoint_type_match:
350
+ continue
351
+
352
+ # Mapping matches
353
+ return True
354
+
355
+ return False
356
+
357
+
358
+ # Endpoint views - only available if netbox_endpoints is installed
359
+ if ENDPOINTS_PLUGIN_INSTALLED:
360
+
361
+ class EndpointISEContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
362
+ """HTMX endpoint that returns ISE content for netbox Endpoint async loading."""
363
+
364
+ permission_required = "netbox_endpoints.view_endpoint"
365
+
366
+ def get(self, request, pk):
367
+ """Fetch ISE data and return HTML content."""
368
+ endpoint = Endpoint.objects.select_related("endpoint_type__manufacturer").get(pk=pk)
369
+
370
+ client = get_client()
371
+ config = settings.PLUGINS_CONFIG.get("netbox_cisco_ise", {})
372
+
373
+ ise_data = {}
374
+ session_data = {}
375
+ error = None
376
+
377
+ if not client:
378
+ error = "Cisco ISE not configured. Configure the plugin in NetBox settings."
379
+ elif not endpoint.mac_address:
380
+ error = "No MAC address configured for this endpoint."
381
+ else:
382
+ # Endpoint lookup by MAC address
383
+ mac_address = str(endpoint.mac_address)
384
+ ise_data = client.get_endpoint_by_mac(mac_address)
385
+ if "error" not in ise_data:
386
+ # Also get session data for connected endpoints
387
+ session_data = client.get_active_session_by_mac(mac_address)
388
+ if "error" in session_data and session_data.get("connected") is False:
389
+ # Not connected is fine, just no active session
390
+ pass
391
+ else:
392
+ error = ise_data.get("error")
393
+ ise_data = {}
394
+
395
+ # Get ISE URL for external links
396
+ ise_url = config.get("ise_url", "").rstrip("/")
397
+
398
+ return HttpResponse(
399
+ render_to_string(
400
+ "netbox_cisco_ise/endpoint_tab_content.html",
401
+ {
402
+ "object": endpoint,
403
+ "ise_data": ise_data,
404
+ "session_data": session_data,
405
+ "error": error,
406
+ "ise_url": ise_url,
407
+ },
408
+ request=request,
409
+ )
410
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-cisco-ise
3
- Version: 0.1.4
3
+ Version: 0.1.7
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
@@ -0,0 +1,17 @@
1
+ netbox_cisco_ise/__init__.py,sha256=ZSzSxpy3uFtG427NwWnYD_fZt0_TGRirS0GZxR_KJsc,5267
2
+ netbox_cisco_ise/ise_client.py,sha256=sFK2UUYN8tvUpxjQxdd6BbU5H01NF3E65kQUAzI69k0,16291
3
+ netbox_cisco_ise/navigation.py,sha256=mMN4EyzZl6ehoqflLeYy7logt39wpCN2TEsPfqn1VtI,507
4
+ netbox_cisco_ise/urls.py,sha256=qlIlqkj4jYeDSiWnMDl7dGfoQTvWgigbahUOPUA4tkU,712
5
+ netbox_cisco_ise/views.py,sha256=oay16jnmS-ZWhT9XYSnvbRvWYEoTvZ1s8-wHaczZfF4,15031
6
+ netbox_cisco_ise/templates/netbox_cisco_ise/device_tab.html,sha256=H8xS-cwU-oOCS7Sgo3RMD01UDnqrIKR2KU3G-GyR9O0,775
7
+ netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab.html,sha256=aS7R0oTjkUmqlP7i2zfrFhYs9F-LknJgcomGGK60u7w,9868
8
+ netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab_content.html,sha256=Cn3JDGVLcUv7u9pgK867nDdLBPPdY1ViGsr5KewDuRE,9795
9
+ netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab.html,sha256=Y5rCGyoSqzRmfridJp2BmC3HkPGRhvWdZ3Mp-C81qYk,8783
10
+ netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab_content.html,sha256=0KyCQ1E1hTC3gD_T4FuC_Ll8cuPVVji_ofqCCrTJxHk,8710
11
+ netbox_cisco_ise/templates/netbox_cisco_ise/netbox_endpoint_tab.html,sha256=fMHwjCz_dXa-nSelZdFP0LLpj62WAh1EU-eaMY6xEps,786
12
+ netbox_cisco_ise/templates/netbox_cisco_ise/settings.html,sha256=ZsnJ_AclJUDa-h7vJTd5Clh7pc3OOBx6Tf9Y_bx3oIE,8953
13
+ netbox_cisco_ise-0.1.7.dist-info/licenses/LICENSE,sha256=KmjHs19UP3vo7K2IWXkq3JDKG9PatSbqeLPwu3o2k7g,10761
14
+ netbox_cisco_ise-0.1.7.dist-info/METADATA,sha256=yxm1emfd_6dqWy_gDbLF6Pl5HwlSWUfdmBSomzx37vM,9099
15
+ netbox_cisco_ise-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ netbox_cisco_ise-0.1.7.dist-info/top_level.txt,sha256=LMP1ppZRzqtdaMGzz53KgacW_PEwyLSM9wwIMuBvJ00,17
17
+ netbox_cisco_ise-0.1.7.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- netbox_cisco_ise/__init__.py,sha256=RNZeW8KihZEVJpBGTL_6SBkjgt1tdSeVirRGrE9MC90,2203
2
- netbox_cisco_ise/ise_client.py,sha256=sfeC-cLv0JWo_NdxZG9M4VEGNyLYCVymB6iItGCUvH8,16403
3
- netbox_cisco_ise/navigation.py,sha256=mMN4EyzZl6ehoqflLeYy7logt39wpCN2TEsPfqn1VtI,507
4
- netbox_cisco_ise/urls.py,sha256=3tJHJyEQXYZ2WXw4zq1kds7xpgyHl1-HwVHlgtJA84E,304
5
- netbox_cisco_ise/views.py,sha256=pT8dSh_1LN1PjDkTHEePukUvShXsy49dRr8Mmhudcgw,9422
6
- netbox_cisco_ise/templates/netbox_cisco_ise/endpoint_tab.html,sha256=aS7R0oTjkUmqlP7i2zfrFhYs9F-LknJgcomGGK60u7w,9868
7
- netbox_cisco_ise/templates/netbox_cisco_ise/nad_tab.html,sha256=Y5rCGyoSqzRmfridJp2BmC3HkPGRhvWdZ3Mp-C81qYk,8783
8
- netbox_cisco_ise/templates/netbox_cisco_ise/settings.html,sha256=ZsnJ_AclJUDa-h7vJTd5Clh7pc3OOBx6Tf9Y_bx3oIE,8953
9
- netbox_cisco_ise-0.1.4.dist-info/licenses/LICENSE,sha256=KmjHs19UP3vo7K2IWXkq3JDKG9PatSbqeLPwu3o2k7g,10761
10
- netbox_cisco_ise-0.1.4.dist-info/METADATA,sha256=Q4Iu1gbIBW87E0SwjkbA6TWguH6MnBMOt2CONuPola0,8859
11
- netbox_cisco_ise-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- netbox_cisco_ise-0.1.4.dist-info/top_level.txt,sha256=LMP1ppZRzqtdaMGzz53KgacW_PEwyLSM9wwIMuBvJ00,17
13
- netbox_cisco_ise-0.1.4.dist-info/RECORD,,