netbox-atlassian 0.2.2__py3-none-any.whl → 0.2.4__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 Jira issues and Confluence pages related to devices in NetBox.
5
5
  Searches by configurable fields (hostname, serial, role, etc.) with OR logic.
6
6
  """
7
7
 
8
+ import logging
9
+
8
10
  from netbox.plugins import PluginConfig
9
11
 
10
- __version__ = "0.2.2"
12
+ __version__ = "0.2.4"
13
+
14
+ logger = logging.getLogger(__name__)
11
15
 
12
16
 
13
17
  class AtlassianConfig(PluginConfig):
@@ -53,6 +57,14 @@ class AtlassianConfig(PluginConfig):
53
57
  {"name": "Role", "attribute": "role.name", "enabled": False},
54
58
  {"name": "Primary IP", "attribute": "primary_ip4.address", "enabled": False},
55
59
  ],
60
+ # Endpoint search fields (for netbox-endpoints plugin)
61
+ # Searches use OR logic - matches any field
62
+ "endpoint_search_fields": [
63
+ {"name": "Name", "attribute": "name", "enabled": True},
64
+ {"name": "MAC Address", "attribute": "mac_address", "enabled": True},
65
+ {"name": "Serial", "attribute": "serial", "enabled": True},
66
+ {"name": "Asset Tag", "attribute": "asset_tag", "enabled": False},
67
+ ],
56
68
  # Jira search settings
57
69
  "jira_max_results": 10,
58
70
  "jira_projects": [], # Empty = search all projects
@@ -70,5 +82,67 @@ class AtlassianConfig(PluginConfig):
70
82
  "device_types": [], # e.g., ["cisco", "juniper"]
71
83
  }
72
84
 
85
+ def ready(self):
86
+ """Register endpoint view if netbox_endpoints is available."""
87
+ super().ready()
88
+ self._register_endpoint_views()
89
+
90
+ def _register_endpoint_views(self):
91
+ """Register Atlassian tab for Endpoints if plugin is installed."""
92
+ import sys
93
+
94
+ # Quick check if netbox_endpoints is available
95
+ if "netbox_endpoints" not in sys.modules:
96
+ try:
97
+ import importlib.util
98
+
99
+ if importlib.util.find_spec("netbox_endpoints") is None:
100
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
101
+ return
102
+ except Exception:
103
+ logger.debug("netbox_endpoints not available, skipping endpoint view registration")
104
+ return
105
+
106
+ try:
107
+ from django.shortcuts import render
108
+ from netbox.views import generic
109
+ from netbox_endpoints.models import Endpoint
110
+ from utilities.views import ViewTab, register_model_view
111
+
112
+ from .views import should_show_atlassian_tab_endpoint
113
+
114
+ @register_model_view(Endpoint, name="atlassian", path="atlassian")
115
+ class EndpointAtlassianView(generic.ObjectView):
116
+ """Display Jira issues and Confluence pages for an Endpoint."""
117
+
118
+ queryset = Endpoint.objects.all()
119
+ template_name = "netbox_atlassian/endpoint_tab.html"
120
+
121
+ tab = ViewTab(
122
+ label="Atlassian",
123
+ weight=9100,
124
+ permission="netbox_endpoints.view_endpoint",
125
+ hide_if_empty=False,
126
+ visible=should_show_atlassian_tab_endpoint,
127
+ )
128
+
129
+ def get(self, request, pk):
130
+ endpoint = Endpoint.objects.get(pk=pk)
131
+ return render(
132
+ request,
133
+ self.template_name,
134
+ {
135
+ "object": endpoint,
136
+ "tab": self.tab,
137
+ "loading": True,
138
+ },
139
+ )
140
+
141
+ logger.info("Registered Atlassian tab for Endpoint model")
142
+ except ImportError:
143
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
144
+ except Exception as e:
145
+ logger.warning(f"Could not register endpoint views: {e}")
146
+
73
147
 
74
148
  config = AtlassianConfig
@@ -219,22 +219,26 @@ class AtlassianClient:
219
219
  issues = []
220
220
  for issue in result.get("issues", []):
221
221
  fields = issue.get("fields", {})
222
- issues.append({
223
- "key": issue.get("key"),
224
- "summary": fields.get("summary", ""),
225
- "status": fields.get("status", {}).get("name", ""),
226
- "status_category": fields.get("status", {}).get("statusCategory", {}).get("key", ""),
227
- "type": fields.get("issuetype", {}).get("name", ""),
228
- "type_icon": fields.get("issuetype", {}).get("iconUrl", ""),
229
- "priority": fields.get("priority", {}).get("name", "") if fields.get("priority") else "",
230
- "priority_icon": fields.get("priority", {}).get("iconUrl", "") if fields.get("priority") else "",
231
- "assignee": fields.get("assignee", {}).get("displayName", "") if fields.get("assignee") else "Unassigned",
232
- "created": fields.get("created", ""),
233
- "updated": fields.get("updated", ""),
234
- "project": fields.get("project", {}).get("name", ""),
235
- "project_key": fields.get("project", {}).get("key", ""),
236
- "url": f"{self.jira_url}/browse/{issue.get('key')}",
237
- })
222
+ issues.append(
223
+ {
224
+ "key": issue.get("key"),
225
+ "summary": fields.get("summary", ""),
226
+ "status": fields.get("status", {}).get("name", ""),
227
+ "status_category": fields.get("status", {}).get("statusCategory", {}).get("key", ""),
228
+ "type": fields.get("issuetype", {}).get("name", ""),
229
+ "type_icon": fields.get("issuetype", {}).get("iconUrl", ""),
230
+ "priority": fields.get("priority", {}).get("name", "") if fields.get("priority") else "",
231
+ "priority_icon": fields.get("priority", {}).get("iconUrl", "") if fields.get("priority") else "",
232
+ "assignee": (
233
+ fields.get("assignee", {}).get("displayName", "") if fields.get("assignee") else "Unassigned"
234
+ ),
235
+ "created": fields.get("created", ""),
236
+ "updated": fields.get("updated", ""),
237
+ "project": fields.get("project", {}).get("name", ""),
238
+ "project_key": fields.get("project", {}).get("key", ""),
239
+ "url": f"{self.jira_url}/browse/{issue.get('key')}",
240
+ }
241
+ )
238
242
 
239
243
  response = {
240
244
  "issues": issues,
@@ -308,16 +312,18 @@ class AtlassianClient:
308
312
  ancestors = page.get("ancestors", [])
309
313
  breadcrumb = " > ".join([a.get("title", "") for a in ancestors])
310
314
 
311
- pages.append({
312
- "id": page.get("id"),
313
- "title": page.get("title", ""),
314
- "space_key": space.get("key", ""),
315
- "space_name": space.get("name", ""),
316
- "last_modified": version.get("when", ""),
317
- "last_modified_by": version.get("by", {}).get("displayName", ""),
318
- "breadcrumb": breadcrumb,
319
- "url": f"{self.confluence_url}{page.get('_links', {}).get('webui', '')}",
320
- })
315
+ pages.append(
316
+ {
317
+ "id": page.get("id"),
318
+ "title": page.get("title", ""),
319
+ "space_key": space.get("key", ""),
320
+ "space_name": space.get("name", ""),
321
+ "last_modified": version.get("when", ""),
322
+ "last_modified_by": version.get("by", {}).get("displayName", ""),
323
+ "breadcrumb": breadcrumb,
324
+ "url": f"{self.confluence_url}{page.get('_links', {}).get('webui', '')}",
325
+ }
326
+ )
321
327
 
322
328
  response = {
323
329
  "pages": pages,
@@ -0,0 +1,25 @@
1
+ {% extends 'netbox_endpoints/endpoint.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block content %}
5
+ {% if loading %}
6
+ <div id="atlassian-content"
7
+ hx-get="{% url 'plugins:netbox_atlassian: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 Atlassian data...</p>
16
+ <p class="text-muted small">Searching Jira and Confluence</p>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ {% elif error %}
21
+ <div class="alert alert-warning">
22
+ <i class="mdi mdi-alert"></i> {{ error }}
23
+ </div>
24
+ {% endif %}
25
+ {% endblock %}
netbox_atlassian/urls.py CHANGED
@@ -5,6 +5,7 @@ URL patterns for NetBox Atlassian Plugin
5
5
  from django.urls import path
6
6
 
7
7
  from .views import (
8
+ ENDPOINTS_PLUGIN_INSTALLED,
8
9
  AtlassianSettingsView,
9
10
  DeviceAtlassianContentView,
10
11
  TestConfluenceConnectionView,
@@ -19,3 +20,11 @@ urlpatterns = [
19
20
  path("device/<int:pk>/content/", DeviceAtlassianContentView.as_view(), name="device_content"),
20
21
  path("vm/<int:pk>/content/", VMAtlassianContentView.as_view(), name="vm_content"),
21
22
  ]
23
+
24
+ # Add endpoint URLs if netbox_endpoints is installed
25
+ if ENDPOINTS_PLUGIN_INSTALLED:
26
+ from .views import EndpointAtlassianContentView
27
+
28
+ urlpatterns.append(
29
+ path("endpoint/<int:pk>/content/", EndpointAtlassianContentView.as_view(), name="endpoint_content"),
30
+ )
netbox_atlassian/views.py CHANGED
@@ -22,6 +22,14 @@ from virtualization.models import VirtualMachine
22
22
  from .atlassian_client import get_client
23
23
  from .forms import AtlassianSettingsForm
24
24
 
25
+ # Check if netbox_endpoints plugin is installed
26
+ try:
27
+ from netbox_endpoints.models import Endpoint
28
+
29
+ ENDPOINTS_PLUGIN_INSTALLED = True
30
+ except ImportError:
31
+ ENDPOINTS_PLUGIN_INSTALLED = False
32
+
25
33
 
26
34
  def get_device_attribute(device, attribute_path: str):
27
35
  """
@@ -354,3 +362,145 @@ class TestConfluenceConnectionView(View):
354
362
  if success:
355
363
  return JsonResponse({"success": True, "message": message})
356
364
  return JsonResponse({"success": False, "error": message}, status=400)
365
+
366
+
367
+ # Endpoint-specific functions for netbox_endpoints plugin
368
+ def get_endpoint_attribute(endpoint, attribute_path: str):
369
+ """
370
+ Get an endpoint attribute by dot-separated path.
371
+
372
+ Args:
373
+ endpoint: Endpoint instance
374
+ attribute_path: Dot-separated attribute path (e.g., "name", "mac_address")
375
+
376
+ Returns:
377
+ Attribute value or None if not found
378
+ """
379
+ try:
380
+ value = endpoint
381
+ for part in attribute_path.split("."):
382
+ value = getattr(value, part, None)
383
+ if value is None:
384
+ return None
385
+ # Convert to string for IP addresses and MAC
386
+ if hasattr(value, "ip"):
387
+ return str(value.ip)
388
+ return str(value) if value else None
389
+ except Exception:
390
+ return None
391
+
392
+
393
+ def get_endpoint_search_terms(endpoint) -> list[str]:
394
+ """
395
+ Get search terms from endpoint based on configured endpoint_search_fields.
396
+
397
+ Returns list of non-empty values from enabled search fields.
398
+ """
399
+ config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
400
+ search_fields = config.get(
401
+ "endpoint_search_fields",
402
+ [
403
+ {"name": "Name", "attribute": "name", "enabled": True},
404
+ {"name": "MAC Address", "attribute": "mac_address", "enabled": True},
405
+ {"name": "Serial", "attribute": "serial", "enabled": True},
406
+ ],
407
+ )
408
+
409
+ terms = []
410
+ for field in search_fields:
411
+ if not field.get("enabled", True):
412
+ continue
413
+
414
+ attribute = field.get("attribute", "")
415
+ if not attribute:
416
+ continue
417
+
418
+ value = get_endpoint_attribute(endpoint, attribute)
419
+ if value and value.strip():
420
+ # Split comma-separated values
421
+ if "," in value:
422
+ for part in value.split(","):
423
+ part = part.strip()
424
+ if part and part not in terms:
425
+ terms.append(part)
426
+ else:
427
+ if value.strip() not in terms:
428
+ terms.append(value.strip())
429
+
430
+ return terms
431
+
432
+
433
+ def should_show_atlassian_tab_endpoint(endpoint) -> bool:
434
+ """
435
+ Determine if the Atlassian tab should be visible for this endpoint.
436
+
437
+ Shows tab if endpoint has at least one searchable field value.
438
+ """
439
+ if not ENDPOINTS_PLUGIN_INSTALLED:
440
+ return False
441
+
442
+ terms = get_endpoint_search_terms(endpoint)
443
+ return len(terms) > 0
444
+
445
+
446
+ # Endpoint views - only available if netbox_endpoints is installed
447
+ if ENDPOINTS_PLUGIN_INSTALLED:
448
+
449
+ class EndpointAtlassianContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
450
+ """HTMX endpoint that returns Atlassian content for Endpoint async loading."""
451
+
452
+ permission_required = "netbox_endpoints.view_endpoint"
453
+
454
+ def get(self, request, pk):
455
+ """Fetch Atlassian data and return HTML content."""
456
+ endpoint = Endpoint.objects.get(pk=pk)
457
+
458
+ config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
459
+ client = get_client()
460
+
461
+ # Get search terms from endpoint
462
+ search_terms = get_endpoint_search_terms(endpoint)
463
+
464
+ # Get configured search fields for display
465
+ search_fields = config.get(
466
+ "endpoint_search_fields",
467
+ [
468
+ {"name": "Name", "attribute": "name", "enabled": True},
469
+ {"name": "MAC Address", "attribute": "mac_address", "enabled": True},
470
+ {"name": "Serial", "attribute": "serial", "enabled": True},
471
+ ],
472
+ )
473
+ enabled_fields = [f for f in search_fields if f.get("enabled", True)]
474
+
475
+ # Search Jira and Confluence
476
+ jira_results = {"issues": [], "total": 0, "error": None}
477
+ confluence_results = {"pages": [], "total": 0, "error": None}
478
+
479
+ if search_terms:
480
+ jira_max = config.get("jira_max_results", 10)
481
+ confluence_max = config.get("confluence_max_results", 10)
482
+
483
+ jira_results = client.search_jira(search_terms, max_results=jira_max)
484
+ confluence_results = client.search_confluence(search_terms, max_results=confluence_max)
485
+
486
+ # Get URLs for external links
487
+ jira_url = config.get("jira_url", "").rstrip("/")
488
+ confluence_url = config.get("confluence_url", "").rstrip("/")
489
+
490
+ return HttpResponse(
491
+ render_to_string(
492
+ "netbox_atlassian/tab_content.html",
493
+ {
494
+ "object": endpoint,
495
+ "search_terms": search_terms,
496
+ "enabled_fields": enabled_fields,
497
+ "jira_results": jira_results,
498
+ "confluence_results": confluence_results,
499
+ "jira_url": jira_url,
500
+ "confluence_url": confluence_url,
501
+ "jira_configured": bool(jira_url),
502
+ "confluence_configured": bool(confluence_url),
503
+ },
504
+ request=request,
505
+ )
506
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-atlassian
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: NetBox plugin to display Jira issues and Confluence pages related to devices
5
5
  Author-email: sieteunoseis <jeremy.worden@gmail.com>
6
6
  License: Apache-2.0
@@ -0,0 +1,15 @@
1
+ netbox_atlassian/__init__.py,sha256=vYAzVqKJPclCQITWPJQRRvs01oWyHUHDZvJ-wHuPsTM,5822
2
+ netbox_atlassian/atlassian_client.py,sha256=UEeAzcdr7Ob8tQQ4tIB1LaNmnv2dqbKjKdC2mwp8P7M,13771
3
+ netbox_atlassian/forms.py,sha256=b8MKsU3Ou-c0s7Cga_72m7E3UCCliZF-17woybN66dk,3385
4
+ netbox_atlassian/navigation.py,sha256=tpRgS8qESmxm5eEuCOd1UjEVXEDTMM12H4lEaZk1BRw,497
5
+ netbox_atlassian/urls.py,sha256=K5KCzgvWgYy801yUwf9XHiBdE4gHJoGFtFOZCzoGAJM,1009
6
+ netbox_atlassian/views.py,sha256=JYAp949EqzrUhPyNZ-G3bMXRqPYTt6QXP1cVmZadDE8,17372
7
+ netbox_atlassian/templates/netbox_atlassian/device_tab.html,sha256=yYNhzZHvVSFGa3liRYiHL0KosJv1offXS5v7L0vVGm0,677
8
+ netbox_atlassian/templates/netbox_atlassian/endpoint_tab.html,sha256=wi9hdRlsoLZ2by4wb8NO7XPv8VuTnkF4OTU9xhCteKA,866
9
+ netbox_atlassian/templates/netbox_atlassian/settings.html,sha256=bmiIzzJ6m1vwT5AOcOrdJ7uD3V31GbKUpT4M6TIP3wE,11709
10
+ netbox_atlassian/templates/netbox_atlassian/tab_content.html,sha256=vLH1glj16Pb_JarGJ9LP7FF1ijAt8B-MyZ-FlzITOjI,9473
11
+ netbox_atlassian/templates/netbox_atlassian/vm_tab.html,sha256=NG2Rk019tRLZtVLbrYmq8vakLyt-Gzj3WhGjxjfyn9Q,691
12
+ netbox_atlassian-0.2.4.dist-info/METADATA,sha256=qVwIj9mYzW9FQ9xrDiNxj70gtyejyq0XAUTi1dc_Ut8,6881
13
+ netbox_atlassian-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ netbox_atlassian-0.2.4.dist-info/top_level.txt,sha256=VaKVLTW0o2SE7896PzRTWUaEeYt8CWsryBXI6Ij4ECg,17
15
+ netbox_atlassian-0.2.4.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- netbox_atlassian/__init__.py,sha256=cCI1AqClDapNesA6g-eoEvmXKGoqweNZFiODqrnQ7eE,2835
2
- netbox_atlassian/atlassian_client.py,sha256=NaCwsHOx5kB9bz_g6rtfbjXFsE3J3KdrVY-wewAUIzg,13567
3
- netbox_atlassian/forms.py,sha256=b8MKsU3Ou-c0s7Cga_72m7E3UCCliZF-17woybN66dk,3385
4
- netbox_atlassian/navigation.py,sha256=tpRgS8qESmxm5eEuCOd1UjEVXEDTMM12H4lEaZk1BRw,497
5
- netbox_atlassian/urls.py,sha256=3eLcwzSE6kG-9iDnrgxp6hFmbgRuUYRm196MIieuSF0,700
6
- netbox_atlassian/views.py,sha256=3-5snH3vIGVFdY-Ejdb1oDXJHcwgkZ4re_KueYLKVkE,12068
7
- netbox_atlassian/templates/netbox_atlassian/device_tab.html,sha256=yYNhzZHvVSFGa3liRYiHL0KosJv1offXS5v7L0vVGm0,677
8
- netbox_atlassian/templates/netbox_atlassian/settings.html,sha256=bmiIzzJ6m1vwT5AOcOrdJ7uD3V31GbKUpT4M6TIP3wE,11709
9
- netbox_atlassian/templates/netbox_atlassian/tab_content.html,sha256=vLH1glj16Pb_JarGJ9LP7FF1ijAt8B-MyZ-FlzITOjI,9473
10
- netbox_atlassian/templates/netbox_atlassian/vm_tab.html,sha256=NG2Rk019tRLZtVLbrYmq8vakLyt-Gzj3WhGjxjfyn9Q,691
11
- netbox_atlassian-0.2.2.dist-info/METADATA,sha256=Us88eAq8_jYfZY7YJ30R_na7jppaX8JJia76Rlr3shs,6881
12
- netbox_atlassian-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
13
- netbox_atlassian-0.2.2.dist-info/top_level.txt,sha256=VaKVLTW0o2SE7896PzRTWUaEeYt8CWsryBXI6Ij4ECg,17
14
- netbox_atlassian-0.2.2.dist-info/RECORD,,